Skip to content

Login Challenges

A login challenge is what happens when the password was right but the sign-in still cannot finish. Foxnox mints a short-lived token bound to one specific next step, and the BFF answers the login request with 202 instead of a session.

Why 202

A login has more than two outcomes. "Wrong password" is a 401 and "here is your session" is a 200, but "correct password, now prove you hold the second factor" is neither — the credentials were accepted, yet no session exists yet.

Returning 202 Accepted with a URL lets the BFF represent that intermediate state explicitly. The BFF chooses the required step from the public pwd row, then asks Foxnox to mint a challenge of that kind.

The Three Kinds

KindToken typeWorkflow pageRaised when
expired-passwordExpired password challenge/password/expiredpwdExpiry is in the past
2fa2FA challenge/2fa/verifytwoFactorEnabled is true and no valid trusted-device cookie
trusted-deviceTrusted device challenge/trusted-devices/promptOffered after a successful 2FA check

Order matters. An expired password is checked before 2FA, because there is no point verifying a second factor for a credential the user is about to be forced to replace.

Login Flow

The sequence uses Gatelin as the example BFF.

The important detail is the last three steps. Completing a challenge does not create a session — Foxnox cannot, it does not issue tokens. Instead it hands the browser a login resume ticket, and the BFF trades that ticket for a session.

Mint a Challenge

Called by the BFF after a successful password check. Also useful directly for testing.

POST /foxnox/challenges
Content-Type: application/json

{
  "userId": 1,
  "kind": "2fa"
}

Foxnox does not inspect an Authorization header on this internal endpoint; it relies on network isolation. Gatelin's public proxy route is protected and requires the caller's authenticated session.

kind must be one of 2fa, expired-password, or trusted-device.

Response (201 Created):

json
{
  "kind": "2fa",
  "challenge": "9f2c1a…",
  "path": "/2fa/verify",
  "url": "http://localhost:8100/api/foxnox/web/2fa/verify?challenge=9f2c1a…",
  "expiresAt": "2026-08-19T18:10:00.000Z"
}
FieldDescription
kindThe challenge kind that was minted
challengeThe plaintext token — returned once, stored only as a hash
pathWorkflow page path, relative to the web base
urlAbsolute URL to redirect the browser to, built from WEB_PUBLIC_ORIGIN + WEB_PUBLIC_BASE
expiresAtWhen the challenge stops being valid

Redirect the browser to url; the query parameter is named challenge, not token, which is how the pages tell a mid-login step apart from an email link.

Verify a Trusted Device

POST /foxnox/devices/verify
Content-Type: application/json

{
  "userId": 1,
  "deviceToken": "<value of the trusted_device cookie>"
}

Response (200 OK): { "trusted": true } — see Trusted Devices.

Checking this before minting a 2FA challenge is what stops the service asking for a code on every single sign-in from the user's own laptop.

Redeem a Login Ticket

Called by the BFF when the frontend posts a resume ticket (Gatelin: POST /gatelin/sessions/resume).

POST /foxnox/login-tickets/redeem
Content-Type: application/json

{
  "ticket": "b71e4d…"
}

Response (200 OK):

json
{ "userId": 1 }

Response (400 Bad Request): { "error": "Missing ticket" } or { "error": "Invalid or expired ticket" }.

Tickets last 10 minutes and have a maximum of one attempt, so redeeming is genuinely one-shot: a second call with the same ticket fails, and a reloaded resume URL is correctly rejected rather than minting a second session.

Challenge Chaining

One login can require more than one step, and each completed page mints the next challenge rather than returning to the BFF. Verifying a 2FA code, for example, consumes the 2FA challenge and redirects to the trusted-device prompt with a fresh challenge attached.

Only the final page in the chain issues the login resume ticket. From the frontend's point of view none of this is visible: it redirects once on 202 and waits for the browser to come back with ?ticket=….

Published and maintained by ALTEN