Member Authentication API
**Referenced Files in This Document** - [worker.js](file://worker.js) - [src/alliance-login.njk](file://src/alliance-login.njk) - [wrangler.jsonc](file://wrangler.jsonc) - [package.json](file://package.json)Table of Contents
- Introduction
- Project Structure
- Core Components
- Architecture Overview
- Detailed Component Analysis
- Dependency Analysis
- Performance Considerations
- Troubleshooting Guide
- Conclusion
- Appendices
Introduction
This document provides comprehensive API documentation for the member authentication system. It covers the magic link authentication endpoints, session management using HMAC-signed JWT tokens stored in HttpOnly cookies, security mechanisms (CSRF protection, token expiration, constant-time HMAC verification, member approval via KV namespace), request/response schemas, authentication flow diagrams, error handling patterns, client implementation examples, anti-bot strategies (honeypot), member enumeration prevention, CORS policies, cookie security attributes, and troubleshooting guidance.
Project Structure
The authentication logic is implemented in a Cloudflare Worker that intercepts specific routes and serves static assets otherwise. The login UI is generated by a Nunjucks template.
graph TB
subgraph "Cloudflare Worker"
W["worker.js<br/>Routes: /alliance/login/, /alliance/verify/, /alliance/logout/<br/>Session cookie handling"]
end
subgraph "Static Assets"
AS["ASSETS binding<br/>Serve built Eleventy site"]
end
subgraph "KV Namespaces"
ME["MEMBER_EMAILS<br/>Key: member:<email><br/>Value: 1"]
MT["MAGIC_TOKENS<br/>Key: token:<hex><br/>Value: <email><br/>TTL: 900s"]
end
subgraph "External Services"
RS["Resend API<br/>Send magic link emails"]
end
U["Browser"] --> W
W --> AS
W --> ME
W --> MT
W --> RS
Diagram sources
- [worker.js:1-321](file://worker.js#L1-L321)
- [wrangler.jsonc:17-26](file://wrangler.jsonc#L17-L26)
Section sources
- [worker.js:1-321](file://worker.js#L1-L321)
- [wrangler.jsonc:1-35](file://wrangler.jsonc#L1-L35)
Core Components
- Magic link issuance endpoint: POST /alliance/login/
- Token verification and session creation: GET /alliance/verify/
- Session termination: GET /alliance/logout/
- Session cookie: ace_member_session (HttpOnly, Secure, SameSite=Lax, Max-Age=30 days)
- Token storage: KV namespace MAGIC_TOKENS with 15-minute TTL
- Member approval: KV namespace MEMBER_EMAILS
- Email delivery: Resend API
- Frontend login UI: src/alliance-login.njk
Section sources
- [worker.js:94-147](file://worker.js#L94-L147)
- [worker.js:150-177](file://worker.js#L150-L177)
- [worker.js:279-295](file://worker.js#L279-L295)
- [worker.js:12-14](file://worker.js#L12-L14)
- [wrangler.jsonc:17-26](file://wrangler.jsonc#L17-L26)
- [src/alliance-login.njk:21-40](file://src/alliance-login.njk#L21-L40)
Architecture Overview
The system uses a stateless magic link flow with server-side token storage and signed session cookies.
sequenceDiagram
participant C as "Client Browser"
participant W as "Worker (worker.js)"
participant KV as "KV : MAGIC_TOKENS"
participant KE as "KV : MEMBER_EMAILS"
participant R as "Resend API"
Note over C,W : Step 1 : Request Magic Link
C->>W : POST /alliance/login/ {email, _gotcha}
W->>KE : Lookup member : <email>
KE-->>W : Approved? (hit/miss)
alt Approved
W->>KV : Put token : <hex>=<email>, TTL=900
W->>R : Send magic link email
else Not approved
W->>R : Skip sending email
end
W-->>C : Redirect /alliance/login/?sent=1
Note over C,W : Step 2 : Verify Token and Create Session
C->>W : GET /alliance/verify/?token=<hex>
W->>KV : Get token : <hex>
KV-->>W : <email> or null
alt Valid token
W->>KV : Delete token : <hex>
W->>W : Sign session token (payload|expiry)
W-->>C : 302 to /alliance/members/ with Set-Cookie ace_member_session
else Invalid/expired
W-->>C : Redirect /alliance/login/?error=expired
end
Note over C,W : Step 3 : Logout
C->>W : GET /alliance/logout/
W-->>C : 302 to /alliance/login/ with Set-Cookie ace_member_session=; Max-Age=0
Diagram sources
- [worker.js:94-147](file://worker.js#L94-L147)
- [worker.js:150-177](file://worker.js#L150-L177)
- [worker.js:279-295](file://worker.js#L279-L295)
Detailed Component Analysis
Endpoint: POST /alliance/login/
- Method: POST
- Purpose: Issue a one-time magic link to an approved member’s email
- Request
- Form fields:
- email: string (required)
- _gotcha: string (optional, hidden field for bot detection)
- Form fields:
- Response
- Redirect to /alliance/login/?sent=1 regardless of whether the email was approved
- Prevents member enumeration by always returning the same “check your email” message
- Security
- Honeypot field _gotcha blocks automated submissions
- Email validation performed before KV lookup
- KV namespaces checked; missing KV returns 503
- Implementation highlights
- Approved members are looked up in MEMBER_EMAILS
- A random 32-byte token is generated and stored in MAGIC_TOKENS with TTL=900 seconds
- An email is sent via Resend API with a link to /alliance/verify/?token=
flowchart TD
Start(["POST /alliance/login/"]) --> Parse["Parse form data<br/>email, _gotcha"]
Parse --> Honeypot{"_gotcha present?"}
Honeypot --> |Yes| BotRedirect["Redirect /alliance/login/?sent=1"]
Honeypot --> |No| Validate["Validate email format"]
Validate --> Valid{"Valid email?"}
Valid --> |No| ErrorEmail["Redirect /alliance/login/?error=email"]
Valid --> |Yes| Lookup["Lookup member:<email> in MEMBER_EMAILS"]
Lookup --> Approved{"Approved?"}
Approved --> |No| SendRedirect["Redirect /alliance/login/?sent=1"]
Approved --> |Yes| GenToken["Generate random token (32 bytes)"]
GenToken --> Store["Put token:<hex>=<email> in MAGIC_TOKENS<br/>TTL=900"]
Store --> SendMail["Send magic link via Resend"]
SendMail --> SendRedirect
BotRedirect --> End(["End"])
ErrorEmail --> End
SendRedirect --> End
Diagram sources
- [worker.js:97-147](file://worker.js#L97-L147)
Section sources
- [worker.js:94-147](file://worker.js#L94-L147)
- [src/alliance-login.njk:21-40](file://src/alliance-login.njk#L21-L40)
Endpoint: GET /alliance/verify/
- Method: GET
- Purpose: Validate the magic link token and create a session cookie
- Request
- Query parameter:
- token: string (required)
- Query parameter:
- Response
- On success: 302 redirect to /alliance/members/ with Set-Cookie ace_member_session
- On failure: 302 redirect to /alliance/login/?error=expired or error=invalid
- Security
- Token must exist in MAGIC_TOKENS and be deleted upon successful verification
- Session cookie is HttpOnly, Secure, SameSite=Lax, Max-Age=30 days
- Session token payload includes email and expiry; verified with HMAC signature
- Constant-time HMAC verification prevents timing attacks
- Implementation highlights
- Validates presence of token
- Retrieves email from MAGIC_TOKENS
- Deletes the token immediately to enforce one-time use
- Creates session token with HMAC signature and sets cookie
flowchart TD
Start(["GET /alliance/verify?token"]) --> CheckToken{"token provided?"}
CheckToken --> |No| ErrInvalid["Redirect /alliance/login/?error=invalid"]
CheckToken --> |Yes| LoadToken["Get token:<hex> from MAGIC_TOKENS"]
LoadToken --> Found{"Found and not expired?"}
Found --> |No| ErrExpired["Redirect /alliance/login/?error=expired"]
Found --> |Yes| DeleteToken["Delete token:<hex>"]
DeleteToken --> CreateSession["Create session token (payload|expiry)<br/>HMAC signature"]
CreateSession --> SetCookie["Set-Cookie ace_member_session<br/>HttpOnly; Secure; SameSite=Lax; Max-Age=2592000"]
SetCookie --> RedirectMembers["302 to /alliance/members/"]
ErrInvalid --> End(["End"])
ErrExpired --> End
RedirectMembers --> End
Diagram sources
- [worker.js:153-177](file://worker.js#L153-L177)
Section sources
- [worker.js:150-177](file://worker.js#L150-L177)
- [worker.js:12-14](file://worker.js#L12-L14)
Endpoint: GET /alliance/logout/
- Method: GET
- Purpose: Terminate the current session by clearing the session cookie
- Response
- 302 redirect to /alliance/login/ with Set-Cookie ace_member_session=; Max-Age=0
- Behavior
- Expires the cookie immediately, removing the session
flowchart TD
Start(["GET /alliance/logout/"]) --> ExpireCookie["Set-Cookie ace_member_session=; Max-Age=0"]
ExpireCookie --> RedirectLogin["302 to /alliance/login/"]
RedirectLogin --> End(["End"])
Diagram sources
- [worker.js:282-295](file://worker.js#L282-L295)
Section sources
- [worker.js:279-295](file://worker.js#L279-L295)
Session Management and Cookie Security
- Session cookie name: ace_member_session
- Attributes:
- Path=/
- HttpOnly
- Secure
- SameSite=Lax
- Max-Age=2592000 seconds (30 days)
- Token format:
- Payload: Base64-encoded "email|expiry"
- Signature: HMAC-SHA256 over payload using SESSION_SECRET
- Token: "<base64_payload>.
"
- Verification:
- Split by "."
- Decode payload and extract email and expiry
- Check expiry and recompute HMAC signature
- Constant-time comparison to prevent timing attacks
classDiagram
class SessionToken {
+string payload_b64
+string signature
+string toString()
}
class Crypto {
+hmacSign(data, secret) string
}
class Worker {
+createSessionToken(email, secret) string
+verifySessionToken(token, secret) string|null
}
SessionToken --> Crypto : "uses HMAC"
Worker --> Crypto : "signs/verifies"
Diagram sources
- [worker.js:20-58](file://worker.js#L20-L58)
- [worker.js:32-58](file://worker.js#L32-L58)
Section sources
- [worker.js:12-14](file://worker.js#L12-L14)
- [worker.js:20-58](file://worker.js#L20-L58)
Security Mechanisms
- CSRF protection
- No CSRF tokens are embedded in forms; however, the login form uses a hidden field (_gotcha) to detect bots. The absence of CSRF tokens is acceptable because:
- The endpoint is POST-only and does not modify state via GET
- The magic link is short-lived and one-time use
- The session cookie is HttpOnly and Secure
- No CSRF tokens are embedded in forms; however, the login form uses a hidden field (_gotcha) to detect bots. The absence of CSRF tokens is acceptable because:
- Token expiration
- Magic link TTL: 900 seconds (15 minutes)
- Session cookie Max-Age: 2592000 seconds (30 days)
- Constant-time HMAC verification
- Implemented to prevent timing attacks during signature comparison
- Member approval via KV namespace
- MEMBER_EMAILS stores approved emails with keys "member:
" - Only approved emails receive magic links
- MEMBER_EMAILS stores approved emails with keys "member:
- Anti-bot and enumeration prevention
- Honeypot field _gotcha blocks automated submissions
- Always returns “check your email” regardless of membership status
- Token deletion ensures one-time use
Section sources
- [worker.js:97-147](file://worker.js#L97-L147)
- [worker.js:153-177](file://worker.js#L153-L177)
- [worker.js:49-54](file://worker.js#L49-L54)
- [wrangler.jsonc:17-26](file://wrangler.jsonc#L17-L26)
Request/Response Schemas
-
POST /alliance/login/
- Request body: multipart/form-data
- email: string (required)
- _gotcha: string (optional, hidden)
- Response: 302 redirect
- Location: /alliance/login/?sent=1 or /alliance/login/?error=
- Notes: Always returns the same “check your email” message for approved/unapproved users
- Location: /alliance/login/?sent=1 or /alliance/login/?error=
- Request body: multipart/form-data
-
GET /alliance/verify/
- Query parameters:
- token: string (required)
- Response: 302 redirect
- Success: Location: /alliance/members/ with Set-Cookie ace_member_session
- Failure: Location: /alliance/login/?error=expired or error=invalid
- Query parameters:
-
GET /alliance/logout/
- Response: 302 redirect
- Location: /alliance/login/ with Set-Cookie ace_member_session=; Max-Age=0
- Response: 302 redirect
-
Session cookie (ace_member_session)
- Value: "<base64_payload>.
" - Attributes: Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000
- Value: "<base64_payload>.
Section sources
- [worker.js:94-147](file://worker.js#L94-L147)
- [worker.js:150-177](file://worker.js#L150-L177)
- [worker.js:279-295](file://worker.js#L279-L295)
- [worker.js:12-14](file://worker.js#L12-L14)
Client Implementation Examples
- HTML form submission
- Use a standard form targeting POST /alliance/login/
- Include a hidden input named _gotcha
- Example fields: email (required), _gotcha (hidden)
- Handling redirects
- After POST, redirect to /alliance/login/?sent=1 indicates the “check your email” screen
- On GET /alliance/verify, follow the 302 redirect to /alliance/members/ and ensure cookies are accepted
- Logout
- Navigate to /alliance/logout/ to clear the session cookie
Section sources
- [src/alliance-login.njk:21-40](file://src/alliance-login.njk#L21-L40)
- [worker.js:94-147](file://worker.js#L94-L147)
- [worker.js:150-177](file://worker.js#L150-L177)
- [worker.js:279-295](file://worker.js#L279-L295)
CORS Policies
- The worker handles preflight OPTIONS for /api/auth and sets:
- Access-Control-Allow-Origin: https://acestrategies.au
- Access-Control-Allow-Methods: GET, POST, OPTIONS
- Access-Control-Allow-Headers: Content-Type
- These policies apply to the GitHub OAuth endpoints and are unrelated to the member authentication endpoints.
Section sources
- [worker.js:183-191](file://worker.js#L183-L191)
Dependency Analysis
- Worker routes depend on:
- KV namespaces MEMBER_EMAILS and MAGIC_TOKENS
- Secret environment variables: SESSION_SECRET, RESEND_API_KEY
- Static asset binding ASSETS for serving the site
- External dependencies:
- Resend API for sending emails
- Build and deployment:
- Eleventy builds static assets
- Wrangler deploys the Worker and binds KV namespaces and secrets
graph LR
W["worker.js"] --> ME["MEMBER_EMAILS KV"]
W --> MT["MAGIC_TOKENS KV"]
W --> RS["Resend API"]
W --> AS["ASSETS binding"]
W --> SEC["Secrets: SESSION_SECRET, RESEND_API_KEY"]
Diagram sources
- [worker.js:1-10](file://worker.js#L1-L10)
- [wrangler.jsonc:17-34](file://wrangler.jsonc#L17-L34)
Section sources
- [worker.js:1-10](file://worker.js#L1-L10)
- [wrangler.jsonc:17-34](file://wrangler.jsonc#L17-L34)
- [package.json:1-32](file://package.json#L1-L32)
Performance Considerations
- KV latency: Each request reads/writes to KV namespaces; keep token TTL low (15 minutes) and leverage Cloudflare’s global edge caching for static assets.
- Email delivery: Resend API adds network latency; consider retry/backoff in production deployments.
- Session cookie size: Minimal overhead due to compact token format.
- Constant-time HMAC verification avoids timing side channels without significant CPU cost.
Troubleshooting Guide
Common issues and resolutions:
- Missing KV namespaces
- Symptom: 503 response indicating KV namespaces not configured
- Resolution: Create MEMBER_EMAILS and MAGIC_TOKENS namespaces and bind them in wrangler.jsonc
- Missing secrets
- Symptom: Errors when signing tokens or sending emails
- Resolution: Set SESSION_SECRET and RESEND_API_KEY via wrangler secret put
- Invalid or expired token
- Symptom: Redirect to /alliance/login/?error=invalid or error=expired
- Resolution: Ensure the token is fresh (within 15 minutes) and not reused
- Email not received
- Symptom: Redirect to “check your email” but no email arrives
- Resolution: Confirm the email is approved in MEMBER_EMAILS; verify Resend API key and domain configuration
- Cookie not set
- Symptom: Redirect succeeds but user remains unauthenticated
- Resolution: Ensure browser accepts third-party cookies if applicable; confirm cookie attributes (HttpOnly, Secure, SameSite=Lax) are compatible with your deployment
- Honeypot detected
- Symptom: Immediate redirect to “sent=1”
- Resolution: Ensure the hidden _gotcha field is present and not manipulated by client-side scripts
Section sources
- [worker.js:70-75](file://worker.js#L70-L75)
- [worker.js:153-177](file://worker.js#L153-L177)
- [worker.js:97-147](file://worker.js#L97-L147)
- [wrangler.jsonc:17-34](file://wrangler.jsonc#L17-L34)
Conclusion
The member authentication system provides a secure, stateless magic link flow with robust protections against enumeration, timing attacks, and misuse. By leveraging KV namespaces for approvals and token storage, HMAC-signed session cookies, and anti-bot measures, it balances usability with strong security. Proper configuration of KV namespaces and secrets is essential for reliable operation.
Appendices
Appendix A: Environment Variables and KV Namespaces
- Required secrets:
- SESSION_SECRET: Random 32+ byte string for HMAC signing
- RESEND_API_KEY: Resend API key for sending emails
- Required KV namespaces (bind in wrangler.jsonc):
- MEMBER_EMAILS: Key format "member:
", value "1" - MAGIC_TOKENS: Key format "token:
", value " ", TTL 900 seconds
- MEMBER_EMAILS: Key format "member:
Section sources
- [worker.js:4-10](file://worker.js#L4-L10)
- [wrangler.jsonc:17-26](file://wrangler.jsonc#L17-L26)