# Aero2 Docs > Documentation for Aero2 — OIDC Provider & Relying Party on Cloudflare Workers ## API Reference ### Relying Party (RP) Endpoints | Endpoint | Description | | ----------------------- | ------------------------------------- | | `GET /rp/authorize` | Initiate OAuth flow with external IdP | | `GET /rp/callback/:idp` | OAuth callback handler | | `GET /rp/userinfo` | Get current user info (requires auth) | | `POST /signout` | Sign out and clear session | ### OIDC Provider (OP) Endpoints | Endpoint | Description | | --------------------------------------- | ------------------------- | | `GET /.well-known/openid-configuration` | OIDC discovery document | | `GET /oauth2/jwks.json` | JSON Web Key Set | | `GET /oauth2/authorize` | Authorization endpoint | | `POST /oauth2/token` | Token endpoint | | `GET /oauth2/userinfo` | UserInfo endpoint | | `POST /oauth2/revoke` | Token revocation endpoint | ### User APIs | Endpoint | Description | | ------------------------------ | ----------------------------- | | `GET /api/users/me` | Get current user profile | | `GET /api/users/me/identities` | Get linked identity providers | | `GET /api/sessions` | List current user's sessions | | `DELETE /api/sessions/:id` | Revoke specific session | | `DELETE /api/sessions` | Revoke all other sessions | ### Management APIs (Admin Required) #### Identity Providers | Endpoint | Description | | ---------------------- | ------------------------ | | `GET /api/idps` | List identity providers | | `POST /api/idps` | Create identity provider | | `GET /api/idps/:id` | Get IdP details | | `PUT /api/idps/:id` | Update identity provider | | `DELETE /api/idps/:id` | Delete identity provider | #### OAuth Clients | Endpoint | Description | | ------------------------------------- | -------------------- | | `GET /api/clients` | List OAuth clients | | `POST /api/clients` | Create OAuth client | | `GET /api/clients/:id` | Get client details | | `PUT /api/clients/:id` | Update client | | `DELETE /api/clients/:id` | Delete client | | `POST /api/clients/:id/rotate-secret` | Rotate client secret | #### Users | Endpoint | Description | | ------------------------------------------ | --------------------------- | | `GET /api/users` | List all users (paginated) | | `GET /api/users/:id` | Get user by ID | | `PUT /api/users/:id` | Update user profile | | `DELETE /api/users/:id` | Delete user | | `GET /api/users/:id/sessions` | List user's active sessions | | `DELETE /api/users/:id/sessions` | Revoke all user sessions | | `DELETE /api/users/:id/identities/:linkId` | Unlink identity provider | #### Roles & Permissions | Endpoint | Description | | ------------------------------------------- | --------------------------- | | `GET /api/roles` | List roles and permissions | | `GET /api/roles/:id` | Get role with permissions | | `POST /api/roles` | Create custom role | | `PUT /api/roles/:id` | Update role | | `DELETE /api/roles/:id` | Delete role | | `POST /api/users/:id/roles` | Assign role to user | | `DELETE /api/users/:id/roles/:roleId` | Remove role from user | | `GET /api/permissions` | List all permissions | | `POST /api/roles/:id/permissions` | Add permission to role | | `DELETE /api/roles/:id/permissions/:permId` | Remove permission from role | #### Audit Log | Endpoint | Description | | -------------------------- | ----------------------------- | | `GET /api/audit` | List audit events (paginated) | | `GET /api/audit/:id` | Get audit event details | | `GET /api/audit/events` | List event types with counts | | `GET /api/users/:id/audit` | User's audit history | ### Health Check | Endpoint | Description | | ------------------ | ----------------------------------- | | `GET /health` | Full health check (database + JWKS) | | `GET /health/live` | Simple liveness probe | ## Development Workflow ### Environments | Environment | URL | Database | Purpose | | ----------- | ---------------------------------------------- | -------- | ------------------- | | Local | [http://localhost:8787](http://localhost:8787) | Local D1 | Development | | QA | [https://qa.aero2.dev](https://qa.aero2.dev) | aero2-qa | Staging & E2E tests | | Production | [https://aero2.dev](https://aero2.dev) | aero2 | Live | ### Local Development ```bash # Install dependencies npm install # Start local dev server npm run dev # Run unit tests npm test # Run linting npm run lint ``` Local development uses a local D1 database. Changes are isolated to your machine. ### Pull Request Workflow When you push a branch and open a PR against `main`, CI runs automatically: * **Lint** — Biome linting * **Build** — Compile and bundle * **Unit Tests** — Vitest against local D1 * **Migration Separation** — Fails if PR mixes code and migration files No deployment happens on PRs. Code is reviewed and merged to `main`. ### Deployment Pipeline When a PR is merged to `main`: 1. **Build & Test** — Lint, build, unit tests 2. **Deploy to QA** — Deploys to [https://qa.aero2.dev](https://qa.aero2.dev) 3. **E2E & API Tests** — Playwright tests against live QA 4. **Deploy to Production** — Deploys to [https://aero2.dev](https://aero2.dev) 5. **Health Check** — Verifies production, auto-rollback on failure ### Database Migrations Migrations are **manual** and follow the expand-contract pattern. #### Creating a Migration ```bash touch migrations/0002_your_migration_name.sql ``` #### Safe Migration Process 1. **PR 1**: Code that works with OLD and NEW schema — merge & deploy 2. **PR 2**: Migration files ONLY — merge (triggers CI but no auto-apply) 3. **Apply migrations manually**: ```bash # Apply to QA first npm run migrate:qa # Verify on QA, then apply to production npm run migrate:prod ``` #### Migration Rules **Safe migrations** (can apply anytime): * `ALTER TABLE ... ADD COLUMN` * `CREATE TABLE` * `CREATE INDEX` **Dangerous migrations** (use expand-contract): * `ALTER TABLE ... DROP COLUMN` * `ALTER TABLE ... RENAME COLUMN` * Changing column types ### NPM Scripts Reference | Script | Description | | ---------------------- | --------------------------------------- | | `npm run dev` | Start local development server | | `npm run build` | Build for production | | `npm run lint` | Run Biome linter | | `npm test` | Run unit tests | | `npm run test:e2e` | Run Playwright E2E tests | | `npm run test:api` | Run API tests | | `npm run deploy:qa` | Deploy to QA (use CI instead) | | `npm run deploy:prod` | Deploy to Production (use CI instead) | | `npm run migrate:qa` | Apply migrations to QA database | | `npm run migrate:prod` | Apply migrations to Production database | ### Secrets Management #### Cloudflare Secrets (per environment) ```bash # QA wrangler secret put SECRET_NAME --env qa # Production wrangler secret put SECRET_NAME --env production ``` Required secrets: * `MASTER_KEY` — Encryption key * `GITHUB_CLIENT_ID` — GitHub OAuth app ID * `GITHUB_CLIENT_SECRET` — GitHub OAuth app secret * `BOOTSTRAP_ADMIN_EMAIL` — Initial admin user email #### GitHub Secrets (for CI/CD) Set in GitHub repo → Settings → Secrets: * `CF_API_TOKEN` — Cloudflare API token * `CF_ACCOUNT_ID` — Cloudflare account ID * `D1_QA_DATABASE_ID` — QA database UUID * `D1_PROD_DATABASE_ID` — Production database UUID ### Troubleshooting #### CI/CD Failures ```bash # View recent workflow runs gh run list # View logs for a specific run gh run view --log-failed ``` #### Check Environment Health ```bash curl https://qa.aero2.dev/health curl https://aero2.dev/health ``` #### View Live Logs ```bash npx wrangler tail --env qa npx wrangler tail --env production ``` ### Quick Reference | Action | Command/Process | | ------------------- | ------------------------------------------------ | | Start coding | `npm run dev` | | Run tests locally | `npm test` | | Open PR | Push branch, CI runs automatically | | Deploy | Merge to main (automatic) | | Apply migration | `npm run migrate:qa` then `npm run migrate:prod` | | Rollback production | `npx wrangler rollback --env production` | | Add secret | `wrangler secret put NAME --env ENV` | ## Quickstart Get Aero2 running locally in 5 minutes. ### Prerequisites * Node.js 18+ * npm * [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/) ### 1. Install Dependencies ```bash npm install ``` ### 2. Create Local D1 Database ```bash npx wrangler d1 execute aero2 --local --file=./migrations/0001_initial_schema.sql ``` ### 3. Configure Environment Variables Create a `.dev.vars` file in the project root: ```bash # Required: Master key for encrypting secrets MASTER_KEY=local-dev-master-key-change-in-production-1234567890 # Optional: First user with this verified email gets admin role BOOTSTRAP_ADMIN_EMAIL=admin@example.com # Optional: GitHub OAuth (from https://github.com/settings/developers) GITHUB_CLIENT_ID=your_github_client_id GITHUB_CLIENT_SECRET=your_github_client_secret # Optional: Google OAuth (from https://console.cloud.google.com/apis/credentials) GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret ``` ### 4. Configure GitHub OAuth App (Optional) To test GitHub login locally: 1. Go to [GitHub Developer Settings](https://github.com/settings/developers) 2. Click **New OAuth App** 3. Fill in: * **Application name**: Aero2 Local Dev * **Homepage URL**: `http://localhost:8787` * **Authorization callback URL**: `http://localhost:8787/rp/callback/github` 4. Copy the Client ID and Client Secret to your `.dev.vars` file ### 5. Start Development Server ```bash npm run dev ``` The application will be available at [http://localhost:8787](http://localhost:8787). ### 6. Test the Login Flow 1. Navigate to [http://localhost:8787/login](http://localhost:8787/login) 2. Click **Sign in with GitHub** (or other configured provider) 3. Authorize the application 4. You'll be redirected to the dashboard showing your profile ### Project Structure ``` src/ ├── backend/ │ ├── index.ts # Main Hono app and route mounting │ ├── rp.ts # Relying Party routes (OAuth client flow) │ ├── op.ts # OIDC Provider routes (token issuance) │ ├── idp.ts # Identity Provider management API │ ├── clients.ts # OAuth client management API │ ├── roles.ts # Role management API │ ├── jwks.ts # JWKS Durable Object for key management │ ├── middleware/ │ │ └── auth.ts # Authentication and authorization middleware │ └── utils/ │ ├── token.ts # JWT signing and verification │ └── crypto.ts # Encryption and hashing utilities ├── frontend/ │ ├── pages/ # React pages (Login, Dashboard, Admin) │ ├── contexts/ # Auth context │ └── routes.tsx # React Router configuration └── migrations/ └── 0001_initial_schema.sql ``` ### Troubleshooting #### "No routes matched location" error Make sure the `run_worker_first` setting in `wrangler.json` includes your API routes. #### JWT verification fails with "no applicable key found" This usually happens after a database reset. Clear your browser cookies and log in again. #### Cookie not being set Ensure you're accessing via `http://localhost:8787`, not `127.0.0.1`. ## Security Audit Report **Date:** February 7, 2026 **Scope:** Backend codebase (`src/backend/`) ### Executive Summary This report identifies security vulnerabilities across the backend codebase, categorized by severity. The codebase demonstrates good security practices in many areas (parameterized queries, PKCE, encryption, RBAC) with specific gaps that need addressing. **Overall Security Posture:** Moderate — good foundation with critical gaps. ### Critical Issues #### 1. In-Memory Rate Limiting (No Persistence) **File:** `middleware/ratelimit.ts` Rate limiting uses an in-memory `Map` that resets on worker cold starts and doesn't share state across instances. Attackers can bypass limits by waiting for cold starts or hitting different isolates. **Recommendation:** Use Durable Objects, KV, or Cloudflare's built-in rate limiting. #### 2. CORS Allows All Origins When ALLOWED\_ORIGINS Not Set **File:** `op.ts` OAuth2 endpoints allow any origin when `ALLOWED_ORIGINS` is not set, enabling CSRF attacks. **Recommendation:** Require `ALLOWED_ORIGINS` in production. Never default to `*`. #### 3. Missing CSRF Protection on Stateful Operations **File:** `rp.ts` OAuth state is validated, but no CSRF token protection for general POST/PUT/DELETE API operations. **Recommendation:** Implement CSRF token validation for all state-changing operations. ### High Issues #### 4-6. SQL Injection Risk in Dynamic Query Construction **Files:** `idp.ts`, `roles.ts`, `users.ts` Dynamic SQL query construction uses string concatenation for column names. While parameters are bound, the pattern is fragile. ```typescript `UPDATE identity_providers SET ${updates.join(", ")} WHERE id = ?` ``` **Recommendation:** Validate all field names against a strict whitelist before building queries. #### 7. Error Messages Leak Information **Files:** Multiple Error messages in catch blocks include details that could leak system information. **Recommendation:** Return generic error messages to clients; log details server-side only. #### 8. Missing Input Validation on URL Parameters **File:** `op.ts` OAuth authorization endpoint parameters lack comprehensive validation. **Recommendation:** Validate all query parameters against strict patterns. #### 9. Authorization Code Replay Prevention Gap **File:** `op.ts` Authorization codes are deleted after retrieval, but if deletion fails, codes could be reused. **Recommendation:** Use atomic operations (DELETE with RETURNING) for one-time consumption. #### 10. Session Token Not Invalidated on User Disable **File:** `middleware/auth.ts` Existing session tokens remain valid when a user is disabled until they naturally expire. **Recommendation:** Implement token revocation on user disable. ### Medium Issues | # | Issue | File | Recommendation | | -- | --------------------------------------- | ---------------------- | --------------------------------------- | | 11 | Weak rate limiting configuration | `ratelimit.ts` | Stricter limits for auth endpoints | | 12 | Missing rate limiting on some endpoints | Multiple | Apply to all endpoints | | 13 | CORS credentials inconsistency | `index.ts` vs `op.ts` | Standardize configuration | | 14 | Hardcoded default issuer/audience | `token.ts` | Require in production, fail fast | | 15 | JWKS signing endpoint unauthenticated | `jwks.ts` | Add auth checks or ensure internal-only | | 16 | Refresh token rotation not atomic | `op.ts` | Use database transactions | | 17 | Missing validation on JWT payload | `token.ts` | Validate before signing | | 18 | Session expiration not consistent | `sessions.ts` | Enforce expiration on all operations | | 19 | Missing input sanitization | `users.ts`, `roles.ts` | Sanitize user input before storage | | 20 | Audit logging failures are silent | `audit.ts` | Alert on failures | ### Low Issues | # | Issue | Recommendation | | -- | ------------------------------------ | --------------------------- | | 21 | TODO comment about master key | Remove or clarify | | 22 | `console.error` for security events | Use structured logging | | 23 | Missing CSP headers | Add Content-Security-Policy | | 24 | Missing HSTS on all routes | Ensure HSTS is global | | 25 | No master key strength validation | Validate at startup | | 26 | Missing request ID in some errors | Include in all responses | | 27 | Clock skew tolerance may be too high | Consider reducing to 5-10s | | 28 | No redirect URI length validation | Add max length check | ### Positive Practices 1. All database queries use parameterized statements 2. PKCE with S256 required for all OAuth flows 3. Sensitive tokens and secrets encrypted at rest 4. Proper session tracking and revocation 5. Role-based access control implemented 6. SSRF protection for IdP URLs 7. Zod schemas for request validation 8. Comprehensive audit logging 9. Refresh token rotation on refresh 10. Email verification required for bootstrap admin ### Remediation Priority #### Immediate (Critical/High) 1. Replace in-memory rate limiting with Durable Objects/KV 2. Fix CORS to require ALLOWED\_ORIGINS in production 3. Add CSRF protection for state-changing operations 4. Harden dynamic SQL query construction 5. Sanitize error messages returned to clients 6. Implement atomic authorization code consumption #### Short-term (Medium) 1. Tighten rate limiting configuration 2. Standardize CORS configuration 3. Require ISSUER/AUDIENCE environment variables 4. Implement atomic refresh token rotation #### Long-term (Low) 1. Add CSP headers 2. Improve logging infrastructure 3. Validate master key strength at startup ## 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: ```typescript 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`, `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. ```typescript 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: ```sql 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: ```typescript 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: | Role | Permissions | | ------- | --------------------------------------- | | `admin` | clients:\*, users:\*, idps:\*, roles:\* | | `user` | users:read | #### 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=Strict` for session cookies ### Configuration #### Required Secrets ```bash 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`: ```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