How to Enable BFF Authentication¶
This guide covers enabling the Backend-for-Frontend (BFF) authentication pattern in SyRF, with step-by-step instructions for both Auth0 and OpenIddict as the identity provider.
Background¶
The BFF pattern moves all OIDC handling to the server side. Angular uses cookies and /api/auth/* endpoints — it has no client-side auth library (no Auth0 SDK, no OIDC client). The API service acts as a confidential OIDC client, handling authorization code flow with PKCE, token exchange, and session management.
┌─────────────┐ cookie ┌─────────────┐ OIDC ┌─────────────────┐
│ Angular │ ────────► │ API (BFF) │ ───────► │ Auth0 │
│ (SPA) │ │ /api/auth │ │ or │
│ │ ◄──cookie─ │ /api/* │ │ OpenIddict │
└─────────────┘ └─────────────┘ └─────────────────┘
The BFF controller uses standard OIDC — discovery document, authorization endpoint, token endpoint, userinfo endpoint. The identity provider is purely a server-side configuration choice. Angular is identical regardless of which IdP is used.
Prerequisites¶
- PR #2416 merged (BFF auth infrastructure)
- Access to the target environment's Helm values (cluster-gitops)
- For Auth0: Admin access to the Auth0
syrftenant - For OpenIddict: Identity service deployed and configured
Option A: BFF with Auth0¶
Use this when you want to test the BFF pattern without changing the identity provider. Auth0 continues to manage users, passwords, and Google OAuth. Only the frontend auth mechanism changes (JWT tokens → cookies).
Step 1: Create an Auth0 Confidential Client¶
The current Auth0 application (UYpAGmQq1leH...) is a Single Page Application (public client) — it cannot hold a client secret. The BFF requires a confidential client because the API sends client_secret in the token exchange.
- Log in to Auth0 Dashboard → tenant
syrf - Go to Applications → Create Application
-
Settings: Name =
SyRF BFF(or similar), Application Type =Regular Web Application -
In the new application's Settings tab, configure:
Allowed Callback URLs (one per environment):
https://api.staging.syrf.org.uk/api/auth/callback,
https://api.syrf.org.uk/api/auth/callback,
https://api.pr-2416.syrf.org.uk/api/auth/callback
Allowed Logout URLs:
Allowed Web Origins: same as logout URLs above.
Grant Types: Ensure Authorization Code is enabled.
Token Endpoint Authentication Method: Post (sends client_secret in body, matching the BFF implementation).
- Note the Client ID and Client Secret
Important: The BFF sends
client_secretin the token request body (line 201 of BffAuthController.cs), not as a Basic Auth header. Ensure "Token Endpoint Authentication Method" is set toPost, notBasic.
Step 2: Configure Auth0 Custom Claims¶
Auth0 needs to include SyRF-specific claims in the userinfo response. If you haven't already, ensure the following Auth0 Action (or Rule) is configured:
Post-Login Action — adds custom claims to the access token and userinfo:
exports.onExecutePostLogin = async (event, api) => {
const namespace = 'https://claims.syrf.org.uk/';
// Map Auth0 user metadata to SyRF claims
api.accessToken.setCustomClaim(namespace + 'user_id', event.user.user_id);
api.idToken.setCustomClaim(namespace + 'user_id', event.user.user_id);
if (event.user.given_name) {
api.accessToken.setCustomClaim(namespace + 'given_name', event.user.given_name);
}
if (event.user.family_name) {
api.accessToken.setCustomClaim(namespace + 'family_name', event.user.family_name);
}
if (event.user.nickname) {
api.accessToken.setCustomClaim(namespace + 'preferred_name', event.user.nickname);
}
if (event.user.picture) {
api.accessToken.setCustomClaim(namespace + 'picture', event.user.picture);
}
};
The BFF controller reads these claims from the userinfo endpoint (see AuthConstants.ClaimTypes in Constants.cs).
Step 3: Create Kubernetes Secrets¶
Create the bff-auth secret in the target namespace with the confidential client credentials from Step 1.
For preview/staging environments, add the secret via GCP Secret Manager + External Secrets Operator, or create directly:
# Example for staging (adapt namespace as needed)
# In practice, use GCP Secret Manager → ExternalSecret
kubectl create secret generic bff-auth \
--namespace=syrf-staging \
--from-literal=clientSecret='YOUR_AUTH0_CLIENT_SECRET'
For session storage (optional but recommended for multi-replica):
kubectl create secret generic bff-redis \
--namespace=syrf-staging \
--from-literal=connectionString='redis://redis:6379'
Note: Without Redis, the API uses
InMemorySessionStorewhich doesn't work across multiple replicas. This is fine for single-replica preview environments but needs Redis for staging/production.
Step 4: Update API Helm Values (cluster-gitops)¶
In cluster-gitops/syrf/environments/{env}/api/values.yaml:
# Enable BFF authentication
bffAuth:
enabled: true
# authority: auto-derived from auth0.customDomain — no need to set explicitly
clientId: "YOUR_AUTH0_BFF_CLIENT_ID" # From Step 1
clientSecretName: "bff-auth" # K8s secret from Step 3
# Redis session store (optional — omit for in-memory)
# redis:
# secretName: "bff-redis"
Step 5: Web Configuration (automatic)¶
The web frontend's authProvider is auto-derived from bffAuth.enabled. When BFF is enabled on the API, the web automatically uses authProvider: "bff". No separate web configuration is needed.
Override: If you need to force a specific auth provider on the web regardless of BFF state, set
authProviderexplicitly in the web Helm values.
Step 6: Commit, Push, and Verify¶
cd cluster-gitops
git add .
git commit -m "feat(auth): enable BFF auth with Auth0 for {environment}"
git push
ArgoCD will sync the changes. Verify:
- API pod restarts with new env vars (check
SYRF__BffAuth__Enabled=true) - Web pod restarts with
authProvider=bffin config - Login flow: Navigate to the app → redirects to
/api/auth/login→ Auth0 Universal Login → callback → cookie set → app loads - Session check:
GET /api/auth/mereturns user profile - API calls: Existing functionality works with cookie auth
Rollback¶
To revert to direct Auth0 SDK auth:
1. Set bffAuth.enabled: false in API values (web auto-reverts to auth0)
2. Commit and push to cluster-gitops
The combined auth scheme still accepts JWT bearer tokens, so existing Auth0 SDK sessions continue to work during transition.
Option B: BFF with OpenIddict¶
Use this after verifying BFF works with Auth0. The only change is the authority URL and client credentials — Angular is identical.
Step 1: Deploy the Identity Service¶
Ensure the identity service is deployed and healthy in the target environment:
- Helm chart:
src/services/identity/.chart/— deployed via ArgoCD - Secrets required by the identity service:
mongodb-identity— MongoDB connection string for identity datagoogle-oauth— Google OAuth client ID and secret (reused from Auth0)ses-credentials— AWS SES credentials for email deliveryopeniddict-signing-key— JWT signing keyopeniddict-encryption-key— JWT encryption key- DNS:
identity.{env}.syrf.org.ukresolving to the ingress
Step 2: Register the API as an OpenIddict Client¶
The identity service needs a registered client for the BFF. This is done via the OpenIddictClientSeeder which runs on startup:
In the identity service's Helm values or appsettings:
{
"OpenIddict": {
"Clients": [
{
"ClientId": "syrf-web",
"ClientSecret": "GENERATE_A_SECURE_SECRET",
"DisplayName": "SyRF Web Application (BFF)",
"RedirectUris": [
"https://api.staging.syrf.org.uk/api/auth/callback"
],
"PostLogoutRedirectUris": [
"https://staging.syrf.org.uk"
],
"Permissions": [
"ept:authorization",
"ept:token",
"ept:logout",
"gt:authorization_code",
"gt:refresh_token",
"scp:openid",
"scp:profile",
"scp:email",
"scp:offline_access",
"scp:syrf_api"
]
}
]
}
}
Step 3: Create Kubernetes Secrets¶
# BFF client secret (must match what OpenIddict expects for syrf-web)
kubectl create secret generic bff-auth \
--namespace=syrf-staging \
--from-literal=clientSecret='SAME_SECRET_AS_STEP_2'
# API-to-identity service-to-service auth (for admin user management)
kubectl create secret generic openiddict-api-client \
--namespace=syrf-staging \
--from-literal=clientSecret='API_CLIENT_SECRET'
Step 4: Update API Helm Values¶
In cluster-gitops/syrf/environments/{env}/api/values.yaml:
# BFF pointing to OpenIddict
bffAuth:
enabled: true
# authority: auto-derived from identityService.baseUrl — no need to set explicitly
clientId: "syrf-web"
clientSecretName: "bff-auth"
# Identity service admin API (server-to-server user management)
identityService:
baseUrl: "https://identity.staging.syrf.org.uk"
clientId: "syrf-api"
clientSecretName: "openiddict-api-client"
Step 5: Web Configuration (automatic)¶
Same as Auth0 — the web's authProvider is auto-derived from bffAuth.enabled. No separate configuration needed.
Step 6: Verify¶
Same verification steps as Auth0, plus:
- Identity service health: GET https://identity.{env}.syrf.org.uk/health
- Discovery document: GET https://identity.{env}.syrf.org.uk/.well-known/openid-configuration
- User registration: Test signup flow through the identity service's pages
- Password reset: Test the email flow via SES
Switching Between Auth0 and OpenIddict¶
The switch requires changing only the identity service URL and client credentials. Authority and authProvider are auto-derived:
| Setting | Auth0 | OpenIddict |
|---|---|---|
bffAuth.enabled |
true |
true |
bffAuth.clientId |
Auth0 BFF client ID | syrf-web |
bffAuth.clientSecretName |
Secret with Auth0 client secret | Secret with OpenIddict client secret |
identityService.baseUrl |
(empty — not needed) | https://identity.{env}.syrf.org.uk |
Auto-derived values (no manual config needed):
| Value | Auth0 | OpenIddict |
|---|---|---|
bffAuth.authority |
from auth0.customDomain |
from identityService.baseUrl |
Web authProvider |
"bff" (from bffAuth.enabled) |
"bff" (from bffAuth.enabled) |
Angular is identical in both cases — no frontend changes needed.
Configuration Reference¶
API Environment Variables (BFF)¶
| Variable | Description | Default |
|---|---|---|
SYRF__BffAuth__Enabled |
Enable BFF endpoints | false |
SYRF__BffAuth__Authority |
OIDC provider URL (auto-derived from identity/auth0 when empty) | "" |
SYRF__BffAuth__ClientId |
OIDC client ID | syrf-web |
SYRF__BffAuth__ClientSecret |
OIDC client secret (from K8s secret) | — |
SYRF__BffAuth__Scopes |
OIDC scopes | openid profile email offline_access syrf_api |
SYRF__BffAuth__SecureCookies |
Set Secure flag on cookies | true |
SYRF__BffAuth__SessionLifetime |
Session duration | 08:00:00 |
SYRF__BffAuth__FrontendOrigin |
Frontend URL (split-origin only) | "" |
SYRF__BffAuth__Redis__Configuration |
Redis connection string (from K8s secret) | — |
Web Configuration¶
The authProvider is auto-derived from bffAuth.enabled:
| Condition | Result | Effect |
|---|---|---|
bffAuth.enabled: true |
authProvider = "bff" |
Frontend uses cookies + /api/auth/* endpoints |
bffAuth.enabled: false (default) |
authProvider = "auth0" |
Frontend uses Auth0 Angular SDK (direct JWT) |
Explicit authProvider override |
Uses the override value | Manual control (e.g., "oidc" for E2E/mock) |
BFF Endpoints¶
| Endpoint | Method | Description |
|---|---|---|
/api/auth/login |
GET | Redirects to IdP login page |
/api/auth/signup |
GET | Redirects to IdP signup page |
/api/auth/callback |
GET | OIDC callback — exchanges code for tokens, creates session |
/api/auth/me |
GET | Returns current user profile (or 401) |
/api/auth/refresh |
POST | Refreshes session using refresh token |
/api/auth/logout |
POST | Destroys session and redirects to IdP logout |
Troubleshooting¶
Login redirects to 404¶
- Check
bffAuth.enabledistruein API config - Check the API pod has
SYRF__BffAuth__Enabled=trueenv var
Callback fails with "Token exchange failed"¶
- Verify the
client_secretmatches what the IdP expects - For Auth0: ensure the application type is "Regular Web Application" (confidential)
- Check the callback URL is in the IdP's allowed callback URLs
User data doesn't load after login¶
- Check the
signInRecorded$effect fires (browser console: "ABOUT TO RECORD SIGN-IN") - Verify
authProvideris"bff"in the web config (not"auth0")
Session lost across API replicas¶
- Configure Redis session store (
bffAuth.redis.secretName) - Without Redis, each replica has its own in-memory store
Auth0: "Unauthorized" on token exchange¶
- Ensure "Token Endpoint Authentication Method" is
Post(notBasic) - The BFF sends
client_secretin the POST body, not as an Authorization header