Security Controls
This document details the security measures implemented in Aero2.
Summary
| Category | Control | Files |
|---|---|---|
| Authentication | Token signature verification, type enforcement, RS256 only, clock tolerance | token.ts, auth.ts |
| CSRF Protection | OAuth state validation, cookie binding, Origin/Referer checking, SameSite cookies | rp.ts, op.ts, index.ts |
| OIDC Compliance | ID token issuance, PKCE required (S256), nonce support | op.ts |
| Token Security | Refresh token HMAC hashing, one-time auth codes, atomic state consumption | op.ts, crypto.ts, rp.ts |
| Access Control | RBAC system, admin endpoint protection, bootstrap admin | auth.ts, rbac.ts |
| Input Validation | SSRF protection for IdP URLs, open redirect prevention | idp.ts, rp.ts |
| Cryptography | PBKDF2 secret hashing (100k iterations), key rotation with bounds | crypto.ts, jwks.ts |
| Transport | HSTS header, secure cookie attributes | index.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 claims —
sub,iat,expmust 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_uriState 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=truefor dev)
Access Control (RBAC)
Two default roles with granular permissions:
| Role | Permissions |
|---|---|
admin | clients:*, users:*, idps:*, roles:* |
user | users |
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
- HSTS —
Strict-Transport-Security: max-age=31536000; includeSubDomains - Cookies —
HttpOnly,Secure,SameSite=Strictfor session cookies
Configuration
Required Secrets
wrangler secret put MASTER_KEY # Strong random key (32+ characters)
wrangler secret put BOOTSTRAP_ADMIN_EMAIL # Optional: first admin emailEnvironment 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