Skip to content

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 syrf tenant
  • 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.

  1. Log in to Auth0 Dashboard → tenant syrf
  2. Go to ApplicationsCreate Application
  3. Settings: Name = SyRF BFF (or similar), Application Type = Regular Web Application

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

https://staging.syrf.org.uk,
https://app.syrf.org.uk,
https://pr-2416.syrf.org.uk

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

  1. Note the Client ID and Client Secret

Important: The BFF sends client_secret in the token request body (line 201 of BffAuthController.cs), not as a Basic Auth header. Ensure "Token Endpoint Authentication Method" is set to Post, not Basic.

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 InMemorySessionStore which 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 authProvider explicitly 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:

  1. API pod restarts with new env vars (check SYRF__BffAuth__Enabled=true)
  2. Web pod restarts with authProvider=bff in config
  3. Login flow: Navigate to the app → redirects to /api/auth/login → Auth0 Universal Login → callback → cookie set → app loads
  4. Session check: GET /api/auth/me returns user profile
  5. 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:

  1. Helm chart: src/services/identity/.chart/ — deployed via ArgoCD
  2. Secrets required by the identity service:
  3. mongodb-identity — MongoDB connection string for identity data
  4. google-oauth — Google OAuth client ID and secret (reused from Auth0)
  5. ses-credentials — AWS SES credentials for email delivery
  6. openiddict-signing-key — JWT signing key
  7. openiddict-encryption-key — JWT encryption key
  8. DNS: identity.{env}.syrf.org.uk resolving 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.enabled is true in API config
  • Check the API pod has SYRF__BffAuth__Enabled=true env var

Callback fails with "Token exchange failed"

  • Verify the client_secret matches 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 authProvider is "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 (not Basic)
  • The BFF sends client_secret in the POST body, not as an Authorization header