Login CAPTCHA Integration (Cap)
This document describes the design of introducing Cap — an open-source CAPTCHA solution based on Proof-of-Work (PoW) and invisible browser fingerprint features — into the OpenFlare control plane, to protect the login API against brute-force attacks and credential-stuffing by crawlers.
1. Business Background and Product Scope
Background and Pain Points
The OpenFlare login endpoint /api/v1/user/login lacks user-dimension protection; attackers can use proxy pools to perform credential stuffing and brute-force attacks on high-privilege accounts (such as root). At the same time, standard visual CAPTCHAs are unfriendly to login-page UX and accessibility.
Product Scope and Technology Choice
- Technology choice: Cap (a Proof-of-Work-driven, invisible, image-free CAPTCHA solution).
- Core principle: the client (Widget/page) obtains a proof-of-work (PoW) challenge from the server, computes the solution in the browser background, and sends the answer back. The server verifies the answer to complete human-machine verification.
- Advantages: invisible, image-free, no dependency on external third-party API nodes (private), tiny package size.
- Integration scope: the control-plane Server login API (
/api/v1/user/login) and the frontend login page. - Config granularity: admins can toggle the CAPTCHA on/off anytime via the console Option table (
cap_login_enabled).
2. System Architecture and Interaction Sequence
2.1 Module Responsibilities
- Frontend:
- Introduces the
cap-widget(React 19 custom element) on the login page. - On form submit, accompanies the submission with the
cap-tokensolved by the Widget.
- Introduces the
- Server (control-plane backend):
- Exposes
POST /api/cap/challengeto distribute the PoW challenge and a signed JWT token to the client. - Exposes
POST /api/cap/redeemto verify the submitted PoW solution and issue a login credential (Redeem Token) with an expiry time. - Stores the Redeem Token and its expiry in the in-memory/Redis cache.
- In
POST /api/v1/user/login, when CAPTCHA protection is enabled, first validates and consumes (single-use) the correspondingcap-token.
- Exposes
2.2 Verification Flow Sequence Diagram
mermaid
sequenceDiagram
autonumber
actor User as User
participant Browser as Browser (Frontend Web)
participant Server as OpenFlare Server (Backend)
participant Cache as Memory/Redis Cache
User->>Browser: Open login page
Browser->>Server: POST /api/cap/challenge (get challenge)
Server->>Browser: Return {challenge, token, expires} (JWT format)
Note over Browser: Widget computes the PoW challenge in background (WASM/Worker)
Browser->>Server: POST /api/cap/redeem (submit solutions + token)
alt PoW solution valid
Server->>Cache: Store Redeem Token (tokenKey:expires)
Server->>Browser: Return {success: true, token} (i.e. cap-token)
else validation failed
Server->>Browser: Return {success: false, reason}
end
User->>Browser: Enter account/password, click login
Browser->>Server: POST /api/v1/user/login (with X-Cap-Token in HTTP header)
alt CapLoginEnabled = true
Server->>Server: Middleware (CapAuth) validates and consumes X-Cap-Token
alt token valid, not expired, not consumed
Server->>Server: c.Next() -> normal login logic (Bcrypt password check)
Server->>Browser: Return login success (Session Cookie)
else token invalid or already consumed
Server->>Browser: Intercept and return CAPTCHA error (401 Unauthorized)
end
else CapLoginEnabled = false
Server->>Server: c.Next() -> normal login logic
end3. Core APIs and Data Model
3.1 API Definitions
1. Get Challenge (POST /api/cap/challenge)
- Method:
POST - Auth: public
- Response payload (unified API envelope,
datais the business payload):json{ "error_msg": "", "data": { "challenge": { "c": 1, "s": 32, "d": 4 }, "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires": 1717660800000 } }
2. Redeem Challenge (POST /api/cap/redeem)
- Method:
POST - Request payload:json
{ "token": "challenge_jwt_token_here", "solutions": [12345, 67890, 54321] } - Response payload (success):json
{ "success": true, "token": "random_id:ver_token", "expires": 1717661000000 }
3. Login API (POST /api/v1/user/login)
- Request payload unchanged:json
{ "username": "root", "password": "your_password" } - CAPTCHA carrier: placed in the HTTP Request Header
X-Cap-Token.
4. Replay Attack Protection and Security Trade-offs
- JWT temporary state binding: the challenge is signed into the JWT payload at generation time, including an expiry limit (10 minutes).
- Replay interception (nonce consumption): when the client calls
/redeemto submit the solution, the backend marks the JWT signature as used in the cache. Re-submitting the same solution package returnsalready_redeemed. - Redeem single-use (one-time invalidation): when the client logs in and submits the
cap-token, the backend immediately deletes the key from the cache after validating it, preventing attackers from extracting historical validcap-tokens for login replay. - Seamless verification: by tuning parameters like
c(challenge count) andd(difficulty), you balance solve time against anti-crawler strength; users solve silently in the background without interrupting the login flow.