Authentication Flow (Internal)
This page documents the detailed internal mechanics of authentication in Aero2, covering both the OIDC Provider and Relying Party flows.
OIDC Provider Flow
When Aero2 acts as an OIDC Provider, it issues tokens to registered OAuth clients on behalf of authenticated users.
Client App Aero2 Worker D1 Database
| | |
| GET /oauth2/authorize | |
| ?client_id=... | |
| &redirect_uri=... | |
| &response_type=code | |
| &scope=openid profile | |
| &code_challenge=... | |
| &code_challenge_method=S256| |
| &state=... | |
| &nonce=... | |
|---------------------------->| |
| | Validate params |
| | - client_id exists? |
| | - redirect_uri allowed? |
| | - PKCE S256 required |
| | - scopes valid? |
| |-------------------------------->|
| | Check active session |
| |<--------------------------------|
| | |
| 302 -> /login | No session: redirect to login |
|<----------------------------| |
| | |
| (user authenticates) | |
| | |
| | Generate auth code |
| | Store code + PKCE + nonce |
| |-------------------------------->|
| | |
| 302 -> redirect_uri | |
| ?code=AUTH_CODE | |
| &state=... | |
|<----------------------------| |
| | |
| POST /oauth2/token | |
| grant_type=authorization_code |
| code=AUTH_CODE | |
| code_verifier=... | |
| client_id=... | |
| client_secret=... | |
|---------------------------->| |
| | Verify client credentials |
| | Verify PKCE (S256) |
| | Delete code (atomic) |
| |-------------------------------->|
| | |
| | Sign JWT with Durable Object |
| | (JWKS key management) |
| | |
| 200 { | |
| access_token: "...", | |
| id_token: "...", | |
| refresh_token: "...", | |
| token_type: "Bearer", | |
| expires_in: 3600 | |
| } | |
|<----------------------------| |Key Implementation Details
- File:
src/backend/op.ts - Auth codes are consumed atomically using
DELETE ... RETURNINGto prevent replay - PKCE with S256 is mandatory; plain method is rejected
- Tokens are signed using the JWKS Durable Object (
src/backend/jwks.ts) - Access tokens include
token_use: "access", ID tokens includetoken_use: "id" - Refresh tokens are HMAC-hashed before storage in D1
- The
nonceclaim is included in ID tokens when provided in the authorize request
Relying Party Flow
When Aero2 acts as a Relying Party, it delegates authentication to an external identity provider (GitHub, Google, or a custom OIDC/OAuth2 provider).
User Browser Aero2 Worker External IdP D1
| | | |
| GET /rp/authorize/github| | |
|---------------------------->| | |
| | Generate state + PKCE | |
| | Store state in D1 | |
| |----------------------------------------------->|
| | Set state cookie (HttpOnly, SameSite=Lax) |
| | | |
| 302 -> github.com/ | | |
| login/oauth/authorize | | |
| ?client_id=... | | |
| &state=... | | |
| &scope=user:email | | |
|<----------------------------| | |
| | | |
| (user authenticates | | |
| at GitHub) | | |
| | | |
| GET /rp/callback/github | | |
| ?code=GITHUB_CODE | | |
| &state=... | | |
|---------------------------->| | |
| | Verify state | |
| | - Match state param | |
| | to state cookie | |
| | - Atomic consume from D1 | |
| |----------------------------------------------->|
| | | |
| | Exchange code for tokens | |
| |--------------------------->| |
| | {access_token, id_token} | |
| |<---------------------------| |
| | | |
| | Fetch user info | |
| |--------------------------->| |
| | {email, name, picture} | |
| |<---------------------------| |
| | | |
| | Create or link user | |
| | - Find by email + app_id | |
| | - Create if not exists | |
| | - Link identity | |
| |----------------------------------------------->|
| | | |
| | Create session | |
| | - Generate session JWT | |
| | - Store in user_sessions | |
| |----------------------------------------------->|
| | | |
| 302 -> / | | |
| Set-Cookie: session=JWT | | |
| (HttpOnly, Secure, | | |
| SameSite=Strict) | | |
|<----------------------------| | |Key Implementation Details
- File:
src/backend/rp.ts - State is stored in both D1 and an HttpOnly cookie for double-bind CSRF protection
- State is consumed atomically:
DELETE FROM oauth_state WHERE state = ? AND idp_name = ? AND expires_at > datetime('now') RETURNING redirect_uri - PKCE code verifier is generated for providers that support it
- User creation/linking is done in a single flow: look up by email, create if new, add identity link
- External IdP tokens (access/refresh) are encrypted with AES-256-GCM before storage
Session Creation
Both flows result in session creation:
- Generate a session JWT signed by the JWKS Durable Object
- Session JWT includes:
sub(user ID),iat,exp,token_use: "session",auth_time - Store session metadata in
user_sessionstable (token hash, IP, user agent, expiry) - Set the session JWT as an HttpOnly, Secure, SameSite=Strict cookie
Token Verification
When a protected endpoint receives a request:
- Extract token from
Authorization: Bearer <token>header or session cookie - Fetch the JWKS from the Durable Object (cached with ETag)
- Verify the RS256 signature against the JWKS
- Validate standard claims:
iss(issuer),aud(audience),exp(not expired) - Validate
token_useclaim matches expected type (session, access, or id) - Look up session in D1 to verify it has not been revoked
- Look up user to verify account is not disabled
- Attach user and session objects to the Hono request context
Key Files
| File | Responsibility |
|---|---|
src/backend/op.ts | OIDC Provider: authorize, token, userinfo, revoke endpoints |
src/backend/rp.ts | Relying Party: authorize redirect, callback, user creation |
src/backend/middleware/auth.ts | Token verification, session validation, RBAC checks |
src/backend/utils/token.ts | JWT signing (via DO), verification, claims building |
src/backend/utils/crypto.ts | HMAC hashing, AES-256-GCM encryption, PBKDF2 |
src/backend/jwks.ts | Durable Object: key generation, rotation, JWKS endpoint |