How it works
Authentication uses AWS Cognito as the identity provider. The iOS app communicates directly with Cognito for signup and signin, then uses the resulting JWT tokens for all API calls.Signup
Standard email/password signup through Cognito. The user receives a verification code via email and must confirm before the account is active. No custom backend logic is involved in signup — it’s entirely Cognito-managed.Signin
The app calls Cognito’sInitiateAuth with email and password. On success, Cognito returns:
- Access token (JWT, short-lived) — used for API authorization
- Refresh token (long-lived) — used to get new access tokens without re-entering credentials
JWT validation
The backend auth middleware validates every request:Verify signature
Validates the JWT against Cognito’s JWKS (JSON Web Key Set) public keys. The keys are cached locally to avoid hitting Cognito on every request.
Admin access
Admin endpoints check thecognito:groups claim in the JWT. A user must belong to either the admin or testing group in Cognito to access admin routes.
Group membership is managed directly in the AWS Cognito console or via AWS CLI. There is no self-service admin promotion.
API Key access
For automation and admin tooling, the backend also supports anX-API-Key header. This bypasses Cognito entirely — the key is validated against a stored set of keys. Use this for server-to-server calls or CI/CD pipelines.
Dev mode
When running locally with auth disabled, the middleware skips all validation and returns hardcoded dev users. This is controlled by environment configuration and should never be active in production.Failure modes
| Scenario | Behavior |
|---|---|
| Expired access token | 401 returned, iOS auto-refreshes via refresh token |
| Expired refresh token | 401 returned, user must re-signin |
| Cognito JWKS unreachable | Middleware uses cached keys; if cache is cold, all requests fail with 500 |
| Invalid API key | 401 returned |
Areas of improvement
Cognito JWKS cold-cache failure is unhandled. The
aws-jwt-verify library caches JWKS keys internally, but on a cold start (fresh deploy, new instance), the first request triggers a network call to Cognito’s JWKS endpoint. If that call fails (e.g., transient network issue), every request returns 500 until the next attempt succeeds. There is no pre-warming of the JWKS cache at server startup.iOS
getAccessToken() does not explicitly handle token refresh. In CognitoAuthService.swift getAccessToken(), the method calls Amplify.Auth.fetchAuthSession() which Amplify internally handles refresh for. However, if the refresh token itself has expired (e.g., user inactive for 30+ days with default Cognito settings), this throws an error that bubbles up as a generic AuthError. The iOS app has no dedicated handling to detect “refresh token expired” and redirect to the sign-in screen — the user sees a cryptic error instead.Duplicate
CognitoJwtVerifier instances. Both auth.ts and adminAuth.ts create their own lazy-initialized CognitoJwtVerifier with identical configuration (same userPoolId, clientId, tokenUse). This means two separate JWKS caches and two separate HTTP connections to Cognito’s JWKS endpoint. Extract the verifier into a shared module to deduplicate.pendingPassword stored in memory during sign-up flow. In CognitoAuthService.swift, the user’s plaintext password is held in pendingPassword so it can be used for auto-sign-in after confirmation. It is cleared on signOut() and resetState(), but if the user abandons the verification flow without completing it or signing out, the password remains in memory for the lifetime of the CognitoAuthService instance.