Implementing Backend-for-Frontend Authentication
The Backend-for-Frontend (BFF) pattern provides the highest level of security for web applications by ensuring authentication tokens never reach the user’s browser. In this pattern, your backend handles all OAuth2 and OpenID Connect flows with the Agglestone Authentication and User Management Service, while your frontend only communicates with your own backend using secure, HTTP-only session cookies.
Why Use Backend-for-Frontend?
Implementing the BFF pattern provides several critical security benefits:
- Token Security: JWTs and refresh tokens never leave your backend, eliminating client-side token exposure risks
- XSS Protection: Even if your frontend is compromised, attackers cannot access authentication tokens
- Centralized Control: All authentication logic and token management happens in one place
- Compliance: Meets security requirements that restrict sensitive data in browsers
- Session Management: Complete control over session lifecycle and security policies
Implementation Examples
Here are examples of how to implement BFF authentication in common backend frameworks. All examples use standard, well-maintained libraries that handle the OAuth2/OIDC complexity for you.
Recommended libraries and frameworks:
- C# / ASP.NET Core:
Microsoft.AspNetCore.Authentication.CookiesandMicrosoft.AspNetCore.Authentication.OpenIdConnect - Node.js / Express:
express-session,passport, andopenid-client - Python / FastAPI:
fastapiandauthlibfor OAuth/OIDC, withstarlettefor session management
Don’t have your Tenant ID yet? Log into your account at https://portal.agglestone.com to find your Tenant ID. Then replace {tenantId} or your-tenant-id in the examples below with your own Tenant ID.
// Add the configuration code below to your Program.cs file
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
var builder = WebApplication.CreateBuilder(args);
var tenantId = "your-tenant-id"; // Get from https://portal.agglestone.com
var issuer = $"https://auth.agglestone.com/tenant/{tenantId}/v2.0/Auth";
// Configure cookie authentication for session management
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.Name = "AggleStone-Auth";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromHours(1);
})
.AddOpenIdConnect(options =>
{
options.Authority = issuer;
options.ClientId = tenantId;
options.ResponseType = "code";
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.SaveTokens = true; // Tokens are stored in the authentication cookie (encrypted)
options.UsePkce = true;
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// Install dependencies:
// npm install express express-session passport passport-openidconnect
// Add this code to your main server file (e.g., app.js, server.js, index.js)
import express from 'express';
import session from 'express-session';
import passport from 'passport';
import { Strategy } from 'passport-openidconnect';
const app = express();
const tenantId = 'your-tenant-id';
const issuer = `https://auth.agglestone.com/tenant/${tenantId}/v2.0/Auth`;
// Configure session middleware with secure cookie settings
app.use(session({
secret: 'your-session-secret-change-in-production', // Use environment variable
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // Prevent JavaScript access
secure: true, // HTTPS only (set to false for local development)
sameSite: 'lax',
maxAge: 60 * 60 * 1000 // 1 hour
}
}));
// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());
// Configure OpenID Connect strategy
passport.use(new Strategy({
issuer: issuer,
authorizationURL: `${issuer}/authorize`,
tokenURL: `${issuer}/token`,
userInfoURL: `${issuer}/userinfo`,
clientID: tenantId,
clientSecret: '', // Not needed for public clients with PKCE
callbackURL: 'https://yourapp.com/auth/callback',
scope: 'openid profile email'
}, (issuer, sub, profile, accessToken, refreshToken, done) => {
// Store tokens in session (they never go to the browser)
return done(null, {
id: sub,
profile: profile,
accessToken: accessToken,
refreshToken: refreshToken
});
}));
// Serialize user for session
passport.serializeUser((user, done) => {
// Store the full user object (including tokens) in the session
done(null, user);
});
passport.deserializeUser((user, done) => {
// Retrieve user from session (includes tokens stored during authentication)
// In production with a session store, you'd fetch the full user object
done(null, user);
});
# Install: fastapi, uvicorn, authlib
# Add this code to your main FastAPI app file (e.g., main.py, app.py)
from fastapi import FastAPI, Request, Depends, HTTPException, status
from fastapi.responses import RedirectResponse
from fastapi.security import HTTPBearer
from authlib.integrations.starlette_client import OAuth
from starlette.middleware.sessions import SessionMiddleware
import secrets
app = FastAPI()
TENANT_ID = "your-tenant-id"
ISSUER_URL = f"https://auth.agglestone.com/tenant/{TENANT_ID}/v2.0/Auth"
CALLBACK_URL = "https://yourapp.com/auth/callback"
# Configure session middleware with secure cookie settings
app.add_middleware(
SessionMiddleware,
secret_key=secrets.token_urlsafe(32), # Use environment variable in production
max_age=3600, # 1 hour
same_site='lax',
https_only=True # Set to False for local development
)
# Configure OAuth client
oauth = OAuth()
oauth.register(
name='agglestone',
client_id=TENANT_ID,
client_secret='', # Not needed for public clients with PKCE
server_metadata_url=f'{ISSUER_URL}/.well-known/openid-configuration',
client_kwargs={
'scope': 'openid profile email'
}
)
Callback Handling and Configuration
After a user authenticates with the Agglestone Authentication and User Management Service, they are redirected back to your backend with an authorization code. Your backend must handle this callback to exchange the code for tokens. Here’s how the callback is handled in each framework and what you need to configure:
// Add these endpoint handlers to your Program.cs file after app.UseAuthorization()
// Login endpoint - redirects to OIDC provider
// After callback, user will be redirected to /dashboard (or ReturnUrl if specified)
app.MapGet("/login", async (HttpContext context, string? returnUrl = null) =>
{
var properties = new AuthenticationProperties
{
RedirectUri = returnUrl ?? "/dashboard" // Where to redirect after successful login
};
await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, properties);
});
// Logout endpoint
app.MapPost("/logout", async (HttpContext context) =>
{
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
return Results.Redirect("/");
});
// Protected endpoint example
app.MapGet("/api/protected", async (HttpContext context) =>
{
var userId = context.User.FindFirst("sub")?.Value;
var groups = context.User.FindAll("groups").Select(c => c.Value).ToList();
// Access stored tokens if needed for API calls
var accessToken = await context.GetTokenAsync("access_token");
return Results.Ok(new { userId, groups });
}).RequireAuthorization();
// Add these endpoint handlers to your server file after passport configuration
// Login endpoint - redirects to OIDC provider
app.get('/login', passport.authenticate('openidconnect'));
// Callback endpoint
app.get('/auth/callback',
passport.authenticate('openidconnect', { failureRedirect: '/login' }),
(req, res) => {
// Successful authentication - tokens are stored in session
res.redirect('/');
}
);
// Logout endpoint
app.post('/logout', (req, res) => {
req.logout((err) => {
if (err) return res.status(500).send('Logout failed');
req.session.destroy();
res.redirect('/');
});
});
// Protected endpoint example
app.get('/api/protected', (req, res) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Access tokens are in req.user (set during passport strategy callback)
// Use tokens to make authenticated API calls
const accessToken = req.user.accessToken;
const userId = req.user.id;
// Groups would be in req.user.profile if included in token
res.json({ userId });
});
# Add these endpoint handlers to your FastAPI app file after OAuth configuration
# Login endpoint - redirects to OIDC provider
@app.get("/login")
async def login(request: Request):
redirect_uri = CALLBACK_URL
return await oauth.agglestone.authorize_redirect(request, redirect_uri)
# Callback endpoint
@app.get("/auth/callback")
async def callback(request: Request):
try:
token = await oauth.agglestone.authorize_access_token(request)
# Store token in session (never sent to browser)
request.session['access_token'] = token['access_token']
request.session['user_id'] = token['userinfo']['sub']
return RedirectResponse(url="/")
except Exception as e:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))
# Logout endpoint
@app.post("/logout")
async def logout(request: Request):
request.session.clear()
return RedirectResponse(url="/")
# Protected endpoint example
@app.get("/api/protected")
async def protected_route(request: Request):
if 'access_token' not in request.session:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
# Access token is in session but never sent to frontend
user_id = request.session.get('user_id')
return {"userId": user_id}
C# / ASP.NET Core
Automatic Callback Handling: The OpenIdConnect middleware automatically handles the callback at /signin-oidc. You don’t need to create a callback endpoint manually – the middleware processes the authorization code, exchanges it for tokens, and creates the authentication cookie automatically.
Token Storage: With SaveTokens = true, tokens are stored in the authentication cookie (encrypted as part of the authentication ticket). While this is encrypted and secure, for maximum security in a BFF pattern, you may prefer to store tokens server-side in a session store or database and only store a session identifier in the cookie. To do this, you can set SaveTokens = false and manually store tokens in your session store during the OnTokenValidated event.
JavaScript / Node.js (Express)
Manual Callback Handling: You must create a callback endpoint that processes the authorization code. The Passport middleware handles the token exchange, but you control the callback endpoint.
Token Storage: Tokens are stored server-side in the session store (configured via express-session). The cookie only contains a session identifier, not the actual tokens. The tokens are stored in the session object and retrieved via req.user after deserialization. This is the recommended approach for BFF as tokens never leave the server.
Python / FastAPI
Manual Callback Handling: You must create a callback endpoint that processes the authorization code and exchanges it for tokens.
Token Storage: Tokens are stored server-side in the session (configured via SessionMiddleware). The cookie only contains a session identifier, not the actual tokens. The tokens are stored in request.session dictionary and retrieved from there. This is the recommended approach for BFF as tokens never leave the server.
Common Callback Configuration Requirements
Regardless of which framework you use, you need to ensure:
- Callback URL is Accessible: The callback endpoint must be publicly accessible (not behind authentication). The Agglestone Authentication and User Management Service needs to redirect users to this endpoint.
- HTTPS in Production: Callback URLs must use HTTPS in production. The authentication service will only redirect to HTTPS URLs.
- URL Matching: The callback URL in your configuration must exactly match the URL where your callback endpoint is hosted. Mismatched URLs will cause authentication to fail.
- Error Handling: Implement proper error handling in your callback endpoint. If token exchange fails, redirect users to a login page or show an appropriate error message.
How Backend-for-Frontend Authentication Works
The BFF pattern implements the OAuth2 authorization code flow with PKCE entirely on the backend. Here’s how it works:
1. User Initiates Login
When a user clicks login in your frontend, the frontend makes a request to your backend (e.g., GET /login). The backend then initiates the OAuth2 flow by redirecting the user to the Agglestone Authentication and User Management Service authorization endpoint.
2. User Authenticates
The user is redirected to the authentication service, where they enter their credentials and complete any required MFA. The authentication service then redirects back to your backend’s callback endpoint with an authorization code.
3. Backend Exchanges Code for Tokens
Your backend receives the authorization code at the callback endpoint and exchanges it for access and refresh tokens. This exchange happens server-to-server, so tokens never pass through the user’s browser.
4. Session Creation
After successfully obtaining tokens, your backend creates a session and stores the tokens server-side (in memory, a database, or a distributed cache). The backend then sets a secure, HTTP-only session cookie in the user’s browser.
5. Subsequent Requests
When the frontend makes API requests, it includes the session cookie. Your backend:
- Validates the session cookie
- Retrieves the stored tokens from the session
- Uses the tokens to make authenticated calls to Agglestone services or your own APIs
- Returns the data to the frontend
6. Token Refresh
When access tokens expire, your backend automatically uses the refresh token to obtain new access tokens. This happens transparently without the user needing to log in again, and the refresh token never leaves your backend.
Cookie Configuration
For BFF to work securely, session cookies must be properly configured:
- HttpOnly: Set to
trueto prevent JavaScript from accessing the cookie - Secure: Set to
true(orCookieSecurePolicy.Alwaysin C#) to ensure cookies are only sent over HTTPS - SameSite: Set to
LaxorStrictto protect against CSRF attacks - Same Domain: Your backend must be on the same domain as your frontend for cookies to work
- ExpireTimeSpan: Sets the lifetime of the authentication cookie. With
SlidingExpirationenabled, the cookie expiration is automatically extended on each request (as long as more than half the expiration time has passed), so active users stay logged in. If a user is inactive for longer than theExpireTimeSpan, the cookie expires and they’ll need to log in again.
Proxying API Calls
Since tokens never leave your backend, your frontend cannot directly call Agglestone APIs or your own APIs that require JWT authentication. Instead, you must proxy these calls through your backend:
- Frontend makes a request to your backend (e.g.,
GET /api/users) - Your backend retrieves the access token from the session
- Your backend makes an authenticated call to the Agglestone API (e.g.,
GET https://auth.agglestone.com/tenant/{tenantId}/v2.0/UserswithAuthorization: Bearer {token}) - Your backend returns the data to your frontend
This proxying pattern ensures tokens remain secure while still allowing your frontend to access the data it needs.
Important Security Considerations
When implementing BFF authentication, these security best practices are essential:
⚠️ Use HTTPS in production – Cookies must be sent over encrypted connections only
⚠️ Configure cookies correctly – Always set HttpOnly and Secure flags on session cookies
⚠️ Store tokens securely – Keep tokens in secure server-side storage (not in cookies or client-accessible locations)
⚠️ Use strong session secrets – Generate cryptographically secure random strings for session secrets
⚠️ Implement session expiration – Set appropriate session timeouts and implement sliding expiration
⚠️ Protect against CSRF – Use SameSite cookie attributes and CSRF tokens for state-changing operations
⚠️ Keep libraries updated – Use well-maintained authentication libraries and keep them updated
The standard authentication libraries used in the examples above handle most of these security concerns when configured correctly.
> 📚 API Documentation: For detailed API documentation, request/response schemas, and to try out the endpoints interactively, visit the Swagger UI.
—
Want to learn more about the BFF pattern? Check out the Backend-for-Frontend Pattern documentation for a detailed explanation of the architecture and benefits.