Appearance
Platform integration checklist
Use this page as the implementation contract for any IESF platform BFF (the server-side part of your web application). Do not integrate with the identity service (identity-api) from the platform: you never see passwords, identity tokens or the identity login pages' internals.
{CENTRAL_BASE} below is the gateway origin, for example https://auth.iesf.gg. The machine-readable contract is {CENTRAL_BASE}/openapi.json (Swagger UI: {CENTRAL_BASE}/docs).
Prerequisites
- Central administrators register your platform via IESF Card Platform Admin (
/admin/platforms) orPOST /v1/platforms(platforms:admin). See Admin operations. - You receive a
clientIdandclientSecretonce (create, generate or rotate). Store them in a secret manager; never put them in browser storage or public config.- Every redirect pair has its own BFF key. Configure each BFF deployment with the key of the pair whose callback it serves: the key only redeems codes sent to that callback.
- A server-side API that only checks sessions (step 6b) uses the platform's backend key. It needs no registered URL and cannot exchange codes.
- After a rotation the previous secret works for 60 more minutes; deploy the new one within that window.
- Registration includes:
redirectPairs(required, at least one) — pairs of a return URL root (final redirect destination) and thecallbackUrlon your BFF that accepts?code=for logins returning there. Use one pair per deployment host, e.g.https://example.com/→https://example.com/api/auth/callbackidentityEnvironmentKey— the identity environment (issuer) users sign in through; defaults toPROD. Register a separate platform (with separate credentials) per environment, e.g.COMPETITIONS_DEVonDEVandCOMPETITIONSonPROD. The same e-mail address in DEV and PROD is two different users. Production environments only accept public HTTPS URLsdefaultOnboardingMode—none,platform, orcentralcentralOnboardingEnabled— must betrueif default mode iscentral
- Your platform is
active. - Onboarding fields for your
platformKeyare configured in Card Onboarding Admin (/admin/onboarding) when the platform needs a schema.
Checklist
1. Start authentication
Redirect the browser to Central:
http
GET {CENTRAL_BASE}/v1/auth/start?platformKey={YOUR_KEY}&returnTo={PATH_OR_URL}Rules:
platformKeymust match the registry exactlyreturnTomust fall under a registered return URL (absolute URL, or a root-relative path resolved against the default pair); its pair decides which callback receives the handoff- Optional
onboardingModemust match the registered default if supplied - Login and registration use the same URL; the identity login page offers both. Add
defaultScreen=upto open the registration page (/sign-up) instead of sign-in (defaultScreen=in, the default), e.g. for a "Create account" button - Do not send users to identity authorize URLs yourself
- Start is a browser navigation: if it fails (unregistered
returnTo, inactive platform, missing BFF key…), the user sees the gateway's JSON error. Try every return URL you use once after registration. The full error table is in Authentication flow - Recommended: set a short-lived HttpOnly "login pending" cookie on your own domain right before this redirect and require it in step 2 (login CSRF protection; the gateway does not pass a platform
statethrough)
2. Own the callback
Implement the callbackUrl of every pair you registered. Central redirects to the callback paired with the matched return URL:
http
GET {callbackUrl}?code=handoff_…Reject callbacks when:
codeis missingcodedoes not start withhandoff_- your "login pending" cookie from step 1 is missing (clear it once used)
The code expires after 60 seconds by default (AUTH_HANDOFF_TTL_SECONDS, at most 300) and works once: exchange it immediately. A user who cancels on the identity login page does not come back to your callback.
3. Exchange server-to-server
From the BFF only, with the BFF key of the pair whose callback received the code:
http
POST {CENTRAL_BASE}/v1/auth/exchange
Authorization: Basic {base64(clientId:clientSecret)}
Content-Type: application/json
Accept: application/json
{"code":"handoff_…"}Validate the JSON:
platform.keyequals your platform keyenvironment.keyis the identity environment this deployment expects (for example rejectDEVin production)session.gatewayTokenis presentsession.expiresAtis in the future
Responses: 200 exchange body, 400 malformed body, 401 wrong credentials, 403 a backend key was used, 404 the code is unknown, expired, already used or was sent to another callback (start the login again).
4. Create your local session
Build a platform-owned session from the exchange body. Suggested minimum:
| Field | Source |
|---|---|
| Local user id | user.id |
| Stable identity | user.issuer + user.sub (unique only as a pair) |
| Platform | platform.key |
| Onboarding snapshot | onboarding.* (and profile.*, always COMPLETE here) |
| Permissions | permissions |
| Gateway credential | session.gatewayToken + session.expiresAt (default lifetime 12 hours) |
| Post-login destination | returnTo |
Requirements:
- Persist
gatewayTokenonly server-side or in an HttpOnly cookie you control - Never put
gatewayToken, Basic credentials, or the handoff code into browser JavaScript, localStorage, or URLs after the callback - Redirect the browser to
returnToonly after the session exists (or to your onboarding UI — see below)
5. Onboarding branch
Before any platform onboarding, the gateway enforces the global profile requirements on every login, in every mode: a user with a missing required global field is sent to the identity UI first and reaches your callback only once it is filled. The exchange reports this as profile.status (COMPLETE for every handoff it issues). Expect the extra identity UI step even in none and platform mode.
Inspect onboarding.status after exchange:
| Mode | Incomplete behavior |
|---|---|
none | Proceed; status is informational |
platform | IN_PROGRESS (always with a non-empty missingFields): send the user to your onboarding UI; your BFF calls Central onboarding APIs with gatewayToken as the cookie (step 6) and finally POST /v1/onboarding/{platformKey}/complete |
central | Incomplete users never reach you: the identity UI collects the fields and the gateway hands off only when COMPLETE; if you somehow see incomplete, treat it as an error |
See Onboarding.
6. Calling Central user APIs with the gateway token
Your BFF calls Central user APIs server to server, sending the gatewayToken from the exchange as a cookie:
http
Cookie: iesf_auth_session={gatewayToken}Your browser code never calls Central directly and never sees gatewayToken. It calls your own BFF routes, and your BFF calls Central.
Rules:
- Cookie name. Use
iesf_auth_session. Keep it in configuration rather than hard-coded. - Encode the token. Send
encodeURIComponent(gatewayToken). - No Bearer. Do not send
gatewayTokenasAuthorization: Bearer. - Set
Originon writes.PUT/POST/PATCH/DELETEcalls must carryOriginset to the origin of one of your registered return URLs (e.g.Origin: https://example.com), otherwise Central answers403 FORBIDDEN. Server-sidefetchdoes not add it for you. Your return URL origins are accepted automatically once your platform is active. 401means the session is gone. The token expired, was revoked, or the user logged out: drop your local session and send the user through step 1 again.
Example (Node / Next.js route handler):
ts
async function central<T>(path: string, gatewayToken: string, init: RequestInit = {}): Promise<T> {
const headers = new Headers(init.headers);
headers.set('Accept', 'application/json');
headers.set('Cookie', `iesf_auth_session=${encodeURIComponent(gatewayToken)}`);
if (init.method && !['GET', 'HEAD'].includes(init.method)) headers.set('Origin', new URL(APP_BASE_URL).origin);
if (init.body) headers.set('Content-Type', 'application/json');
const response = await fetch(`${CENTRAL}${path}`, { ...init, headers, cache: 'no-store' });
if (response.status === 401) throw new SessionExpiredError();
if (!response.ok) throw new Error(`central_${response.status}`);
return response.json() as Promise<T>;
}
// await central('/v1/onboarding/MY_PLATFORM/requirements', session.gatewayToken);
// await central('/v1/onboarding/MY_PLATFORM/values', session.gatewayToken, { method: 'PUT', body: JSON.stringify({ values }) });Calls that use this header: /v1/users/me, /v1/profile/me*, /v1/account/me, /v1/onboarding/{platformKey}/* (platform onboarding mode) and /v1/auth/logout (step 7).
Admin and onboarding-schema APIs are not called with gatewayToken. A platform backend that needs them uses a machine client (see Identity operations).
GET /v1/account/me returns accountManagementUrl (the identity UI's /account page) — link users there for password, Google link and identity sessions; the gateway has no password endpoints.
6b. Checking the session from a platform backend (introspect)
A platform backend that receives the gatewayToken from its BFF checks it with the gateway. Do not call /v1/users/me with the cookie for this. Authenticate with the platform's backend key (or a BFF key). The call is server to server, so no URL of the backend has to be registered: CORS and Origin checks only apply to browsers and cookie-authenticated writes.
http
POST {CENTRAL_BASE}/v1/auth/introspect
Authorization: Basic base64(clientId:clientSecret)
Content-Type: application/json
{ "token": "{gatewayToken}" }- Active session:
200 { "active": true, "sessionId", "expiresAt", "platformKey", "user": { "id", "issuer", "subject", "email", "emailVerified", "status" }, "environment": { "key", "tier" } }. - Any unusable token:
200 { "active": false }. This covers unknown, expired, revoked and suspended-user tokens, and tokens created by another platform's login, because sessions are bound to the platform that started the login. 401means your platform credentials are wrong; it never means the user's session is wrong.5xxor a timeout means Central is unavailable: answer503and do not sign the user out.- Caching. Cache only an active result, for 30–60 s and never beyond
expiresAt, keyed bysha256(token). Do not cacheactive: falseor errors. Logout and admin session revocation then take effect after that delay. Full response example: API reference. - User key. Key your users by
(user.issuer, user.subject). The subject is unique only within its issuer.
7. Logout
Make logout a POST on your own BFF (never a GET link, which prefetching could trigger). In it:
- Drop your local session.
- Call Central from the BFF with the user's
gatewayTokenand your ownOrigin:
http
POST {CENTRAL_BASE}/v1/auth/logout
Cookie: iesf_auth_session={URL-encoded gatewayToken}
Origin: {your registered return URL origin}
Content-Type: application/json
{ "returnTo": "https://example.com/" }- Redirect the browser to
logoutUrlwhen it is notnull; after the identity logout the browser comes back toreturnTo(through{CENTRAL_BASE}/v1/auth/logged-out). WhenlogoutUrlisnull, or the call failed, redirect to your own signed-out page.
Notes:
returnTois optional and follows the startreturnTorules for your registered return URLs. Without it the user ends on the return URL of your default pair. Either way, only the return URLs registered for your platform under/admin/platformsare used; nothing has to be registered in the identity environment.returnTomay also be sent as a query parameter instead of the body, as with/v1/auth/start:POST {CENTRAL_BASE}/v1/auth/logout?returnTo=https%3A%2F%2Fexample.com%2F. When both are present the body wins.- The body may be omitted, empty or
{}. 403means a missing or unregisteredOrigin, or areturnTooutside your return URLs;400a malformedreturnToor body. The gateway session is revoked even then, so still drop your session and redirect.- Skipping
logoutUrlkeeps the identity session alive: the next login then completes without a password prompt. - If the identity session already ended elsewhere (expired, or the user logged out in another tab), the identity provider currently answers
logoutUrlwith an error page instead of redirecting back.
Details: Authentication flow.
8. Hard no’s
- Do not create an identity client for the platform
- Do not expect ID / access / refresh tokens from Central
- Do not reuse a handoff code
- Do not trust client-supplied completion flags
- Do not accept
returnToor callback URLs from the query string as overrides of the registry
Minimal BFF pseudocode
ts
// GET /api/auth/callback?code=handoff_…
async function handleCallback(code: string) {
if (!code.startsWith('handoff_')) throw new Error('invalid_callback');
const response = await fetch(`${CENTRAL}/v1/auth/exchange`, {
method: 'POST',
headers: {
Authorization: `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ code }),
});
if (!response.ok) throw new Error('handoff_exchange_failed');
const exchange = await response.json();
if (exchange.platform.key !== PLATFORM_KEY || exchange.environment.key !== ENVIRONMENT_KEY
|| !exchange.session.gatewayToken || Date.parse(exchange.session.expiresAt) <= Date.now()) {
throw new Error('invalid_handoff');
}
await savePlatformSession({
userId: exchange.user.id,
sub: exchange.user.sub,
issuer: exchange.user.issuer,
gatewayToken: exchange.session.gatewayToken,
expiresAt: exchange.session.expiresAt,
onboarding: exchange.onboarding,
permissions: exchange.permissions,
});
if (ONBOARDING_MODE === 'platform' && exchange.onboarding.status !== 'COMPLETE') {
return redirect(PLATFORM_ONBOARDING_PATH);
}
return redirect(exchange.returnTo);
}
// POST /api/auth/logout
async function handleLogout(session: PlatformSession | null) {
await deletePlatformSession();
if (!session) return redirect('/signed-out');
const response = await fetch(`${CENTRAL}/v1/auth/logout`, {
method: 'POST',
headers: {
Cookie: `iesf_auth_session=${encodeURIComponent(session.gatewayToken)}`,
Origin: new URL(APP_BASE_URL).origin,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ returnTo: `${APP_BASE_URL}/signed-out` }),
}).catch(() => null);
const body = response?.ok ? await response.json() : null;
return redirect(body?.logoutUrl ?? '/signed-out', 303);
}