Backend for Frontend (BFF) Authentication

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, tokens need to be stored in memory or local state, your code has to rotate expired tokens, and you need to understand 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:

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

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

This article does not adhere strictly to the proposed BFF specification. Instead, it aims to illustrate core concepts and essential elements. As the specification evolves and gains broader adoption, certain details may change.

Understanding Backend for Frontend (BFF) Authentication#

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

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

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 and server-side session storage.

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

/**
 * 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.

/**
 * 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.
  2. 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.

In order for the client-side app to send the cookie to the Backend API, it must include the cookies. By default the browser fetch API won't send cookies. So the credentials property needs to be set to same-origin.

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:

/**
 * 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",
    },
  });
}

Summary#

Backend for Frontend (BFF) Authentication is a security approach designed to optimize both user experience and security in web applications. Utilizing standard OAuth flows, BFF Authentication enables backend clients to authenticate users seamlessly, setting up session cookies to maintain secure and smooth interactions. This technique allows for a more efficient and user-centric way of managing user login and security requirements, without compromising on speed or user convenience. With BFF Authentication, developers have a powerful tool that reconciles the often conflicting demands of robust security and an uninterrupted user experience.

If you would like to run this entire sample, you can see the full source on Github or you can deploy the source to Zuplo.

ZupIt

Zuplo is a platform for API Management built for developers. You can instantly deploy this sample to Zuplo by clicking this link. Zuplo makes it easy to secure your API by adding authentication, rate limiting, and more in minutes.

Questions? Let's chatOPEN DISCORD
0members online

Designed for Developers, Made for the Edge