Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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 ... RETURNING to 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 include token_use: "id"
  • Refresh tokens are HMAC-hashed before storage in D1
  • The nonce claim is included in ID tokens when provided in the authorize request
  • Users with users.membership_status = 'pending' (invite-only signups without a valid invite, or similar) are not issued an authorization code at /oauth2/authorize; they are redirected to the app login with error=membership_pending.
  • The token endpoint (POST /oauth2/token with authorization_code) rejects pending users with invalid_grant (403) so codes cannot be exchanged if state changed between steps.

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|                            |               |
    |    ?invite=<token>       |                            |               |
    |---------------------------->|                          |               |
    |                          |  Generate state + PKCE     |               |
    |                          |  Store state + invite_token|               |
    |                          |----------------------------------------------->|
    |                          |  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, invite_token, ...
  • Optional query parameter invite on /rp/authorize is stored in oauth_state.invite_token and used when creating or activating users so OAuth sign-in respects the same invite-only / restricted-domain policy as email-code signup.
  • 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

The relying-party callback flow and successful email-code verification (when membership is active after verify) follow the same session steps:

  1. Generate a session JWT signed by the JWKS Durable Object
  2. Session JWT includes: sub (user ID), iat, exp, token_use: "session", auth_time, and membership_status (active or pending)
  3. Store session metadata in user_sessions table (token hash, IP, user agent, expiry)
  4. Set the session JWT as an HttpOnly, Secure, SameSite=Strict cookie

Email code (POST /api/auth/verify-code, src/backend/email-code.ts): If the user is still pending after verification (and no invitation promotes them to active), the handler returns 200 with membership_pending: true and a message, and does not mint a session JWT, write user_sessions, or set the access_token cookie—matching the OAuth authorize gate for pending users.

Token Verification

When a protected endpoint receives a request:

  1. Extract token from Authorization: Bearer <token> header or session cookie
  2. Fetch the JWKS from the Durable Object (cached with ETag)
  3. Verify the RS256 signature against the JWKS
  4. Validate standard claims: iss (issuer), aud (audience), exp (not expired)
  5. Validate token_use claim matches expected type (session, access, or id)
  6. Look up session in D1 to verify it has not been revoked
  7. Look up user to verify account is not disabled
  8. If membership_status is pending, only a small allowlist of routes is permitted (e.g. GET /api/users/me); other API calls return 403 with code: "membership_pending".
  9. Attach user and session objects to the Hono request context

Operators can approve pending users via POST /api/applications/users/:userId/approve (dashboard), which sets membership_status to active and assigns the default app role.

Key Files

FileResponsibility
src/backend/op.tsOIDC Provider: authorize, token, userinfo, revoke endpoints
src/backend/rp.tsRelying Party: authorize redirect, callback, user creation
src/backend/middleware/auth.tsToken verification, session validation, RBAC checks
src/backend/utils/token.tsJWT signing (via DO), verification, claims building
src/backend/utils/crypto.tsHMAC hashing, AES-256-GCM encryption, PBKDF2
src/backend/jwks.tsDurable Object: key generation, rotation, JWKS endpoint