Skip to content

Slice 4: Auth failure modes & token lifecycle hardening #4

Description

@mGasiorek998

What to build

Every failure path from the feature spec, end-to-end. Tampered, expired, malformed, and orphaned-subject tokens are rejected by the server with the right status codes and bodies; the web client treats those failures as logout signals and redirects to /login — with a "Your session has expired" toast when the failure happens after the user was already inside the app. Validation errors (422), Content-Type errors (415), and DB-unavailability errors (503) are also covered. Every failure emits a structured log event with the right name and fields, and never leaks plaintext passwords or tokens.

End-to-end deliverables:

API failure paths (per the feature spec)

  • EC-002 / EH-004: expired JWT → 401 + { error: 'Token expired' }, log event auth.token.expired
  • EH-002: tampered signature OR signed with a different secret → 401 + { error: 'Invalid token' }, log event auth.token.invalid
  • EC-004: malformed Authorization header (missing Bearer prefix, empty token, multiple spaces) → 401 + { error: 'Malformed Authorization header' }
  • VR-004: missing Authorization header on a protected route → 401 + { error: 'Authorization header required' }
  • EC-005: JWT sub no longer in users table → 401 + { error: 'Account not found' }
  • VR-001 / VR-003: POST /auth/login body fails Zod (missing/short fields, non-email, exceeds length caps) → 422 + { error: 'Validation failed', issues: [...] }
  • VR-002: POST /auth/login with wrong Content-Type → 415 + { error: 'Unsupported Media Type' }
  • EH-005: DB unreachable during credential lookup → 503 + { error: 'Service unavailable' }, log event auth.login.db_error
  • EH-001 (regression from Slice 2): wrong-creds AND unknown-email both produce identical 401 + { error: 'Invalid credentials' }, log event auth.login.failed with the hashed email plus request id (never the plaintext email)
  • Successful login emits auth.login.success with user_id, role, request id

Logger redaction (extend the Slice 1 primitive)

  • Any field named password, token, authorization, or jwt is redacted before emit, including in nested structures (proven by exhaustive unit tests)

Web failure handling

  • Global TanStack Query error handler: a 401 from GET /auth/me clears the token via the storage wrapper, resets the query cache, redirects to /login — silently (the user was already logged out or never was)
  • Global TanStack Query error handler: a 401 from any other authenticated request clears the token, resets the query cache, redirects to /login, and shows a toast "Your session has expired. Please log in again."
  • Login page: shows the validation issues array next to the offending field (using the standard error shape from the contract); shows the neutral 401 message above the form on bad credentials
  • Login page: on a 415 from a misconfigured client (defense-in-depth check), surfaces the same generic "Something went wrong" message

Tests

  • One server integration test per failure path (the eight items above) asserting status + body + the right structured log entry got written
  • Logger redaction unit test covering nested redaction
  • Boot guard regression test (already in Slice 2 — re-run)
  • Playwright e2e: log in successfully, then mutate localStorage to a known-tampered token, navigate to /patient, assert the redirect to /login AND the "session expired" toast
  • Playwright e2e: with no token in localStorage, hit /auth/me directly via the queryClient (or simulate the boot path) and assert the silent redirect with no toast

Acceptance criteria

  • An expired JWT against GET /auth/me returns 401 + { error: 'Token expired' }
  • A tampered JWT (signature mutated) against GET /auth/me returns 401 + { error: 'Invalid token' }
  • A JWT signed with a different secret returns 401 + { error: 'Invalid token' }
  • A request with Authorization: Token abc (wrong scheme), Authorization: Bearer (empty token), or Authorization: Bearer abc (double space) returns 401 + { error: 'Malformed Authorization header' }
  • A protected request with no Authorization header returns 401 + { error: 'Authorization header required' }
  • A valid JWT whose sub was removed from users returns 401 + { error: 'Account not found' } (test deletes the row and replays the call)
  • POST /auth/login with { email: 'not-an-email', password: '' } returns 422 + { error: 'Validation failed', issues: [...] } listing both fields
  • POST /auth/login with Content-Type: text/plain returns 415 + { error: 'Unsupported Media Type' }
  • When the DB is unreachable, POST /auth/login returns 503 + { error: 'Service unavailable' } (test simulates the connection failure)
  • Every failure path writes the named structured log event listed above with the right fields
  • No log entry contains a plaintext password or full JWT (asserted by an exhaustive redaction test, including nested fields)
  • Web e2e: tampered token in localStorage → navigation to /patient → redirected to /login with a "session expired" toast visible
  • Web e2e: missing token + visiting /patient → redirected to /login silently (no toast — user was never in)
  • The login form surfaces validation errors next to the offending field on a 422 response
  • pnpm verify is green

User stories covered

  • US 13 from PRD: "As a user submitting wrong credentials, I want to see a single neutral error message."
  • US 14 from PRD: "As a user submitting a non-existent email, I want to see the same neutral error message as for a wrong password."
  • US 15 from PRD: "As a user submitting a malformed email or empty password, I want to see a clear validation message."
  • US 23 from PRD: "As a developer reading server logs, I want every authentication outcome to emit a structured log entry with event name, user id when known, role, and request id."
  • US 24 from PRD: "As a developer reading server logs, I want plaintext passwords and full JWTs to never appear in any log entry."
  • US 27 from PRD: "As a security-aware developer, I want a tampered or expired JWT to be rejected with a clear status code."
  • US 29 from PRD: "As a security-aware developer, I want a JWT whose subject no longer exists in the users table to be rejected."

Blocked by

Can run in parallel with #3 (Slice 3 — RBAC).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    afkEligible for the main agent looppriority:1Priority 1 — bugs default heresliceVertical tracer-bullet slice

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions