Skip to content

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

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:

  1. 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 using httptest.
  2. Test client — registered at the stub with redirect URI matching the component's callback path.
  3. 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}
  • Location host == Koder ID base URL
  • query params: client_id, redirect_uri, response_type=code, scope contains openid profile email, state (≥16 chars, unguessable), code_challenge, code_challenge_method=S256
  • redirect_uri slug == koder-id (NEVER KoderID, 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
  • Location matches /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=0 or expires=<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)

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:

  • state param survives the round-trip
  • redirect_to query param is preserved through authorize
  • Final destination matches captured URL
  • Open-redirect protection: deep-link to <other-origin>/evil rejected → 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:

SurfaceTest locationFramework
Backend (Go)<component>/tests/auth/oauth_flow_test.gotesting, httptest, chi/gin
Mobile (Flutter)<component>/app/mobile/integration_test/oauth_flow_test.dartflutter_test, integration_test
Desktop (Flutter)<component>/app/desktop/integration_test/oauth_flow_test.dartsame as mobile
TV (React)<component>/app/tv/__tests__/oauthFlow.spec.tsvitest, @testing-library
Web (Flutter Web)<component>/app/web/test/oauth_flow_test.dartflutter_test
Web (templ+HTMX)<component>/tests/e2e/oauth_flow_test.gochromedp, testing
CLI (Go cobra)<component>/app/cli/tests/oauth_flow_test.gotesting, stub HTTP
TUI (Bubble Tea)<component>/app/tui/tests/oauth_flow_test.gotesting, 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