Architecture
Infrastructure
Section titled “Infrastructure”The auth service runs on Cloudflare Workers with two storage backends:
D1 (SQLite)
Section titled “D1 (SQLite)”Durable storage for users, projects, sessions, and PINs. ACID transactions for token rotation.Ephemeral storage for PKCE codes (5-min TTL), rate limit counters, and magic link tokens (15-min TTL).Database Schema
Section titled “Database Schema”users├── id (TEXT, PK) -- usr_xxx├── email (TEXT, UNIQUE)├── password_hash (TEXT) -- Argon2id, nullable├── email_verified (INT) -- 0 or 1├── created_at (TEXT)└── updated_at (TEXT)
projects├── id (TEXT, PK) -- proj_xxx (= client_id)├── name (TEXT)├── owner_id (TEXT, FK)├── client_secret_hash (TEXT) -- Argon2id├── redirect_uris (TEXT) -- JSON array, exact-match├── signing_key (TEXT) -- 32-byte hex (HS256)├── auth_methods (TEXT) -- JSON: ["password","pin","magic_link"]├── branding (TEXT) -- JSON: colors, logo, mode├── registration (TEXT) -- "open" | "closed" | "invite_only"└── created_at (TEXT)
project_users├── project_id (TEXT, PK)├── user_id (TEXT, PK)├── role (TEXT) -- "admin" | "member"├── status (TEXT) -- "active" | "blocked"├── metadata (TEXT) -- JSON, reserved└── joined_at (TEXT)
project_pins├── id (TEXT, PK) -- pin_xxx├── project_id (TEXT, FK)├── pin_hash (TEXT) -- Argon2id├── label (TEXT)├── status (TEXT) -- "active" | "revoked"├── privileges (TEXT) -- JSON array├── created_at (TEXT)└── revoked_at (TEXT)
refresh_tokens├── id (TEXT, PK) -- 64-char hex├── user_id (TEXT) -- NULL for PIN sessions├── project_id (TEXT, FK)├── expires_at (TEXT)├── revoked (INT) -- 0 or 1├── revoked_at (TEXT)├── is_pin_session (INT)├── pin_id (TEXT)└── created_at (TEXT)Security Model
Section titled “Security Model”Token Isolation
Section titled “Token Isolation”Each project has its own signing_key. A token issued for proj_gym cannot be verified by proj_trip — they use different HS256 secrets. This means:
- A compromise of one app’s signing key doesn’t affect others
- Tokens cannot be replayed across projects
- Each app independently decides what claims to trust
Stateless Access, Stateful Refresh
Section titled “Stateless Access, Stateful Refresh”Access tokens are verified locally by client apps — no network call to the auth service. They embed all claims (role, status, email) needed for authorization decisions.
Refresh tokens require a round-trip to the auth service. This is where:
- User blocking is enforced
- PIN revocation is checked
- Token rotation happens
- Session tracking is maintained
Cryptography
Section titled “Cryptography”| Purpose | Algorithm | Details |
|---|---|---|
| Password hashing | Argon2id | 19 MiB memory, 2 iterations, 1 parallelism |
| PIN hashing | Argon2id | Same parameters as passwords |
| Access token signing | HS256 | Per-project 256-bit key |
| ID token signing (OIDC) | ES256 | Single EC P-256 key pair |
| State encryption | AES-GCM | 256-bit key derived from JWT secret |
| Refresh token | Random | 256-bit (64 hex chars), stored plaintext |
| PKCE verifier | Random | 256-bit (32 bytes, base64url) |
Rate Limiting
Section titled “Rate Limiting”All rate limits are enforced via KV counters with sliding windows. They fail closed — if KV is unavailable, the request is denied.
| Endpoint | Limit | Window |
|---|---|---|
| Login (password/PIN) | 5 attempts | 15 min per IP per project |
| Signup | 3 attempts | 1 hour per IP |
| Magic link send | 3 sends | 1 hour per email |
| Token exchange | 20 requests | 60 sec per client_id |
Request Flow
Section titled “Request Flow”1. User visits app.beshoy.ai2. Middleware checks for valid access_token cookie ├── Valid → proceed to app ├── Expired → try refresh (step 5) └── Missing → redirect to auth (step 3)
3. Redirect to auth.beshoy.ai/oauth/authorize └── With: client_id, redirect_uri, code_challenge, state
4. User authenticates at auth.beshoy.ai ├── Password → verify Argon2 hash ├── PIN → verify against all project PINs └── Magic link → verify KV token └── On success: store PKCE data in KV, redirect back with code
5. App exchanges code at POST /oauth/token ├── Verify client_secret ├── Verify PKCE (code_verifier vs stored challenge) ├── Create access token (HS256, 5 min) ├── Create refresh token (stored in D1) └── Return both tokens
6. App sets cookies, user continues
7. On access token expiry: POST /oauth/token with grant_type=refresh_token ├── Verify refresh token exists + not expired ├── Check user status (blocked → 403) ├── Check PIN status if PIN session (revoked → 403) ├── Revoke old refresh token (rotation) ├── Issue new access + refresh tokens └── Return both tokens