---
title: "Backend for Frontend (BFF) Authentication"
description: "Learn about Backend for Frontend (BFF) authentication and how to implement it with a Javascript backend."
canonicalUrl: "https://zuplo.com/blog/2023/09/11/backend-for-frontend-authentication"
pageType: "blog"
date: "2023-09-11"
authors: "nate"
tags: "API Authentication, API Best Practices, Tutorial"
image: "https://zuplo.com/og?text=Backend%20for%20Frontend%20(BFF)%20Authentication"
---
If you've built or managed a web app, you've probably dealt with the headache
that is user login and security. You log the user in with
[OAuth](/learning-center/jwt-api-authentication), tokens need to be stored in
memory or local state, your code has to rotate expired tokens, and you need to
[understand](https://auth0.com/docs/troubleshoot/authentication-issues/renew-tokens-when-using-safari#workarounds)
how same-site cookies impact silent token refresh, etc., etc.

BFF Authentication offers a way to simplify your auth code while enhancing
security, and continuing to provide your end users with a nice login experience.
The big change with BFF authentication is that you no longer need to juggle all
of this authentication logic on the client - you authenticate the user, store
some session state on the server, and use good old browser cookies to handle API
authentication. When you have BFF authentication wired up correctly, making an
API request from your browser app is as simple as making a fetch:

```ts
await fetch("https://api.example.com", {
  credentials: "same-origin",
});
```

In this article, we'll walk through how BFF Authentication works and guide you
through the process of building a simple API to implement BFF Authentication in
a client application.

<CalloutAudience
  variant="useIf"
  items={[
    `Building web apps with OAuth login
flows`,
    `Want to simplify client-side authentication code`,
    `Need secure
session-based API authentication`,
    `Moving away from token juggling in the
browser`,
  ]}
/>

:::note

This article does not adhere strictly to the
[proposed BFF specification](https://www.ietf.org/archive/id/draft-bertocci-oauth2-tmi-bff-01.html#name-the-bff-token-endpoint).
Instead, it aims to illustrate core concepts and essential elements. As the
specification evolves and gains broader adoption, certain details may change.
BFF is one of several
[API gateway patterns](/learning-center/api-gateway-patterns) that can help you
manage traffic between frontends and backends.

:::

:::tip

Want to skip ahead? The
[complete BFF Authentication example](https://github.com/zuplo/zuplo/tree/main/examples/bff-auth)
is available on GitHub, or you can deploy it directly using the sample at the
bottom of this article.

:::

## How BFF Authentication Works

BFF Authentication employs standard OAuth flows to authenticate backend clients.
If you've worked with OAuth before in frameworks like ExpressJS or ASP.NET, this
process should be familiar. The system uses "web app" OAuth flows to
authenticate the backend, which then sets a session cookie passed along with
each subsequent browser request to the Backend API.

The diagram below shows the steps involved in the initial authentication.

![BFF Diagram](https://cdn.zuplo.com/assets/56b805d0-8f8d-4bb4-8acd-74a8f13093bd.svg)

1. The Browser App redirects to /auth/login on the Backend API.
1. The Backend API constructs an OIDC Authorize URL with the identity provider
   and redirects to /authorize.
1. The user authenticates through available methods (e.g., username/password,
   passkey, third-party OAuth provider).
1. Upon successful authentication, the identity provider redirects back to the
   Backend API, which then retrieves and validates access_token, id_token, and
   refresh_token.
1. The Backend API fetches the user's profile information via the /userinfo
   endpoint.
1. User profile, tokens, and session data are stored securely.
1. Finally, the Backend API sets a secure session cookie and redirects back to
   the Browser App.

:::info{title="Did you know?"}

If you've been developing long enough to remember the pre-React and pre-SPA
days, BFF Authentication may sound familiar. That's because it revisits
traditional methods involving
[session cookies](/learning-center/prevent-session-hijacking) and server-side
session storage.

:::

<CalloutDoc
  title="OpenID JWT Authentication"
  description={`Validate JWTs from any OpenID Connect compliant identity provider like Auth0, Okta, or Clerk.`}
  href="https://zuplo.com/docs/policies/open-id-jwt-auth-inbound"
  features={[
    `OIDC
discovery support`,
    `Automatic key rotation`,
    `Claims validation`,
  ]}
/>

## Examining BFF Authentication Code

Before diving into the code snippets, it's important to note that they have been
simplified for readability; error handling and other nuances have been omitted.
You can explore the
[complete project on GitHub](https://github.com/zuplo/zuplo/tree/main/examples/bff-auth)
for full details.

### Initializing OAuth Authentication

The first step initiates the authentication process by sending the user to
/auth/login. The backend then constructs an OAuth URL to interface with the
identity provider.

```ts title="/auth/login"
/**
 * Login the user by redirecting to the identity provider
 */
export async function login(request: Request) {
  const authUrl = new URL(AUTHORIZATION_URL);
  authUrl.searchParams.set("client_id", environment.CLIENT_ID);
  authUrl.searchParams.set("scope", SCOPE);
  authUrl.searchParams.set("response_type", "code");
  authUrl.searchParams.set(
    "redirect_uri",
    new URL("/auth/callback", request.url).toString(),
  );
  return Response.redirect(authUrl.toString(), 307);
}
```

### Completing the OAuth Cycle

After user authentication, identity provider redirects to the callback which
then retrieves tokens and profile information, saves these in a session store,
and sets a session cookie.

```ts title="/auth/callback"
/**
 * OAuth redirect URL
 */
export async function authCallback(request: Request) {
  const requestUrl = new URL(request.url);
  const code = requestUrl.searchParams.get("code");

  const data = {
    grant_type: "authorization_code",
    client_id: environment.CLIENT_ID,
    client_secret: environment.CLIENT_SECRET,
    code,
    redirect_uri: new URL("/auth/callback", request.url).toString(),
  };

  const tokenResponse = await fetch(TOKEN_URL, {
    method: "POST",
    headers: {
      authorization: `Bearer ${environment.CLIENT_SECRET}`,
      "content-type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams(data),
  });

  const userInfo = await fetch(USERINFO_URL, {
    headers: {
      authorization: `Bearer ${tokenResponse.access_token}`,
    },
  }).then((response) => response.json());

  const sessionId = crypto.randomUUID();
  const sessionInfo: SessionState = {
    ...tokenResponse,
    expires_on: Date.now() + tokenResponse.expires_in * 1000,
    info: userInfoResult,
  };

  await saveSession(sessionId, sessionInfo);

  const cookie = `${COOKIE_NAME}=${sessionId}; path=/; secure; HttpOnly; SameSite=Strict; Max-age=${tokenResponse.expires_in}`;

  return new Response("", {
    headers: {
      location: APP_URL,
      "set-cookie": cookie,
    },
    status: 307,
  });
}
```

## Client-Side Application Authentication

After the backend authentication the browser will load the client-side app,
which now needs to determine two things:

1. Whether the user is authenticated.
1. The identity of the user.

To acquire this information, the browser app calls the Backend API's session
info endpoint. This endpoint returns the session's profile information, allowing
the client-side app to confirm the login state and display relevant user
information.

Following
[API security best practices](/learning-center/api-security-best-practices), the
client-side app needs to send the cookie to the Backend API with each request.
By default the browser `fetch` API won't
[send cookies](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch).
So the `credentials` property needs to be set to `same-origin`.

```js
const profile = await fetch(`${BACKEND_API}/auth/session-info`, {
  credentials: "same-origin",
}).then((response) => response.json());
console.log(profile);

// Log output will look something like:
// {
//   "sub": "12356",
//   "name": "Nate Totten",
//   "email": "nate@example.com"
// }
```

The backend code for the session info is as follows:

```ts title="/auth/session-info"
/**
 * Get the session info from session storage
 *
 * See: https://www.ietf.org/archive/id/draft-bertocci-oauth2-tmi-bff-01.html#name-the-bff-sessioninfo-endpoin
 */
export async function bffSessionInfo(request: Request) {
  const sessionState = await getSessionState(request);
  if (!sessionState) {
    // If no session, the session has expired or is otherwise invalid
    return HttpProblems.unauthorized(request, context, {
      detail: err.message,
    });
  }

  // Send the user profile to the client
  return new Response(JSON.stringify(sessionState.info, null, 2), {
    headers: {
      "content-type": "application/json",
      "cache-control": "no-store",
    },
  });
}
```

## BFF vs. Token-in-Browser: Security Comparison

The most common alternative to BFF Authentication is storing tokens directly in
the browser — either in `localStorage`, `sessionStorage`, or in-memory. Here's
how the two approaches compare from a security perspective.

**Token-in-Browser** stores the access token (and sometimes the refresh token)
in client-side JavaScript. This creates several risks:

- **XSS vulnerability** — Any cross-site scripting attack can read tokens from
  storage and send them to an attacker. Once stolen, the attacker can make API
  calls as the user until the token expires.
- **Token refresh complexity** — The client must handle token expiration, silent
  renewal, and edge cases like expired refresh tokens. This logic is error-prone
  and varies across identity providers.
- **Third-party cookie restrictions** — Modern browsers are increasingly
  blocking third-party cookies, which breaks silent token refresh flows that
  rely on hidden iframes.

**BFF Authentication** avoids these problems by keeping tokens on the server:

- **No tokens in the browser** — The only credential the browser sees is a
  session cookie. Even if an XSS attack occurs, the attacker can't extract an
  OAuth token.
- **HttpOnly cookies** — The session cookie is set with the `HttpOnly` flag,
  which means JavaScript can't read it. Combined with `SameSite=Strict` and
  `Secure`, the cookie is well-protected against common web attacks.
- **Server-side refresh** — Token renewal happens on the backend, where it's
  simpler and more reliable. The client doesn't need to know about access tokens
  at all.

The trade-off is that BFF Authentication requires a backend component to manage
sessions. If you're already running a server (or using an API gateway like
Zuplo), that's not a significant cost. If you're building a purely static SPA
with no backend, a BFF adds infrastructure you wouldn't otherwise need.

## When NOT to Use BFF Authentication

BFF Authentication is a strong choice for many web applications, but it's not
the right fit for every scenario. Here are cases where a different approach
makes more sense:

- **Mobile apps and native clients** — Mobile apps typically use OAuth with PKCE
  and store tokens in the platform's secure storage (Keychain on iOS, Keystore
  on Android). These storage mechanisms are sandboxed per app, so the XSS risk
  that motivates BFF doesn't apply.
- **Machine-to-machine communication** — Server-to-server API calls use client
  credentials grants. There's no browser involved, so session cookies are
  irrelevant.
- **Static sites with no backend** — If your application is entirely client-side
  (e.g., a static site hosted on a CDN with no server), adding a backend purely
  for authentication adds complexity. In this case, consider using a hosted
  authentication service that handles token management for you.
- **Short-lived, low-risk applications** — For internal tools or prototypes
  where the security stakes are low, the overhead of setting up BFF
  Authentication may not be justified. A simpler token-in-browser approach with
  short-lived tokens could be sufficient.

If your application is a web app with a backend (or an API gateway) and you're
using OAuth to authenticate users, BFF Authentication is almost always the
better choice compared to managing tokens in the browser.

## Why BFF Authentication Simplifies Your Auth Code

BFF Authentication moves the complexity of OAuth token management from the
browser to the server. Instead of juggling access tokens, refresh logic, and
silent renewals in client-side code, you authenticate once on the backend and
use a session cookie for every subsequent request. The result is simpler
frontend code and stronger security — tokens never touch the browser.

If you're building a web app that uses OAuth and you're tired of debugging token
refresh flows in your SPA, BFF Authentication is worth considering. You get a
clean separation between your frontend and your auth logic, and your users still
get a smooth login experience.

For a deeper look at how different authentication methods compare, check out our
guide on the
[top API authentication methods](/learning-center/top-7-api-authentication-methods-compared).

<CalloutSample
  title="BFF Authentication Example"
  description="A complete Backend for Frontend authentication implementation with OAuth, session management, and secure HTTP-only cookies."
  deployUrl="https://zuplo.com/examples/bff-auth"
  repoUrl="https://github.com/zuplo/zuplo/tree/main/examples/bff-auth"
  localCommand="npx create-zuplo-api --example bff-auth"
/>