Ir al contenido

ADR-008 — Authentication Architecture

Accepted

The MVP needs email-and-password authentication managed by Django. Post-MVP, authentication migrates to AWS Cognito (RS256, JWKS) with support for social login (Google and others) via Cognito federation. If the MVP design does not anticipate this migration, there will be changes in the frontend, the data model, and the business logic — exactly the kind of rework that destroyed credibility in previous projects.

Research prior to this decision revealed that Cognito issues JWT tokens with standard OIDC claims (sub, email, given_name, family_name, email_verified), uses RS256 with a public key distributed via a JWKS endpoint, and in social federation acts as an intermediary — the backend never receives Google tokens, only Cognito tokens. The Cognito sub claim is an immutable UUID that uniquely identifies the user forever, even if they change their email or add a social provider.

Two patterns were discarded: storing tokens in localStorage (vulnerable to XSS) and using the Authorization: Bearer header (requires the frontend to have access to the token — incompatible with httpOnly cookies).

Authentication is transported exclusively in httpOnly cookies. The frontend never sees or stores JWT tokens.

User model. The User model uses OIDC claims from day one (full entity spec in [[adr-014-user-entity|ADR-014]]):

FieldTypeCognito claimNotes
subUUIDField(unique=True)subPermanent identifier. Auto-generated in dev; comes from Cognito in prod.
emailEmailField(unique=True)emailUSERNAME_FIELD
given_nameCharFieldgiven_name
family_nameCharFieldfamily_name
email_verifiedBooleanFieldemail_verified
roleCharField(choices)cognito:groupsADMIN / MANAGER / SUPERVISOR / VIEWER / EMPLOYEE

Swap point: the DRF authentication class. The only component that changes between dev and prod is the DEFAULT_AUTHENTICATION_CLASSES class:

  • Dev (MVP): CookieJWTAuthentication — extends JWTAuthentication from SimpleJWT. Reads the token from the access_token cookie, verifies HS256 signature with SECRET_KEY, enforces CSRF on mutations.
  • Prod (Cognito): CognitoJWTAuthentication — replaces the previous class. Reads the same access_token cookie (which Cognito will have set via the OAuth flow), verifies RS256 signature against the Cognito JWKS endpoint (https://cognito-idp.<region>.amazonaws.com/<pool_id>/.well-known/jwks.json), performs get_or_create of User by sub.

The frontend, the User model, the cookies, the CSRF pattern, and all endpoints remain identical.

Tokens and cookies.

CookieContentHttpOnlySameSite (dev)SameSite (prod)Max-Age
access_tokenSigned JWTYesLaxNone1 hour
refresh_tokenSigned JWTYesLaxNone7 days
csrftokenCSRF tokenNoLaxNonesession

CSRF. The Angular interceptor reads csrftoken from document.cookie and sends it as the X-CSRFToken header on POST/PUT/PATCH/DELETE. CookieJWTAuthentication verifies that header via CsrfViewMiddleware before processing the mutation.

Social login (post-MVP). Google and other social providers federate through Cognito Managed Login. Cognito receives the Google token, creates or updates a profile in its User Pool, and issues its own JWT tokens with the same sub for that user. The Django backend never sees Google tokens. The sub remains stable even if the user links their Google account to an existing account.

Refresh and logout. The refresh rotates the token: ROTATE_REFRESH_TOKENS=True, BLACKLIST_AFTER_ROTATION=True. Logout blacklists the refresh token and clears the cookies. The Angular error interceptor catches 401s, attempts a single refresh, and on failure executes logout.

  • Migrating to Cognito requires: (1) writing CognitoJWTAuthentication, (2) configuring three environment variables (COGNITO_USER_POOL_ID, COGNITO_CLIENT_ID, COGNITO_REGION; see [[adr-006-environment-variables|ADR-006]]), (3) changing DEFAULT_AUTHENTICATION_CLASSES in settings. No changes in the frontend.
  • The sub field in the User model is the canonical identifier for all foreign keys pointing to users. The auto-incremental id is never used as an external reference.
  • The SameSite=None; Secure cookies in production require that the Amplify domain and the App Runner domain be different — standard CORS cross-origin configuration.
  • django-cognito-jwt exists as a library (labd/django-cognito-jwt) but its maintenance is irregular. The preferred implementation is a custom class that uses python-jose to verify RS256 against the cached JWKS.