Koder ID OAuth Flow — TDD Test Template
auth specs/auth/oauth-flow-test-template.kmd
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).
When this spec applies
All triggers
- Implement T1-T8 antes de release de qualquer componente Koder com user auth
- Adicionar nova surface (variante de UI) a um componente existente
- Auditar conformance de componente legado à spec oauth-flow
Specification body
OAuth Flow — TDD Test Template
Tests are behavioral per policies/regression-tests.kmd
classification. Each T-test maps to one or more requirements
(R1-R12) of specs/auth/oauth-flow.kmd.
Test infrastructure
Implementations MUST provide:
- Stub 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. - Test client — registered at the stub with redirect URI matching the component's callback path.
- Deterministic state/nonce generation via test seed (no random PRNG in tests).
T1 — Anonymous redirect to Koder ID (R3, R6)
Given unauthenticated session
When GET /<auth-prefix>/oauth2/koder-id (or hit /user/login
which bounces)
Then response is 302/303/307 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
Asserts:
- 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)
Given stub Koder ID returns code=valid-code after authorize
When GET <component>/<auth-prefix>/oauth2/koder-id/callback? code=valid-code&state=<from-T1>
Then:
- 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)
Asserts:
- 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-specific authenticated 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)
Given stub returns 4xx on token exchange
When GET callback with code=bad-code
Then:
- 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)
Given session established via T2
When GET <component>/<authenticated-route> with session cookie
Then response is 200 (authenticated), user context present
Asserts:
- No redirect to OAuth flow
- User identity (email/handle) reflected in response body
T5 — Logout invalidates session + redirects (R8)
Given authenticated session
When POST /logout (or GET, per component convention)
Then:
- 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)
Given unauthenticated session
When GET <component>/
Then response renders the anonymous landing (marketing content,
no user-specific data)
Given authenticated session (from T2)
When GET <component>/
Then response is EITHER the dashboard OR a 302 to the dashboard
canonical URL. NEVER the anonymous landing.
Asserts:
- Authenticated
/does not contain the anonymous landing's hero CTA ("Sign up", "Get started", marketing copy) - Authenticated
/contains user-specific data (avatar, recent items, dashboard chrome)
T7 — Deep-link preserved via redirect_to (R9)
Given unauthenticated user GETs <component>/deep/link?x=1
(a route requiring auth)
When component redirects to OAuth flow with redirect_to=<original-url>
encoded
Then after T2 callback completes successfully, final response
location matches the original <component>/deep/link?x=1
Asserts:
stateparam survives the round-tripredirect_toquery param is preserved through authorize- Final destination matches captured URL
- Open-redirect protection: deep-link to
<other-origin>/evilrejected → falls back to authenticated home (R9 validation)
T8 — Token refresh (R8)
Given access_token TTL = 60s, refresh_token issued
When authenticated request made at 50s (≥75% of TTL)
Then component automatically refreshes the access_token via
Koder ID's token endpoint with grant_type=refresh_token before
serving the request
Asserts:
- 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
Given Koder ID staging at https://stg.id.koder.dev with
component registered as OAuth client
When full T1+T2 flow executed against staging
Then test passes end-to-end
Run frequency: pre-release smoke. Failure blocks release.
I2 — Integration: token revocation propagation
Given authenticated session via I1 When Koder ID admin revokes session via admin API Then next authenticated request to component returns 401/302 within ≤60s (session validation interval)
I3 — Integration: SSO across Koder apps (R10 S2/S3/S6/S7)
Given authenticated session in one Koder app on the same device
When second Koder app launches and queries auth_token via
KoderIPC (per koder-app/behaviors.kmd §1.3)
Then second app skips its own OAuth flow, reuses the token,
arrives at authenticated dashboard directly
N1 — State mismatch attack (R11)
Given attacker forges callback with valid code but different state
When GET callback
Then 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)
Given attacker manipulates redirect_uri to point off-origin
When Koder ID authorize endpoint validates against registered
list (T1 chain)
Then Koder ID returns invalid_redirect_uri error; component
never receives a tampered code
N3 — Replay attack
Given valid code used successfully in T2
When same code POSTed to token endpoint a second time
Then Koder ID returns 4xx invalid_grant; component handles
gracefully per T3 path
N4 — Open redirect via redirect_to (R9)
Given redirect_to=https://evil.example.com/take-over
When OAuth flow completes
Then component validates same-origin and falls back to
authenticated home; does NOT redirect to evil.example.com
Per-surface localization
Implementations of T1-T8 + I1-I3 + 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 T1-T8 + I1-I3 + 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: input identifier resolution, lower tier)policies/regression-tests.kmd(behavioral category)
References
specs/auth/oauth-flow.kmdspecs/identity/login-resolution-test-template.kmdregistries/koder-id-auth-coverage.mdregistries/regression-test-cases.md