First-Party Sessions (BFF)
Login with Foir hands your app short-lived access tokens. That’s ideal for mobile and server-to-server, but for a web app you often want your users to have a plain first-party session — a single HttpOnly cookie on your own domain, with no tokens touched by browser JavaScript at all.
You get that by running a thin Backend-for-Frontend (BFF): your own server sits between the browser and Foir, does the OAuth exchange server-side, and sets its own session cookie. Foir is the identity provider and API; your app owns the session.
This works today on top of the existing Login-with-Foir flow — no special Foir features required.
When to use it
- Your web app has a server (Next.js, Remix, Hydrogen/Oxygen, Rails, a Workers backend, …) — anything that can hold a secret and set a cookie.
- You want the browser to never see an access or refresh token (defends against token exfiltration via XSS).
- You want a normal
HttpOnly; Secure; SameSitesession cookie and server-side logout/revocation.
If your app is a pure static SPA with no backend, you can’t run a BFF — but you can get the same “no tokens in JS” result with Foir-hosted sessions below, where Foir sets the cookie on your domain instead of your server.
How it works
Browser (your-domain.com) Your BFF (your-domain.com) Foir
│ │ │
│ GET /login │ │
│─────────────────────────────────►│ customerLoginWithFoir │
│ │─────────────────────────────►│
│ 302 → Foir hosted login ◄───────┼──────────────────────────────│
│ …user authenticates (pwd/OTP/SSO) at Foir… │
│ 302 → /callback?code=&state= ───►│ customerLoginWithFoirCallback│
│ │─────────────────────────────►│
│ │ ◄ { token, refreshToken, customerId }
│ Set-Cookie: session=… │ (store tokens server-side, │
│ HttpOnly; Secure; SameSite=Lax ◄│ keyed by the session id) │
│ │ │
│ GET /api/... (cookie only) │ attach Bearer <token> │
│─────────────────────────────────►│─────────────────────────────►│ api.foir.devThe browser only ever holds an opaque, HttpOnly session cookie. Your BFF keeps the Foir access + refresh tokens server-side, refreshes them transparently, and attaches the bearer when it calls (or proxies to) the Foir public API.
Build it
-
Register a relying party for your app and complete the Login with Foir setup. Your BFF is the server that calls the two mutations.
-
Start login — on
GET /login, callcustomerLoginWithFoirand 302 the browser to the returnedredirectUrl. Keep the OAuthstateserver-side (in your session store) so the callback can verify it. -
Handle the callback — on your registered redirect URI, call
customerLoginWithFoirCallbackwith thecode. You get back{ token, refreshToken, customerId, email }. -
Create your session — generate an opaque session id, store
{ customerId, token, refreshToken, expiresAt }server-side keyed by it, and set the cookie:Set-Cookie: session=<opaque-id>; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=…Because the cookie is set by your own origin, it is first-party to your app — no third-party-cookie restrictions, no token in
localStorage. -
Authenticate requests — on each request, read the session cookie, look up the stored tokens, and call the Foir public API with
Authorization: Bearer <token>. When the 15-minute access token is near expiry, use the storedrefreshTokento rotate it (refresh tokens are single-use and rotate on every call — store the new one). -
Logout — delete the server-side session, clear the cookie, and optionally revoke the refresh token.
Why this is the secure default
- No tokens in the browser. Access and refresh tokens never leave your server, so XSS can’t exfiltrate them. The cookie is
HttpOnly, so script can’t read it either. - Server-side refresh + revocation. Rotation and reuse-detection happen behind your BFF; you can kill a session instantly server-side.
- Standard session semantics. Your app behaves like any normal first-party web app — the framework’s existing session/CSRF tooling applies.
Custom login domains (white-label)
By default the brief login redirect goes to auth.foir.dev. With custom login domains the redirect happens at login.your-domain.com instead — end-users never leave your brand, which removes the off-brand redirect that trains users to trust look-alike phishing pages.
For the BFF pattern above, that is purely a presentation upgrade — your server owns the session either way. For the backend-less pattern below it is load-bearing: it is what makes Foir’s session cookie first-party to your app.
Foir-hosted sessions (no backend)
If you have no server at all, Foir can own the session for you. It is the same trade as the BFF — a long-lived HttpOnly cookie on your domain, a short-lived token in memory — except Foir sets the cookie instead of your backend.
- Register a relying party with a custom login domain on the same registrable domain as your app (
app.acme.com+login.acme.com), and verify it. - On login, Foir sets
foir_customer_session—HttpOnly; Secure; SameSite=Lax; Domain=.acme.com— so the cookie reaches your app. - Your SPA calls
GET https://login.acme.com/session/tokenwithcredentials: 'include'. Foir validates the cookie and returns a 15-minute access token. - The SPA uses that bearer against
api.foir.dev, and silently re-exchanges the cookie when it nears expiry.
The cookie never touches the data plane, and the token never touches storage — so there is nothing for cross-site scripting to exfiltrate, exactly as with a BFF.
@foir/sdk does all of this for you; see the SPA quickstart. Note there is no refresh token in this flow: re-exchanging the cookie is the refresh, so there is nothing to rotate and nothing to store.
The custom login domain is required. A cookie can only be sent on a background request if it is first-party, so an SPA on a domain Foir does not share cannot hold a Foir session — the session endpoint refuses credentialed cross-site calls by design. If you can’t add a DNS record, use the Login with Foir token flow and keep the access token in memory.