Auth Worker

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:

  1. 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.

  2. Scope and realm injection — Every Auth0 request is stamped with the correct scope (openid email profile offline_access), audience, realm/connection, and grant_type. Clients do not need to know these values.

  3. 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.

Table 1. Route dispatch table
Path Method Handler module

/oauth/token

POST

login.ts

/oauth/refresh

POST

refresh-token.ts

/signup

POST

signup.ts

/change-password

POST

change-password.ts

/email/verify

POST

verify-email.ts

/email/validate

POST

validate-email.ts

/layer/session

GET

create-session-token.ts

/layer/session/account

POST

get-session-account.ts

/userinfo

GET

get-user-info.ts

/mfa/token

POST

get-mfa-token.ts

/mfa/first-enroll

POST

enroll-first-mfa-authenticator.ts

/mfa/associate

POST

enroll-first-mfa-authenticator.ts (alias)

/mfa/authenticators

GET

get-mfa-authenticators.ts

/mfa/challenge

POST

challenge-mfa-oob-authenticator.ts

/mfa/confirm

POST

submit-mfa-oob-challenge.ts

/social/signin

GET

social-signin.ts

/social/complete

GET

social-callback.ts

/anon/track

POST

floatmetric-track.ts

/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

POST /oauth/token

Auth0 endpoint

POST /oauth/token — grant type password-realm

Castle calls

Pre-login filter → post-login risk (on success) or failed-login filter (on failure)

user-service

POST /users-by-email — only if Auth0 returns user is blocked (PAUSED account unblock check)

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

POST /oauth/refresh

Auth0 endpoint

POST /oauth/token — grant type refresh_token

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

POST /signup

Auth0 endpoint

POST /dbconnections/signup

Castle calls

Pre-signup filter → post-signup risk (on success)

user-service

None

Side effects

verify_email: false is always set — verification email is sent separately via /email/verify; response is normalized to { id, email } only

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

POST /change-password

Auth0 endpoint

POST /dbconnections/change_password

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

POST /email/verify

Auth0 endpoint

POST /api/v2/jobs/verification-email (Management API)

Castle calls

None

user-service

None

Side effects

Auth0 triggers a verification email; requires a valid Bearer token in the Authorization header; token is validated via JWK before calling the Management API

The JWT sub claim is split on | to derive provider and user_id for the Management API payload.

validate-email

Field Detail

Path / Method

POST /email/validate

Auth0 endpoint

None

Castle calls

Pre-signup email-validation filter (custom event check-email-in-use)

user-service

POST /users-by-email

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

GET /layer/session

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

POST /layer/session/account

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

GET /userinfo

Auth0 endpoint

GET /userinfo

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

POST /mfa/token

Auth0 endpoint

POST /oauth/token — grant type password-realm, audience AUTH0_MFA_AUDIENCE

Castle calls

None

user-service

None

Side effects

Returns a short-lived MFA token scoped to enroll read:authenticators only

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

POST /mfa/first-enroll (also aliased to POST /mfa/associate)

Auth0 endpoint

POST /mfa/associate

Castle calls

None

user-service

GET /user/phone?email=…​ — only when oob_channel=sms

Side effects

Auth0 creates the OOB authenticator and sends an SMS or email OOB code; response normalizes to { oob_code, masked_phone_number } or { oob_code, masked_email }

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

POST /mfa/associate

Castle calls

None

user-service

None

Side effects

Enrolls an additional authenticator using an explicit phone_number supplied by the client; returns { oob_code }

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

GET /mfa/authenticators

Auth0 endpoint

GET /mfa/authenticators

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

POST /mfa/challenge

Auth0 endpoint

POST /mfa/challenge

Castle calls

None

user-service

None

Side effects

Auth0 triggers an OOB code delivery (SMS or email) to the enrolled authenticator; response is { oob_code }

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

POST /mfa/confirm

Auth0 endpoint

POST /oauth/token — grant type mfa-oob

Castle calls

Post-challenge risk

user-service

None

Side effects

Exchanges OOB code for a full token set; Castle event type $challenge / $succeeded / $other / app-mfa-login; if refresh_token is absent in Auth0 response it is replaced with a placeholder value to prevent downstream null errors — this is a known bug and should be surfaced as a hard failure instead

Castle blocking applies: if blocked, returns { error: "unauthorized", message: "blocked by security policy" }.

social-signin

Field Detail

Path / Method

GET /social/signin

Auth0 endpoint

GET /authorize (redirect, not a direct API call)

Castle calls

None

user-service

None

Side effects

302 redirect to Auth0’s /authorize with connection=google-oauth2, full scope, and audience; redirect_uri points back to /social/complete

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

GET /social/complete

Auth0 endpoint

POST /oauth/token — grant type authorization_code

Castle calls

None

user-service

POST /users-by-email — to determine if account already exists

Side effects

302 redirect to floatme://links.floatme.io/social?access_token=…​&refresh_token=…​&id_token=…​&signup=true|false

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

POST /anon/track

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’s x-castle-request-token request header (provided by the Castle SDK running on device). If this header is absent, a CastleError is thrown and the handler returns 400.

  • context.ip — resolved from CF-Connecting-IP header (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

POST /v1/filter

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)

POST /v1/risk

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)

POST /v1/log

Audit logging without blocking

Not currently invoked from handlers directly (internal helper handleLoginLog exists but is not called from the public handler paths)

Policy outcomes

Castle returns a policy.action in its response. The worker interprets this as follows:

Action Behavior

allow

Request proceeds normally; for login/refresh, challengeRequired is set to false in the Auth0 payload (MFA step may be skipped)

challenge

Request proceeds; challengeRequired is set to true — Auth0 will enforce an MFA step

deny

Handler returns 401 { error: "unauthorized", message: "blocked by security policy" } immediately, only if ENABLE_CASTLE_BLOCKING=true (always true in prod, false in test env)

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:

  1. WORKER_ENV === "test" (the test Cloudflare environment)

  2. The email domain is integrationtest.com or integrationtest.floatme.io

When active, if the email contains a + character (e.g. user+CA@integrationtest.com), the helper:

  1. Parses the +state segment out of the local part (e.g. extracts CA as the state).

  2. Derives a canonical email without the + suffix (e.g. user@integrationtest.com).

  3. Calls POST {FLOATME_QA_SERVICE_URL}/qa/integration/user (signed with AWS SigV4, always hardcoded to us-west-2) with { email, password, state }.

  4. Returns the canonical email, which the login handler uses as the username for 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

auth.floatme.io/*

auth.test.floatme.io/*

Auth0 tenant

floatme.auth0.com

floatme-test.us.auth0.com

User-service region

us-east-2

us-west-2

Castle blocking

enabled (ENABLE_CASTLE_BLOCKING=true)

disabled (ENABLE_CASTLE_BLOCKING=false)

QA bypass

inactive

active for integrationtest.com / integrationtest.floatme.io emails