Auth Worker
- Overview
- Entry Point and Routing
- Handler Reference
- login
- refresh-token
- signup
- change-password
- verify-email
- validate-email
- create-session-token
- get-session-account
- get-user-info
- get-mfa-token
- enroll-first-mfa-authenticator
- enroll-mfa-authenticator
- get-mfa-authenticators
- challenge-mfa-oob-authenticator
- submit-mfa-oob-challenge
- social-signin
- social-callback
- floatmetric-track
- Castle Integration
- QA / Integration Test Bypass
- Local Development
Overview
The auth Cloudflare Worker is an Auth0 authentication proxy deployed to auth.floatme.io (prod) and auth.test.floatme.io (test).
It is the sole authentication entry point for the FloatMe mobile app and handles every login, signup, MFA, and token-exchange operation.
A dedicated proxy exists for three reasons:
-
Secret isolation — Auth0
client_secret, MFA audience credentials, and Castle API keys never leave Cloudflare’s secret store. The mobile app sends only a username and password; all Auth0 client credentials are injected by the worker before forwarding. -
Scope and realm injection — Every Auth0 request is stamped with the correct
scope(openid email profile offline_access),audience,realm/connection, andgrant_type. Clients do not need to know these values. -
Castle fraud-detection layer — Pre-flight and post-flight Castle API calls gate login, signup, refresh, and MFA OOB submissions without requiring any changes to Auth0 or the mobile client.
Entry Point and Routing
Source: cloudflare/workers/auth/index.ts
The worker exposes a single Cloudflare fetch handler.
Each incoming request is matched against a static dispatch table keyed on { pathname, method }.
An unknown pathname returns 403 Forbidden; a known pathname with a wrong method returns 405 Method Not Allowed.
| Path | Method | Handler module |
|---|---|---|
|
POST |
|
|
POST |
|
|
POST |
|
|
POST |
|
|
POST |
|
|
POST |
|
|
GET |
|
|
POST |
|
|
GET |
|
|
POST |
|
|
POST |
|
|
POST |
|
|
GET |
|
|
POST |
|
|
POST |
|
|
GET |
|
|
GET |
|
|
POST |
|
/mfa/associate and /mfa/first-enroll share the same handler (enrollFirstMfaAuthenticatorHandler).
enroll-mfa-authenticator.ts is a separate module (used for adding a subsequent authenticator given an explicit phone_number) that is exported from the handlers index but is not yet wired to a distinct route.
See enroll-mfa-authenticator below for its interface.
|
Every handler is wrapped by helpers/request-handler.ts, which provides uniform error catching and Datadog structured logging, and by ctx.waitUntil(DatadogLogger.sendQueuedLogs(env)) to flush logs after the response is sent.
Handler Reference
login
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
|
Castle calls |
Pre-login filter → post-login risk (on success) or failed-login filter (on failure) |
user-service |
|
Side effects |
Unblocks PAUSED users in Auth0 Management API if blocked-but-not-banned; QA user provisioning in test env (see QA Helper) |
The handler injects client_id, client_secret, audience, realm, and scope from env vars.
It also forwards CF-Connecting-IP (or x-real-ip) to Auth0 via the auth0-forwarded-for header.
If Auth0 returns mfa_required, the error is passed through transparently so the client can begin the MFA flow.
refresh-token
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
|
Castle calls |
Pre-refresh filter → post-refresh risk (on success) |
user-service |
None |
Side effects |
None |
Castle authentication method is reported as $biometrics / refresh-token.
The worker passes scope and client_id but does not inject client_secret (not required for refresh grants in Auth0’s PKCE-style configuration here).
signup
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
|
Castle calls |
Pre-signup filter → post-signup risk (on success) |
user-service |
None |
Side effects |
|
Pre-signup Castle call sends email, username, and phone in E.164 format.
Post-signup Castle risk call additionally sends full address fields and the Auth0-assigned user ID.
If user_exists error code is returned by Auth0, the handler normalizes it to { error: "invalid_signup" } (400).
change-password
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
|
Castle calls |
Pre-change-password filter |
user-service |
None |
Side effects |
Auth0 sends a password-reset email; response is the Auth0 plain-text confirmation string |
Castle event type is $password_reset_request / $attempted.
verify-email
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
|
Castle calls |
None |
user-service |
None |
Side effects |
Auth0 triggers a verification email; requires a valid Bearer token in the |
The JWT sub claim is split on | to derive provider and user_id for the Management API payload.
validate-email
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
None |
Castle calls |
Pre-signup email-validation filter (custom event |
user-service |
|
Side effects |
None — read-only availability check |
Returns 200 OK with "Email Is Available" when the user-service 404s (email not found), or 401 Unauthorized with "Email Is Invalid" when the user already exists.
Designed to be called before the signup form is submitted.
create-session-token
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
None |
Castle calls |
None |
user-service |
None (calls transactions-service) |
Side effects |
None |
Proxies to GET {FLOATME_TXN_SERVICE_URL}/transactions/layer/session using AWS SigV4 signing (via aws4fetch).
Returns the Layer session token payload verbatim.
get-session-account
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
None |
Castle calls |
None |
user-service |
None (calls transactions-service) |
Side effects |
None |
Proxies to POST {FLOATME_TXN_SERVICE_URL}/transactions/layer/session/account with body { public_token } using AWS SigV4 signing.
get-user-info
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
|
Castle calls |
None |
user-service |
None |
Side effects |
None |
Passes the caller’s Authorization header directly to Auth0’s /userinfo endpoint and returns the response verbatim.
Returns 401 if Auth0 reports any error.
get-mfa-token
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
|
Castle calls |
None |
user-service |
None |
Side effects |
Returns a short-lived MFA token scoped to |
This token is used exclusively as a Bearer token for the MFA enrollment and authenticator-listing endpoints. It is distinct from the regular authentication audience token.
enroll-first-mfa-authenticator
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
|
Castle calls |
None |
user-service |
|
Side effects |
Auth0 creates the OOB authenticator and sends an SMS or email OOB code; response normalizes to |
For SMS enrollment the phone number is fetched from the user-service (not supplied by the client) so the client cannot enroll an arbitrary number.
Only the last 4 digits of the phone number are returned in masked_phone_number.
For email enrollment the email is masked using helpers/email.ts before returning.
Auth0 errors invalid_phone_number, User is already enrolled, and invalid MFA token are mapped to structured error codes.
enroll-mfa-authenticator
| Field | Detail |
|---|---|
Path / Method |
Not wired to a route in the current dispatch table (exported but unused as of Phase 5) |
Auth0 endpoint |
|
Castle calls |
None |
user-service |
None |
Side effects |
Enrolls an additional authenticator using an explicit |
Differs from enroll-first-mfa-authenticator in that the phone number is taken directly from the request body rather than fetched from user-service.
This is appropriate when adding a second authenticator for a user who has already completed first enrollment.
Phase 6/9 should confirm whether a route should be registered or whether this handler is superseded by the /mfa/associate alias.
get-mfa-authenticators
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
|
Castle calls |
None |
user-service |
None |
Side effects |
None |
Requires an MFA token in the Authorization header.
Filters Auth0’s full authenticator list to OOB authenticators only and returns a normalized array: [{ id, name, active, oob_channel }].
challenge-mfa-oob-authenticator
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
|
Castle calls |
None |
user-service |
None |
Side effects |
Auth0 triggers an OOB code delivery (SMS or email) to the enrolled authenticator; response is |
Injects client_id, client_secret, and challenge_type: "oob" from env vars.
The client supplies authenticator_id and mfa_token.
Rate-limit error Too many SMS sent by the user is mapped to { error: "too_many_sms" }.
submit-mfa-oob-challenge
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
|
Castle calls |
Post-challenge risk |
user-service |
None |
Side effects |
Exchanges OOB code for a full token set; Castle event type |
Castle blocking applies: if blocked, returns { error: "unauthorized", message: "blocked by security policy" }.
social-signin
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
|
Castle calls |
None |
user-service |
None |
Side effects |
302 redirect to Auth0’s |
The redirect_uri is environment-aware: https://auth.floatme.io/social/complete (prod) or https://auth.test.floatme.io/social/complete (test).
social-callback
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
|
Castle calls |
None |
user-service |
|
Side effects |
302 redirect to |
The signup query parameter is true when the user does not exist in the user-service (i.e., this is a first-time social login that requires FloatMe account creation in the app).
The ID token is validated against Auth0’s JWKs before checking user existence.
| Tokens are currently passed as URL query parameters in the deeplink, which exposes them to URL logging and referrer leakage. This is a known security risk and a candidate for migration to a short-lived authorization code exchange pattern (Authorization Code + PKCE). |
floatmetric-track
| Field | Detail |
|---|---|
Path / Method |
|
Auth0 endpoint |
None |
Castle calls |
None |
user-service |
None (calls metrics service) |
Side effects |
None |
Proxies the request body verbatim to POST {FLOATME_METRICS_URL}/anon/track using AWS SigV4 signing.
Allows unauthenticated clients (pre-login) to emit analytics events without exposing AWS credentials.
Castle Integration
Castle (https://castle.io) provides real-time fraud detection.
The worker integrates with Castle’s v1 API via cloudflare/services/castle/castle.ts.
Fingerprint data sent to Castle
Every Castle request includes:
-
request_token— extracted from the client’sx-castle-request-tokenrequest header (provided by the Castle SDK running on device). If this header is absent, aCastleErroris thrown and the handler returns 400. -
context.ip— resolved fromCF-Connecting-IPheader (set by Cloudflare, not spoofable). -
context.headers— all request headers forwarded in Castle’s name-value-pair format. -
expand: ["all"]— requests full signal and policy expansion in the response.
Endpoints used
| Castle endpoint | Usage | Handlers |
|---|---|---|
|
Anonymous risk scoring (no user context required) |
login (pre-login), refresh-token (pre-refresh), signup (pre-signup), change-password (pre-change-password), validate-email (pre-email-check), login (on failure) |
|
Authenticated risk scoring (user identity known) |
login (post-login success), refresh-token (post-refresh success), signup (post-signup success), submit-mfa-oob-challenge (post-challenge success) |
|
Audit logging without blocking |
Not currently invoked from handlers directly (internal helper |
Policy outcomes
Castle returns a policy.action in its response.
The worker interprets this as follows:
| Action | Behavior |
|---|---|
|
Request proceeds normally; for login/refresh, |
|
Request proceeds; |
|
Handler returns |
When ENABLE_CASTLE_BLOCKING=false (test environment), deny responses from Castle are logged but the request is allowed through.
If Castle returns a non-2xx response or throws, a CastleError is raised.
When ENABLE_CASTLE_BLOCKING=true, a CastleError on a post-login/post-signup check also returns 401 { error: "unauthorized", message: "no security policy" } to fail closed.
Pre-flight Castle failures (filter calls) always return 400 regardless of the blocking flag.
QA / Integration Test Bypass
Source: cloudflare/helpers/qa-helper.ts
The handleQARequest helper is invoked only from the login handler and only when both conditions hold:
-
WORKER_ENV === "test"(the test Cloudflare environment) -
The email domain is
integrationtest.comorintegrationtest.floatme.io
When active, if the email contains a + character (e.g. user+CA@integrationtest.com), the helper:
-
Parses the
+statesegment out of the local part (e.g. extractsCAas the state). -
Derives a canonical email without the
+suffix (e.g.user@integrationtest.com). -
Calls
POST {FLOATME_QA_SERVICE_URL}/qa/integration/user(signed with AWS SigV4, always hardcoded tous-west-2) with{ email, password, state }. -
Returns the canonical email, which the login handler uses as the
usernamefor the Auth0 authentication call.
This allows integration tests to provision a test user in a specific account state on demand during the login flow without pre-creating fixture data.
The hardcoded us-west-2 region ensures QA provisioning can never target prod infrastructure.
The helper is a no-op (returns empty string) when WORKER_ENV !== "test" or when the email contains no +.
Local Development
Running the worker locally
make auth.local
This expands to:
wrangler dev cloudflare/workers/auth/index.ts --env test -c cloudflare/workers/auth/wrangler.toml
Wrangler starts a local miniflare instance using the test environment configuration from wrangler.toml.
Non-secret vars (Auth0 base URL, service URLs, region, Castle domain) are loaded from [env.test.vars].
Required secrets for local development
Secrets are not stored in wrangler.toml.
For local development, create a .dev.vars file (gitignored) in the worker directory with the following keys:
DATADOG_API_KEY=...
AUTH0_CLIENT_SECRET=...
AUTH0_CONNECTION=...
AUTH0_AUTHENTICATION_AUDIENCE=...
AUTH0_AUTHENTICATION_ISSUER=...
AUTH0_MFA_AUDIENCE=...
CASTLE_CLIENT_SECRET=...
FLOATME_AWS_ACCESS_KEY_ID=...
FLOATME_AWS_SECRET_ACCESS_KEY=...
AUTH0_MANAGEMENT_CLIENT_ID=...
AUTH0_MANAGEMENT_CLIENT_SECRET=...
AUTH0_MANAGEMENT_AUDIENCE=...
These correspond to the secrets uploaded to Cloudflare via cloudflare/workers/auth/secrets.sh for deployed environments.
Deploying secrets to Cloudflare
FLOATME_ENVIRONMENT=test bash cloudflare/workers/auth/secrets.sh
FLOATME_ENVIRONMENT=prod bash cloudflare/workers/auth/secrets.sh
The script reads each secret from the current shell environment and pipes it to wrangler secret put.
Environment differences
| Setting | prod | test |
|---|---|---|
Route |
|
|
Auth0 tenant |
|
|
User-service region |
|
|
Castle blocking |
enabled ( |
disabled ( |
QA bypass |
inactive |
active for |