ADR-008 — Authentication Architecture
ADR-008 — Authentication Architecture
Sección titulada «ADR-008 — Authentication Architecture»Accepted
Context
Sección titulada «Context»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).
Decision
Sección titulada «Decision»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]]):
| Field | Type | Cognito claim | Notes |
|---|---|---|---|
sub | UUIDField(unique=True) | sub | Permanent identifier. Auto-generated in dev; comes from Cognito in prod. |
email | EmailField(unique=True) | email | USERNAME_FIELD |
given_name | CharField | given_name | |
family_name | CharField | family_name | |
email_verified | BooleanField | email_verified | |
role | CharField(choices) | cognito:groups | ADMIN / 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— extendsJWTAuthenticationfrom SimpleJWT. Reads the token from theaccess_tokencookie, verifies HS256 signature withSECRET_KEY, enforces CSRF on mutations. - Prod (Cognito):
CognitoJWTAuthentication— replaces the previous class. Reads the sameaccess_tokencookie (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), performsget_or_createofUserbysub.
The frontend, the User model, the cookies, the CSRF pattern, and all endpoints remain identical.
Tokens and cookies.
| Cookie | Content | HttpOnly | SameSite (dev) | SameSite (prod) | Max-Age |
|---|---|---|---|---|---|
access_token | Signed JWT | Yes | Lax | None | 1 hour |
refresh_token | Signed JWT | Yes | Lax | None | 7 days |
csrftoken | CSRF token | No | Lax | None | session |
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.
Consequences
Sección titulada «Consequences»- 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) changingDEFAULT_AUTHENTICATION_CLASSESin settings. No changes in the frontend. - The
subfield in theUsermodel is the canonical identifier for all foreign keys pointing to users. The auto-incrementalidis never used as an external reference. - The
SameSite=None; Securecookies in production require that the Amplify domain and the App Runner domain be different — standard CORS cross-origin configuration. django-cognito-jwtexists as a library (labd/django-cognito-jwt) but its maintenance is irregular. The preferred implementation is a custom class that usespython-joseto verify RS256 against the cached JWKS.