Skip to content

Frontend Integration ​

Most of Foxnox needs nothing from your frontend — the workflow pages are served by Foxnox itself. There is one exception, and it is the one that breaks logins if you skip it: handling the 202 challenge response.

The One Thing You Must Handle ​

A login can return 202 Accepted. The password was correct, but a mid-login step is required before a session exists. Code that treats any non-200 as a failure will leave every 2FA user unable to sign in.

The example uses Gatelin's session endpoint; a custom BFF should return the same 202 body with a url.

typescript
const response = await fetch('/api/gatelin/sessions', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, pwd })
});

if (response.status === 202) {
  // Password OK, but a challenge is required (2FA / expired password).
  const { url } = await response.json();
  window.location.assign(url);
  return;
}

if (!response.ok) {
  // 401 wrong password, 403 locked (Foxnox refused compare while lockedUntil is in the future), 404 unknown user
  throw new Error('Login failed');
}

const { accessToken, refreshToken } = await response.json();

credentials: 'include' matters here: it is what sends the trusted_device cookie, and without it every login from a remembered browser is challenged again.

Resuming After a Challenge ​

Once the user has completed the workflow pages, Foxnox redirects the browser to WEB_LOGIN_RESUME_URL with ?ticket=…. Your login page has to notice that parameter and redeem it before rendering a form:

typescript
const ticket = new URLSearchParams(window.location.search).get('ticket');

if (ticket) {
  const response = await fetch('/api/gatelin/sessions/resume', {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ticket })
  });

  if (!response.ok) throw new Error('Resume failed');

  const { accessToken, refreshToken } = await response.json();

  // Drop ?ticket= from the URL so a reload cannot retry a spent ticket
  history.replaceState({}, '', window.location.pathname);
  return;
}

Tickets are valid for 10 minutes and can be redeemed exactly once. Clearing the query string is not cosmetic — a reload would otherwise post a spent ticket and fail.

Handling 403 Lockout ​

A 403 means the account is locked after repeated failures. Users have no way to guess what to do next, so show the unlock workflow:

typescript
if (response.status === 403) {
  showMessage('Account temporarily locked.');
  showLink('Request an unlock', '/api/foxnox/web/unlock');
  return;
}

These are plain links — full page navigations to Foxnox, not fetch calls:

WhereLink
Login page/api/foxnox/web/recover — forgotten password
Login page/api/foxnox/web/unlock — locked account
Account settings/api/foxnox/web/2fa/setup — enable 2FA
Account settings/api/foxnox/web/security-questions — enroll recovery questions
Account settings/api/foxnox/web/trusted-devices — review remembered devices

Do not try to fetch these and render the HTML yourself. They set cookies, enforce CSRF, and redirect between states; they only work as real navigations.

Pass ?lang=fr to force a language, otherwise the Accept-Language header decides.

Cookies ​

Foxnox sets two cookies, and both are HttpOnly — your JavaScript cannot and does not need to read them.

CookiePathPurpose
trusted_device/Lets the BFF skip 2FA on a remembered browser. Scoped to the root so it reaches the login endpoint, not just the workflow pages.
foxnox_csrf/foxnox/webDouble-submit CSRF protection for workflow forms. Handled entirely by the pages.

Both gain the Secure flag when COOKIE_SECURE=1 or NODE_ENV=production.

Calling the Admin API ​

The CRUD endpoints are ordinary authenticated JSON calls through the BFF:

typescript
const response = await fetch('/api/foxnox/pwd/search', {
  method: 'POST',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${accessToken}`
  },
  body: JSON.stringify({
    pagination: true,
    first: 0,
    limit: 10,
    filters: { userId: { value: 1, matchMode: '=' } }
  })
});

Every route is restricted to the Super admin and Admin roles, so these are admin-tool calls rather than something to expose to end users.

If you are building admin screens, GET /api/foxnox/pwd/schema returns the field definitions — types, limits, and which operations each field participates in — which lets forms stay in sync with the backend instead of duplicating its rules.

What Not to Build ​

It is worth being explicit about the screens you should not write, because Foxnox already owns them and building parallel versions means duplicating password policy and token validation:

  • Password reset forms
  • 2FA code entry and QR enrollment
  • Security question enrollment
  • Trusted device lists
  • Forced password change on expiry

Link to the workflow pages instead. The one piece of login logic that genuinely belongs in your frontend is the 202-and-ticket handling above.

Published and maintained by ALTEN