Skip to content

Implement Backend-For-Frontend (BFF) pattern for OAuth token storage #119

@colinmxs

Description

@colinmxs

Summary

Migrate OAuth token handling from client-side localStorage to a server-side Backend-For-Frontend (BFF) pattern. The App API (FastAPI) will handle the entire OAuth code exchange, store tokens server-side, and issue an HttpOnly session cookie to the browser. The frontend will never see or store OAuth tokens.

This addresses the findings in CODE_REVIEW_TOKEN_STORAGE.md — specifically F-01 (refresh token in localStorage), F-02 (access token in localStorage), F-03 (no token binding), F-04 (tamperable expiry), F-05 (persistence after logout intent), and F-07 (cross-tab sync gap).

Context

The IETF draft-ietf-oauth-browser-based-apps-26 explicitly recommends the BFF pattern for "business applications, sensitive applications, and applications that handle personal data." This application handles student data at an institution and qualifies.

See CODE_REVIEW_TOKEN_STORAGE.md in the repo root for the full security review, risk analysis, and alternative options considered.

Backend Changes (App API)

New Auth Endpoints

Add the following endpoints to the App API (backend/src/apis/app_api/auth/):

Endpoint Method Description
/auth/login GET Initiates OAuth flow — generates PKCE challenge, state, and redirects to IdP
/auth/callback GET Handles IdP callback — exchanges code for tokens, creates server-side session, sets cookie
/auth/session GET Returns current session info (user profile, expiry) — used by frontend to check auth state
/auth/refresh POST Refreshes the access token server-side using the stored refresh token
/auth/logout POST Clears server-side session and cookie

Server-Side Session Storage

  • Store sessions in DynamoDB (new sessions-auth table or reuse existing session infrastructure)
  • Session record contains: session_id, user_id, access_token, refresh_token, token_expiry, created_at, last_accessed
  • Session ID is a cryptographically random value (not the access token)
  • TTL on DynamoDB records for automatic cleanup of abandoned sessions

Cookie Configuration

Set-Cookie: __Host-session=<session_id>; HttpOnly; Secure; SameSite=Strict; Path=/
  • HttpOnly — not accessible to JavaScript
  • Secure — only sent over HTTPS
  • SameSite=Strict — not sent on cross-origin requests
  • __Host- prefix — requires Secure, no Domain, Path=/

Request Proxying

The App API already serves as the BFF for the Inference API. Extend this pattern so that all authenticated requests include the access token server-side by reading it from the session store, rather than expecting the frontend to attach it.

Frontend Changes

auth.service.ts

  • Remove all localStorage.getItem/setItem calls for tokens
  • login() redirects to App API /auth/login instead of directly to the IdP
  • handleCallback() is no longer needed (App API handles the callback)
  • isAuthenticated() checks session via /auth/session endpoint (or cookie presence)
  • logout() calls /auth/logout and clears local state

auth.interceptor.ts

  • Remove token attachment logic entirely — cookies are sent automatically by the browser
  • Handle 401 responses by redirecting to login

auth.guard.ts

  • Replace local token inspection with a lightweight /auth/session call
  • Cache the session check result briefly to avoid excessive API calls on route transitions

Infrastructure Changes

  • Add sessions-auth DynamoDB table to Infrastructure stack (if not reusing existing tables)
  • Export table name/ARN via SSM
  • Grant App API Fargate task role read/write access
  • Configure TTL attribute for automatic session expiry cleanup

Acceptance Criteria

  • OAuth tokens are never sent to or stored by the frontend
  • Browser receives only an HttpOnly session cookie
  • /auth/session returns user profile and auth state without exposing tokens
  • Token refresh happens server-side transparently
  • Logout clears both server-side session and browser cookie
  • Existing auth providers (Cognito, Entra ID) continue to work
  • Multi-tab usage works correctly (cookie is shared across tabs)
  • Session expiry and cleanup are handled via DynamoDB TTL

References

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions