Koder ID OAuth Flow — TDD Test Template

mandatory

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:

  1. *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 using httptest.

  2. *est client*— registered at the stub with redirect URI

    matching the component's callback path.

  3. *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}
  • 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)

*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
  • 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)

*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=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)

*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 hero

    CTA ("Sign up", "Get started", marketing copy)

  • Authenticated / contains user-specific data (avatar, recent

    items, dashboard chrome)

*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*

  • state param survives the round-trip
  • redirect_to query param is preserved through authorize
  • Final destination matches captured URL
  • Openredirect protection: deeplink to <other-origin>/evil

    rejected → 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: input

    identifier resolution, lower tier)

  • policies/regression-tests.kmd (behavioral category)

Source: ../home/koder/dev/koder/meta/docs/stack/specs/auth/oauth-flow-test-template.kmd