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

Security Controls

This document details the security measures implemented in Aero2 at the code level.

Summary

CategoryControlFiles
AuthenticationToken signature verification, type enforcement, RS256 only, clock tolerancetoken.ts, auth.ts
CSRF ProtectionOAuth state validation, cookie binding, Origin/Referer checking, SameSite cookiesrp.ts, op.ts, index.ts
OIDC ComplianceID token issuance, PKCE required (S256), nonce supportop.ts
Token SecurityRefresh token HMAC hashing, one-time auth codes, atomic state consumptionop.ts, crypto.ts, rp.ts
Access ControlRBAC system, admin endpoint protection, bootstrap adminauth.ts, rbac.ts
Input ValidationSSRF protection for IdP URLs, open redirect preventionidp.ts, rp.ts
CryptographyPBKDF2 secret hashing (100k iterations), key rotation with boundscrypto.ts, jwks.ts
TransportHSTS, CSP, Permissions-Policy, secure cookie attributesindex.ts, rp.ts

Token Security

Signature Verification

All endpoints verify JWT signatures against the JWKS before trusting claims:

const result = await jose.jwtVerify(token, keySet, {
    issuer: getIssuer(env),
    audience: getAudience(env),
    algorithms: ["RS256"],
    clockTolerance: 30,
    requiredClaims: ["sub", "iat", "exp"],
});
  • Algorithm restriction — Only RS256 accepted, preventing algorithm confusion attacks
  • Clock tolerance — 30 seconds for distributed system clock skew
  • Required claimssub, iat, exp must be present

Token Type Enforcement

Prevents token confusion attacks by validating the token_use claim:

  • Session tokens: token_use: "session"
  • Access tokens: token_use: "access"
  • ID tokens: token_use: "id"

Refresh Token Hashing

Refresh tokens are HMAC-hashed before storage. Database compromise doesn't expose usable tokens:

const hashedRefreshToken = await hmacHash(rawRefreshToken, env.MASTER_KEY);

CSRF Protection

OAuth State Validation

State tokens are stored in the database with expiration and consumed atomically on callback:

DELETE FROM oauth_state
WHERE state = ? AND idp_name = ? AND expires_at > datetime('now')
RETURNING redirect_uri

State Cookie Binding

State is bound to an HttpOnly cookie to prevent login CSRF:

setCookie(c, "oauth_state", state, {
    httpOnly: true,
    secure: isSecure,
    sameSite: "Lax",
    maxAge: 600,
});

Origin/Referer Checking

Cookie-authenticated POST/PUT/DELETE/PATCH requests must include a valid Origin or Referer header. Requests missing both headers with a session cookie are rejected with 403. Requests using Bearer token authentication are exempt (not vulnerable to CSRF).

OIDC Compliance

PKCE Required

PKCE with S256 is mandatory for all authorization requests. Plain method is not supported.

ID Token Issuance

When openid scope is requested, an ID token is issued with proper claims including sub, auth_time, and nonce.

Content Security Policy

API/OAuth2 Endpoints

Content-Security-Policy: default-src 'none'; frame-ancestors 'none'

SPA HTML Pages

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none'

Permissions-Policy

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()

CORS Configuration

  • Production — Requires ALLOWED_ORIGINS env var. Rejects all cross-origin requests if not set (fail-closed).
  • Development — Allows all origins when ISSUER is not HTTPS.
  • Origins are parsed from comma-separated list and matched exactly.
  • credentials: false on OAuth2 endpoints.

SSRF Protection

IdP URL configuration validates all endpoints:

  • Must be HTTPS
  • Blocks private IPs (10.x, 172.16-31.x, 192.168.x, 127.x, ::1)
  • Blocks localhost (unless ALLOW_LOCALHOST_IDP=true for dev)

Access Control (RBAC)

Two default roles with granular permissions:

RolePermissions
adminclients:*, users:*, idps:*, roles:*, audit:*
userusers

Bootstrap Admin

First user matching BOOTSTRAP_ADMIN_EMAIL with verified email gets admin role automatically.

Cryptography

Secret Hashing

Client secrets use PBKDF2 with 100,000 iterations, 16-byte random salt, SHA-256, and constant-time comparison.

Key Rotation

JWKS keys rotate automatically with a Durable Object managing the rotation cycle. Storage is bounded to MAX_KEYS=3 to prevent unbounded growth.

JWKS Caching

  • JWKS endpoint: Cache-Control: public, max-age=3600 with ETag support
  • Individual key endpoint: Cache-Control: public, max-age=600
  • Conditional requests supported via If-None-Match → 304 responses

Transport Security

  • HSTSStrict-Transport-Security: max-age=31536000; includeSubDomains
  • CookiesHttpOnly, Secure, SameSite=Strict for session cookies
  • X-Frame-OptionsDENY
  • X-Content-Type-Optionsnosniff
  • Referrer-Policystrict-origin-when-cross-origin

Session Security

  • Sessions tracked in database with user agent and IP
  • Individual or bulk revocation via API
  • Proactive invalidation — disabling a user immediately revokes all active sessions
  • Disabled user check runs before every authenticated action

Open Redirect Prevention

All redirect URIs are validated by validateRedirectUri() utility:

  • Only same-origin paths allowed (must start with /)
  • Protocol-relative URLs rejected
  • External URLs rejected
  • Falls back to /dashboard on invalid input

Configuration

Required Secrets

SecretPurpose
MASTER_KEYHMAC key for token hashing, encryption
BOOTSTRAP_ADMIN_EMAILFirst admin user (optional)

Environment Variables

VariablePurpose
ISSUERToken issuer claim
AUDIENCEToken audience claim
ALLOWED_ORIGINSComma-separated CORS origins (required in production)

Security Testing Checklist

  • Access protected endpoints with forged/modified JWTs
  • Attempt HS256-signed tokens (should reject)
  • Use access token as session token (should reject)
  • Attempt callback with invalid/reused/expired state
  • Attempt callback without state cookie
  • Test external URLs in redirect_uri
  • Configure IdP with private IP addresses (should reject)
  • Access admin endpoints without authentication
  • Access admin endpoints as non-admin user
  • Attempt authorization without code_challenge
  • Attempt to use raw refresh token value from logs
  • POST without Origin/Referer with session cookie (should reject)
  • POST without Origin/Referer with Bearer token (should succeed)