Koder ID OAuth Flow — TDD Test Template
Test template normativo pra implementações da `specs/auth/oauth-flow.kmd`. T1-T8 baseline behavioral (R3-R9 do contrato) + I1-I3 integração com Koder ID staging + N1-N4 negativos. Cada surface (backend/mobile/ desktop/tv/web/cli/tui) localiza os tests no path canônico per- framework. Cobertura por componente × surface rastreada em `registries/koder-id-auth-coverage.md`. Sibling do `identity/login-resolution-test-template.kmd` (que cobre o tier abaixo: resolução de identificador textual → handle/email).
OAuth Flow — TDD Test Template
Tests are *ehavioral*per policies/regression-tests.kmd classification. Each T-test maps to one or more requirements (R1R12) of `specsauthoauthflow.kmd`.
Test infrastructure
Implementations MUST provide:
- *tub Koder ID*— local HTTP service mirroring the relevant
endpoints (
/oauth/v2/authorize,/oauth/v2/token,/oauth/v2/userinfo,/.well-known/openid-configuration,/.well-known/jwks.json). Reference:services/foundation/id/ internal/testing/stub-server(when shipped) OR ad-hoc per component usinghttptest. - *est client*— registered at the stub with redirect URI
matching the component's callback path.
- *eterministic state/nonce*generation via test seed (no random
PRNG in tests).
T1 — Anonymous redirect to Koder ID (R3, R6)
*iven*unauthenticated session *hen*GET /<auth-prefix>/oauth2/koder-id (or hit /user/login which bounces) *hen*response is 302303307 with Location: matching <koder-id-base>/oauth/v2/authorize?client_id=...&redirect_uri=
<component>/<auth-prefix>/oauth2/koder-id/callback&response_type=code
&scope=openid+profile+email&state=...&code_challenge=...
&code_challenge_method=S256
*sserts*
- HTTP status in {302, 303, 307}
Locationhost =Koder ID base URL- query params:
client_id,redirect_uri,response_type=code,scopecontainsopenid profile email,state(≥16 chars, unguessable),code_challenge,code_challenge_method=S256 redirect_urislug =koder-id(NEVERKoderID,koderid,etc. — R2)
T2 — Callback with valid code → dashboard (R3, R4, R8)
*iven*stub Koder ID returns code=valid-code after authorize *hen*GET <component>/<auth-prefix>/oauth2/koder-id/callback?
code=valid-code&state=<from-T1> *hen*
- Component POSTs to Koder ID token endpoint (verified via stub call log)
- Component validates id_token via JWKS (stub provides matching pubkey)
- Component sets session cookie (R8:
Path=/; Secure; HttpOnly; SameSite=Lax) - Response is 302/303 to component's authenticated home (NOT to
landing/marketing URL)
*sserts*
- Token POST received by stub with correct
client_id,code=valid-code,redirect_uri,grant_type=authorization_code,code_verifier - Set-Cookie header present with session cookie
Locationmatches/dashboard,/, or component-specificauthenticated home (NOT
<landing-marketing-url>)- Subsequent GET to authenticated route returns 200 with user
context (e.g., user email reflected in response)
T3 — Callback with invalid code (R11)
*iven*stub returns 4xx on token exchange *hen*GET callback with code=bad-code *hen*
- Response is user-facing error page (200 with error message OR
302 to error route)
- No session cookie set
- Structured log entry:
flow=oauth, step=token_exchange, error_code=invalid_code
T4 — Session persists cross-route (R8)
*iven*session established via T2 *hen*GET <component>/<authenticated-route> with session cookie *hen*response is 200 (authenticated), user context present
*sserts*
- No redirect to OAuth flow
- User identity (email/handle) reflected in response body
T5 — Logout invalidates session + redirects (R8)
*iven*authenticated session *hen*POST /logout (or GET, per component convention) *hen*
- Session cookie cleared (
Max-Age=0orexpires=<past>) - Response redirects to
<koder-id-base>/logout(central revocation) - Component-side session state purged (verify via subsequent
authenticated route → 302 to OAuth flow)
T6 — Landing vs dashboard at / (R5)
*iven*unauthenticated session *hen*GET <component>/ *hen*response renders the anonymous landing (marketing content, no user-specific data)
*iven*authenticated session (from T2) *hen*GET <component>/ *hen*response is EITHER the dashboard OR a 302 to the dashboard canonical URL. *EVER*the anonymous landing.
*sserts*
- Authenticated
/does not contain the anonymous landing's heroCTA ("Sign up", "Get started", marketing copy)
- Authenticated
/contains user-specific data (avatar, recentitems, dashboard chrome)
T7 — Deep-link preserved via redirect_to (R9)
*iven*unauthenticated user GETs <component>/deep/link?x=1 (a route requiring auth) *hen*component redirects to OAuth flow with redirect_to=<original-url> encoded *hen*after T2 callback completes successfully, final response location matches the original <component>/deep/link?x=1
*sserts*
stateparam survives the round-tripredirect_toquery param is preserved through authorize- Final destination matches captured URL
- Open
redirect protection: deeplink to<other-origin>/evilrejected → falls back to authenticated home (R9 validation)
T8 — Token refresh (R8)
*iven*accesstoken TTL = 60s, refreshtoken issued *hen*authenticated request made at 50s (≥75% of TTL) *hen*component automatically refreshes the access_token via Koder ID's token endpoint with grant_type=refresh_token before serving the request
*sserts*
- Stub receives refresh_token POST exactly once
- New access_token stored in session
- Original request served successfully without user-visible
re-auth prompt
I1 — Integration: real Koder ID staging
*iven*Koder ID staging at https://stg.id.koder.dev with component registered as OAuth client *hen*full T1+T2 flow executed against staging *hen*test passes endtoend
Run frequency: pre-release smoke. Failure blocks release.
I2 — Integration: token revocation propagation
*iven*authenticated session via I1 *hen*Koder ID admin revokes session via admin API *hen*next authenticated request to component returns 401/302 within ≤60s (session validation interval)
I3 — Integration: SSO across Koder apps (R10 S2S3S6/S7)
*iven*authenticated session in one Koder app on the same device *hen*second Koder app launches and queries auth_token via KoderIPC (per koder-app/behaviors.kmd §1.3) *hen*second app skips its own OAuth flow, reuses the token, arrives at authenticated dashboard directly
N1 — State mismatch attack (R11)
*iven*attacker forges callback with valid code but different state *hen*GET callback *hen*component rejects with state_mismatch error; no session created; structured log entry flow=oauth, error_code=state_mismatch, severity=warn
N2 — redirect_uri tampering (R11)
*iven*attacker manipulates redirect_uri to point off-origin *hen*Koder ID authorize endpoint validates against registered list (T1 chain) *hen*Koder ID returns invalid_redirect_uri error; component never receives a tampered code
N3 — Replay attack
*iven*valid code used successfully in T2 *hen*same code POSTed to token endpoint a second time *hen*Koder ID returns 4xx invalid_grant; component handles gracefully per T3 path
N4 — Open redirect via redirect_to (R9)
*iven*redirect_to=https://evil.example.com/take-over *hen*OAuth flow completes *hen*component validates same-origin and falls back to authenticated home; does NOT redirect to evil.example.com
Per-surface localization
Implementations of T1T8 + I1I3 + N1-N4 live at canonical paths:
| Surface | Test location | Framework |
|---|---|---|
| Backend (Go) | <component>/tests/auth/oauth_flow_test.go |
testing, httptest, chi/gin |
| Mobile (Flutter) | <component>/app/mobile/integration_test/oauth_flow_test.dart |
flutter_test, integration_test |
| Desktop (Flutter) | <component>/app/desktop/integration_test/oauth_flow_test.dart |
same as mobile |
| TV (React) | <component>/app/tv/__tests__/oauthFlow.spec.ts |
vitest, @testing-library |
| Web (Flutter Web) | <component>/app/web/test/oauth_flow_test.dart |
flutter_test |
| Web (templ+HTMX) | <component>/tests/e2e/oauth_flow_test.go |
chromedp, testing |
| CLI (Go cobra) | <component>/app/cli/tests/oauth_flow_test.go |
testing, stub HTTP |
| TUI (Bubble Tea) | <component>/app/tui/tests/oauth_flow_test.go |
testing, tea.WithRunOptions |
Each surface's tests run T1T8 + I1I3 + N1-N4 (12 cases) in the framework idiomatic for that runtime. Test IDs match across surfaces so the registry can grid them.
Coverage registration
Each new component+surface running this template adds a row to registries/koder-id-auth-coverage.md:
| 2026-05-12 | services/foundation/flow | backend | T1-T8 PASS, I1-I3 SKIP (no stg yet), N1-N4 PASS | reference impl |Pre-release release engineering MUST gate on green grid for the component's enabled surfaces.
Referências
specs/auth/oauth-flow.kmd(the contract this template tests)specs/identity/login-resolution-test-template.kmd(sibling: inputidentifier resolution, lower tier)
policies/regression-tests.kmd(behavioral category)