SPAs, mobile apps and the BFF pattern

OAuth, OIDC & JWT · Practical · 15 min

Most token theft happens in browsers; the client architecture decides how exposed tokens are.

Confidential clients (server-side apps) can keep a secret. Public clients (SPAs, mobile and desktop apps) cannot: anything shipped to a device can be extracted. Both use the authorization code flow with PKCE, but token handling differs.

Native apps (RFC 8252): open the login page in the system browser or an in-app browser tab (ASWebAuthenticationSession, Custom Tabs), never an embedded WebView that could read the password. Receive the redirect via claimed HTTPS links (Universal/App Links) or a private-use URI scheme, and store refresh tokens in the Keychain or Keystore.

Single-page apps are the hardest case. Any token readable by JavaScript can be stolen by an XSS bug or a compromised dependency. Options, from most to least secure:

  • Backend for frontend (BFF): a small server-side component performs the OAuth flow as a confidential client, keeps tokens server-side, and gives the browser an HttpOnly, Secure, SameSite session cookie. API calls go through the BFF. This is the current recommendation for sensitive apps.
  • Token-mediating backend: the backend gets tokens and hands short-lived access tokens to the SPA.
  • Browser-only: tokens in memory (not localStorage), refresh token rotation, strict CSP, and DPoP if available.

The implicit grant (tokens in the URL fragment) is deprecated; do not use it.

Example

Browser                     BFF (same site)                 Auth server / API
-------                     ---------------                 -----------------
GET /login        ------->  build authorize URL + PKCE
                  <-------  302 to auth server
login + consent   ---------------------------------------->  /authorize
callback ?code=   ------->  POST /token (client secret +      /token
                            code_verifier)          ------->
                  <-------  Set-Cookie: sid=...; HttpOnly;
                            Secure; SameSite=Strict
fetch /api/orders ------->  look up tokens for sid,
  (cookie only)             add Authorization: Bearer ---->  API
                  <-------  JSON                      <----

Tokens never reach JavaScript in the browser.

Remember

  • Public clients cannot keep secrets; use code flow + PKCE
  • Native apps: system browser, never embedded WebViews
  • SPAs: a BFF keeps tokens server-side behind an HttpOnly cookie
  • The implicit grant is deprecated

Watch out for

  • Storing refresh tokens in localStorage, where any XSS can read them.

Check yourself

  1. Why is a backend-for-frontend safer for an SPA than storing tokens in the browser?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.rfc-editor.org/rfc/rfc8252