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.

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 header, 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

POST/PUT/DELETE/PATCH requests validate the Origin header matches the host.

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.

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:*
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 every 24 hours with bounded storage (MAX_KEYS=3).

Transport Security

  • HSTSStrict-Transport-Security: max-age=31536000; includeSubDomains
  • CookiesHttpOnly, Secure, SameSite=Strict for session cookies

Configuration

Required Secrets

wrangler secret put MASTER_KEY           # Strong random key (32+ characters)
wrangler secret put BOOTSTRAP_ADMIN_EMAIL # Optional: first admin email

Environment Variables

Set in wrangler.json:

{
  "vars": {
    "ISSUER": "https://your-domain.com",
    "AUDIENCE": "your-app-name"
  }
}

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