Using a Backend for Frontend (BFF)
~15 minA Backend for Frontend (BFF) is a small server that belongs to your app. It signs users in and keeps the tokens, so the refresh token never reaches the browser, and it receives back-channel logout. Use this setup when you want to keep your React app but need single logout or safer token storage.
How it works
The browser talks only to the BFF, through an HttpOnly session cookie. The BFF holds the tokens and the client secret, adds the access token to API calls, and receives the back-channel logout ThunderID sends when the user's session ends elsewhere.
Browser
React app
Holds an
HttpOnly session cookie. No refresh or ID token./api/auth/* + cookie
session, API responses
/api/auth/* + cookie
session, API responses
Your server
BFF (Express)
Holds the access, refresh, and ID tokens and the client secret. Adds the access token to API calls.
sign-in, tokens, back-channel logout
Identity provider
ThunderID
Tells the BFF, server to server, when the user's session ends.
Compared with React only
Set up the BFF
Register a confidential application
In the Console, create an application with a client secret. Open it, go to Advanced Settings → OAuth2 Configuration, and set these URIs. The redirect URIs use the React app's origin. The back-channel logout URI points at the BFF itself, because ThunderID calls it directly, server to server.Authorized redirect URI http://localhost:5173/api/auth/callbackPost-logout redirect URI http://localhost:5173Back-channel logout URI http://localhost:3001/api/auth/backchannel-logout
Install the Express SDK
npm install express cookie-parser @thunderid/express
Add the environment variables
Keep the client secret on the server. APP_URL is the origin the browser opens the React app on.
THUNDERID_BASE_URL=https://localhost:8090THUNDERID_CLIENT_ID=<your-client-id>THUNDERID_CLIENT_SECRET=<your-client-secret>APP_URL=http://localhost:5173
Mount the SDK and receive back-channel logout
Mount thunderID() under /api/auth and add the back-channel logout route. The SDK stores the tokens on the server and sets the HttpOnly session cookie. Start the server with node --env-file=.env index.mjs.
import express from 'express'import cookieParser from 'cookie-parser'import { thunderID, handleBackchannelLogout } from '@thunderid/express'const appUrl = process.env.APP_URLconst app = express()app.use(cookieParser())const auth = express.Router()app.use('/api/auth', auth)auth.use(thunderID({baseUrl: process.env.THUNDERID_BASE_URL,clientId: process.env.THUNDERID_CLIENT_ID,clientSecret: process.env.THUNDERID_CLIENT_SECRET,afterSignInUrl: `${appUrl}/api/auth/callback`,afterSignOutUrl: appUrl,sessionCookie: { httpOnly: true, sameSite: 'lax' },}),)// ThunderID posts a logout token here when the user's session ends elsewhere.auth.post('/backchannel-logout', handleBackchannelLogout())// Add the endpoints the React SDK calls. See "Endpoints the React SDK calls".app.listen(3001)
Connect the React app
Serve the BFF on the app's origin
Forward /api/auth to the BFF, so the session cookie is first party and you need no CORS setup. In production, serve the React build and the BFF from the same origin, for example behind one reverse proxy.
import { defineConfig } from 'vite'import react from '@vitejs/plugin-react'export default defineConfig({plugins: [react()],server: {proxy: {'/api/auth': { target: 'http://localhost:3001' },},},})
Switch the provider to BFF mode
Set endpoints.backend. The app needs no client ID or base URL, because every endpoint defaults to a path under /api/auth on the app's origin. session.checkInterval sets how often, in seconds, the SDK checks that the session is still live.
<ThunderIDProviderendpoints={{ backend: {} }}session={{ checkInterval: 30 }}><App /></ThunderIDProvider>
Hooks and components work as before. Calls through the SDK's http client go to /api/auth/proxy/*, and the BFF adds the access token.
Endpoints the React SDK calls
The BFF serves these under /api/auth. The SDK sends the header X-Requested-With: XMLHttpRequest on every request it makes with fetch. Reject requests without it, except the sign-in redirects, so a page on another site cannot use the session cookie.
Try single logout
Allow a local back-channel logout URI
ThunderID rejects localhost and private network addresses as back-channel logout URIs by default. For local testing only, allow them in deployment.yaml and restart the server.
oauth:logout:backchannel:reject_private_addresses: false
Add single sign-on to the sign-in flow
The flow needs the Check SSO Session and Save / Load Session nodes. Without them ThunderID records no session, so it sends no logout.
Sign out from another app
Sign in to this app and to a second app that uses the same ThunderID, in the same browser. Sign out of the second app. Within 30 seconds, or as soon as you switch back to this app's tab, it signs you out too.