Backend Validation
For CLI-driven validation of backends behind OIDC auth. Deliberately tool-specific — Hurl, websocat, oauth2c. If the project has already picked Bruno/Postman/Insomnia, stop and use those instead; don't fork the tooling.
Framing before you test
Before picking flags and writing assertions, work the change as a thinking exercise — the tools come after.
- What feature does this change represent? State it as a user-visible effect, not a function signature. If you can't state it that way, you don't yet know what to validate.
- What does success look like? Be concrete — "a new session appears with status 'active' within 2s" beats "it works." Vague success criteria produce vague tests.
- Validate in two passes. User-lens: drive the feature as a user would (for backend, that often means exercising the endpoints the UI calls, in the order and shape the UI sends them). System-lens: verify the system did the right thing underneath — status codes, DB rows, WS events, logs clean. Bugs hide in exactly one of these; running both catches them.
- Check 2–3 adjacent paths. A
/sessions POSTprobably affects the/sessions/minelisting, any subscribed realtime channel, caches. Don't attempt full regression — pick nearby behaviors that would obviously break. - Cover realistic failure modes (missing auth, invalid body, not-found, race on a shared resource), not exhaustive ones. One test per failure class beats ten happy-path tests.
- Skip what can't plausibly break from this change. Judgment, not ritual.
When in doubt, run --help
Flags drift between tool versions and training data. For anything beyond the common invocations below — reconnect behavior, mTLS, a subprotocol you haven't used, a new oauth2c flag — run hurl --help, websocat --help, or oauth2c --help first rather than guessing. One --help call is cheaper than a debug session chasing a flag that changed meaning.
The stack
The tools are committed: Hurl for HTTP tests, websocat for WebSocket probes, oauth2c for OIDC token flows. All three are cross-platform (Rust/Go binaries). The only OS-specific surface is how the refresh token is cached:
- macOS:
security add-generic-password/security find-generic-password(macOS Keychain) - Linux:
secret-tool(libsecret / GNOME Keyring) — install withapt install libsecret-toolsor equivalent - Windows:
cmdkey+ PowerShell'sCredentialManagermodule, or WSL users reuse the Linux path
The reference script (references/token-script.sh) uses the macOS security command directly. On Linux/Windows, swap those two calls for the platform equivalent; the auth flow itself is identical.
The three orthogonal concerns
Keep these separate in your head and in the code:
- Token acquisition — auth-code + PKCE with the refresh token cached in OS-native secret storage (see above). No passwords in env files, no ROPC against public clients (it fails — see below).
- HTTP validation —
.hurlfiles with[Captures]for chaining auth into calls and[Asserts]for pass/fail. - WebSocket probes —
websocatone-shots withAuthorization: Bearerheader; scripted with-n1, interactive without.
Decision tree
| Situation | Reach for |
|---|---|
| "Does this endpoint work with auth?" | oauth2c for token, then hurl or xh |
| "Is the WebSocket responding?" | websocat with bearer header |
| "Write tests for this API" | .hurl files with [Captures] + [Asserts] |
| Debugging a 401 | Decode JWT claims; check iss, aud, exp, email_verified |
The auth-code + PKCE pattern (why and how)
Public PKCE clients — which most SPAs and mobile apps use at runtime — refuse ROPC (password grant). Don't waste cycles trying grant_type=password: Authentik and most IdPs return invalid_grant regardless of whether credentials are right, identical error for every failure mode.
The pattern that works: authorization-code + PKCE for the initial browser login, then a cached refresh_token for silent renewals. Oauth2c handles the browser dance; the resulting refresh_token lives in OS secret storage (service name <project>-refresh-token) and gets rotated on each use.
Non-obvious flags:
--prompt loginon oauth2c forces a fresh sign-in prompt. Without it, an already-logged-in admin session in the browser gets used — the cached token ends up being for the wrong identity. This bit me once and will bite again without the flag.--login-hint <username>pre-fills the username field, useful when multiple accounts exist.offline_accessscope is required to receive arefresh_tokenat all.- Redirect URI — the OIDC provider must whitelist
http://localhost:9876/callback(oauth2c's default) in Strict mode. email_verified: truemust be set on the test user's attributes in the IdP. Many backends reject JWTs where this claim is false. In Authentik: Directory → Users → Attributes →email_verified: true(plain YAML, not wrapped in brackets — that's a list).
A full reference script lives in references/token-script.sh.
Hurl patterns
One file per scenario (smoke.hurl, sessions-crud.hurl). Parameterize base_url and token as --variable so the same file runs anywhere. [Captures] chains response values into later requests; [Asserts] defines pass/fail.
GET {{base_url}}/api/me
Authorization: Bearer {{token}}
HTTP 200
[Asserts]
jsonpath "$.id" isString
Run: hurl --test --variable token=$TOKEN --variable base_url=$BASE_URL *.hurl.
See references/hurl-patterns.md for captures chaining, retries, GraphQL, parallel runs, CI output.
WebSocket patterns
The one thing that's non-obvious: headers go via -H, which is how you pass the bearer. Everything else is either in websocat --help or references/websocat-patterns.md.
echo '{"op":"ping"}' | websocat -n1 -H "Authorization: Bearer $TOKEN" ws://.../endpoint
For subprotocols (graphql-transport-ws), mTLS (use wscat instead), auto-reconnect, ping/pong: see the reference.
When NOT to use this skill
- Project uses Bruno/Postman/newman/Insomnia — use those.
- Backend has no auth — plain
curlorxhis enough. - UI validation — use Playwright MCP or an in-house browser tool.
- CI secret storage — keychain is developer-machine-only; CI needs a secret manager.
References
references/token-script.sh— complete bash script for token acquisition + refresh cachingreferences/hurl-patterns.md— captures, assertions, retries, GraphQL, parallel, CIreferences/websocat-patterns.md— subprotocols, mTLS, reconnect, debuggingreferences/oidc-token-flows.md— grant-type comparison, 401 debugging, discovery endpoint