Skip to Content
GuidesFirst-Party Sessions (BFF)

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; SameSite session 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.dev

The 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

  1. Register a relying party for your app and complete the Login with Foir setup. Your BFF is the server that calls the two mutations.

  2. Start login — on GET /login, call customerLoginWithFoir and 302 the browser to the returned redirectUrl. Keep the OAuth state server-side (in your session store) so the callback can verify it.

  3. Handle the callback — on your registered redirect URI, call customerLoginWithFoirCallback with the code. You get back { token, refreshToken, customerId, email }.

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

  5. 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 stored refreshToken to rotate it (refresh tokens are single-use and rotate on every call — store the new one).

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

  1. 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.
  2. On login, Foir sets foir_customer_sessionHttpOnly; Secure; SameSite=Lax; Domain=.acme.com — so the cookie reaches your app.
  3. Your SPA calls GET https://login.acme.com/session/token with credentials: 'include'. Foir validates the cookie and returns a 15-minute access token.
  4. 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.

Last updated on