Skip to content

Architecture Overview: CommandCenter Platform with BFF Gateway#

Current Architecture Context diagram

flowchart TB
    user(user)

    user--personal data-->bff

    subgraph external [Internet]
        direction LR
        entraId[Entra ID]
        bff((YARP Gateway\nFront Door BFF))
        react((React\nWeb UI))

        bff-->react
        react-.api calls.->bff
    end

    subgraph dmz [Internal Network]
        direction LR
        ldb[|borders:tb|legacy db]
        servers((Servers API))

        bff--api routes-->api
        db<-->api
        api-.->legacy-api
        api-.->entraId
        api-->mediation

        subgraph cc-api [CommandCenter API]
            direction LR
            db[|borders:tb|database]
            api((CommandCenter\nAPI))
        end

        subgraph legacy-api [Legacy CC]
            direction LR
            legacy((Legacy API))
            legacy-service((CC Service))

            bff--legacy api routes-->legacy
            legacy<-->ldb
            legacy-service<-->ldb
        end
        subgraph mediation [Mediation Layer]
            direction LR
            servers-.->ldb
        end
        subgraph benefit [BenefitManager]
            direction LR
            db-customer[|borders:tb|customer db]
            bm-service((BenefitManager\nService))
            bm-app((BenefitManager\nWeb))
            api -- "`versioned req/res`" --> bm-app
            bm-service <--> db-customer
            bm-app <--> db-customer
        end
    end

%% Element type definitions

    classDef boundary fill:none
    dmz:::boundary
    external:::boundary
    classDef apis stroke-dasharray: 5 5
    legacy-api:::apis
    mediation:::apis
    cc-api:::apis
    benefit:::apis
Hold "Alt" / "Option" to enable pan & zoom

This architecture describes a layered enterprise web platform built around the Backend-for-Frontend (BFF) pattern, separating an internet-facing edge from an internal network that hosts both a modern API and a legacy system in mid-migration. The picture that emerges is a classic strangler-fig setup: a new CommandCenter API gradually taking over responsibilities from a Legacy CC system, with a mediation layer and a domain-specific BenefitManager module living alongside them.

The Internet-Facing Edge#

The user's entry point is a YARP Gateway acting as a Front Door BFF. All personal data flows from the user into this gateway, which serves two roles at once: it delivers the React Web UI to the browser, and it receives the API calls that the React app makes back to it. This is the security-relevant design decision of the whole architecture — the browser never talks to any backend service directly. The BFF is the single choke point where authentication, token handling, request routing, and header sanitization can be enforced, which is exactly what YARP (Yet Another Reverse Proxy, the .NET reverse-proxy library) is built for.

Entra ID sits in the internet zone as the identity provider. Notably, the dashed line shows the CommandCenter API calling out to Entra ID — suggesting the internal API validates tokens or queries directory/graph data itself, rather than identity being handled exclusively at the edge.

The Internal Network#

Everything behind the gateway lives in an internal network zone (labeled as a DMZ-style boundary), which the BFF reaches through two distinct routing rules:

CommandCenter API is the modern core. The BFF forwards standard API routes here, and the API owns its own database with bidirectional read/write access. From this hub, three outbound relationships fan out: a dashed (loosely coupled or transitional) dependency on the Legacy API, the dashed call to Entra ID, and a solid dependency on the Mediation Layer.

Legacy CC is the system being phased out. Importantly, the BFF routes legacy API routes directly to the Legacy API — meaning the old system is still serving live traffic for some endpoints, not just being called internally. Both the Legacy API and a background CC Service share read/write access to the legacy db. That shared-database pattern between an API and a service is typical of older architectures and is usually one of the harder things to untangle during migration.

Mediation Layer contains a Servers API with a dashed connection into the legacy database. Its role appears to be an anti-corruption or adapter layer: it gives the modern CommandCenter API a controlled, indirect path to legacy data without coupling the new API directly to the old schema. The dashed lines reinforce that this is intended as a temporary or loosely bound bridge.

BenefitManager is a self-contained business domain with three parts: a BenefitManager Web front-end, a BenefitManager Service, and a dedicated customer db that both components read and write. The CommandCenter API communicates with BenefitManager Web through explicitly versioned request/response contracts — a deliberate choice that decouples the two systems' release cycles and signals that BenefitManager is treated as an independent product with its own lifecycle rather than an internal module.

Key Design Characteristics#

A few properties are worth calling out:

  • Single ingress, zero direct backend exposure. All user traffic funnels through the YARP BFF; internal services are unreachable from the internet. Personal data crosses the boundary exactly once, at a point where it can be inspected and protected.
  • Strangler-fig migration in progress. Solid arrows mark the target-state paths (BFF → CommandCenter API → its own database), while dashed arrows mark transitional dependencies (API → Legacy, Mediation → legacy db). The diagram's dashed subgraph borders on Legacy CC, Mediation, and the API groups visually echo this "boundary in flux" status.
  • Database-per-context, with one legacy exception. CommandCenter, BenefitManager, and Legacy each own their data store. The only shared database is the legacy one — shared between Legacy API, CC Service, and (indirectly) the Servers API — which is precisely where the migration risk concentrates.
  • Contract versioning at the seam. The versioned req/res link between CommandCenter and BenefitManager is the formal integration contract that lets the two evolve independently.

In short: a YARP-fronted React application backed by a modern CommandCenter API, coexisting with a legacy system through direct legacy routing, a mediation/adapter layer, and a versioned contract to an autonomous BenefitManager domain — an architecture clearly designed to migrate away from the legacy core incrementally while keeping a single hardened entry point for all user traffic.

Authentication & Authorization Flow: OIDC Authorization Code + PKCE with On-Behalf-Of Delegation#

Overview Participant

  • BFF (YARP + ASP.NET Core)
  • AzureEntraID as Entra ID
  • API 1 (ASP.NET Core)
  • API 2 (ASP.NET Core)
sequenceDiagram
    participant SPA
    participant Browser
    participant BFF
    participant Entra ID
    participant API 1
    participant API 2

    Note over SPA, BFF: Initial Page Load (User Not Authenticated)
    SPA->>Browser: Load application
    Browser->>SPA: Renders UI (e.g., shows Login button)

    Note over SPA, BFF: User Initiates Login
    SPA->>Browser: User clicks Login
    Browser->>BFF: GET /login (or similar auth trigger endpoint)

    Note over BFF, Entra ID: BFF Initiates Auth Code Flow
    BFF->>Browser: Redirect (302) to Entra ID /authorize endpoint (with client_id, redirect_uri, scope, code_challenge, state)
    Browser->>Entra ID: GET /authorize endpoint
    Entra ID->>Browser: Shows Login Page (if not already signed in)
    Browser->>Entra ID: User enters credentials
    Entra ID->>Browser: User authenticates, grants consent (if needed)
    Entra ID->>Browser: Redirect (302) back to BFF /signin-oidc (with authorization_code, state)

    Note over BFF, Entra ID: BFF Exchanges Code for Tokens
    Browser->>BFF: GET /signin-oidc (Callback)
    BFF->>BFF: Validates state
    BFF->>Entra ID: POST /token endpoint (with client_id, client_secret, code, code_verifier, redirect_uri)
    Entra ID-->>BFF: Returns ID Token, Access Token (for BFF), Refresh Token
    BFF->>BFF: Validates tokens, Creates session (e.g., cookie) for the user
    BFF->>Browser: Redirect (302) back to SPA (e.g., '/') with session cookie set (HttpOnly, Secure)
    Browser->>SPA: Follows redirect, now has session cookie

    Note over SPA, BFF: Authenticated SPA Call
    SPA->>Browser: Makes API call (e.g., fetch('/api/proxy/resource1'))
    Browser->>BFF: GET /api/proxy/resource1 (includes session cookie)
    BFF->>BFF: Validates session cookie, identifies user

    Note over BFF, API 1: BFF Calls Downstream API 1 (On-Behalf-Of User)
    BFF->>BFF: Needs to call API 1
    BFF->>Entra ID: POST /token endpoint (OBO Flow - grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer, client_id, client_secret, assertion=<User's Access Token for BFF>, requested_token_use=on_behalf_of, scope=<API 1 Scope>)
    Entra ID-->>BFF: Returns new Access Token scoped for API 1 (representing the original user)
    BFF->>API 1: GET /resource (includes 'Authorization: Bearer <API 1 Access Token>')

    Note over API 1: API 1 Validates Token & Authorizes
    API 1->>API 1: Receives request, validates Bearer token (signature, issuer, audience=API 1, expiry, scopes/roles)
    API 1->>API 1: Checks authorization policies (e.g., user has required scope 'Resource1.Read')
    API 1-->>BFF: Returns resource data (e.g., 200 OK with JSON)

    Note over BFF, API 2: (Similar OBO flow if calling API 2)
    BFF->>BFF: Needs to call API 2
    BFF->>Entra ID: POST /token endpoint (OBO Flow - requesting token for API 2 scope)
    Entra ID-->>BFF: Returns new Access Token scoped for API 2
    BFF->>API 2: GET /other-resource (includes 'Authorization: Bearer <API 2 Access Token>')
    API 2->>API 2: Validates token (audience=API 2), checks authorization
    API 2-->>BFF: Returns other resource data

    Note over BFF, SPA: BFF Returns Response
    BFF->>Browser: Returns combined/transformed response to original request
    Browser->>SPA: Delivers response data
Hold "Alt" / "Option" to enable pan & zoom

This sequence diagram completes the architecture picture by showing how the YARP BFF earns its place as the security choke point. The design implements the token handler pattern: the browser never sees, stores, or transmits an OAuth token of any kind. All tokens live server-side in the BFF, and the SPA's only credential is an opaque, HttpOnly session cookie. Combined with the On-Behalf-Of (OBO) flow toward the downstream APIs, this gives you end-to-end user identity propagation without ever exposing bearer tokens to JavaScript.

Phase 1 — Anonymous Load and Login Trigger#

The SPA loads and renders in an unauthenticated state — no token acquisition happens in the browser, which is the first deliberate departure from the classic SPA-with-MSAL approach. When the user clicks Login, the SPA simply navigates to a BFF endpoint (GET /login). From this moment on, the server drives authentication, not the client.

Phase 2 — Authorization Code Flow with PKCE (BFF ↔ Entra ID)#

The BFF responds with a 302 redirect to Entra ID's /authorize endpoint, carrying the client_id, redirect_uri, requested scopes, a state value, and a code_challenge. Two things are notable here:

  • PKCE on a confidential client. Even though the BFF holds a client secret, it still sends a code_challenge/code_verifier pair. This is current best practice (and required by OAuth 2.1): PKCE binds the authorization code to the party that initiated the flow, neutralizing code-injection and interception attacks at negligible cost.
  • state for CSRF protection. The BFF validates the returned state at the callback before doing anything else, preventing a forged callback from being processed.

The user authenticates against Entra ID directly — credentials never touch the BFF or the SPA — and consents if required. Entra ID redirects the browser back to the BFF's /signin-oidc callback with the one-time authorization code.

Phase 3 — Code Exchange and Session Establishment#

The BFF performs the back-channel exchange: it POSTs the code to Entra ID's /token endpoint together with its client_secret and the code_verifier. In return it receives the ID Token (user identity claims), an Access Token scoped for the BFF itself, and a Refresh Token for silent renewal.

After validating the tokens (signature, issuer, audience, expiry, nonce), the BFF creates a server-side session and hands the browser an HttpOnly, Secure session cookie, then redirects back to the SPA. This is the trust boundary translation of the whole design: OAuth tokens on the inside, a hardened cookie on the outside. Because the cookie is HttpOnly, XSS in the SPA cannot exfiltrate credentials — the worst an injected script can do is ride the existing session, which is a strictly smaller attack surface than token theft.

Phase 4 — Authenticated API Calls Through the Proxy#

The SPA now calls relative endpoints like /api/proxy/resource1; the browser attaches the session cookie automatically (same-origin, so no CORS complexity either). The BFF validates the session, resolves the user, and determines which downstream API the route maps to — in the architecture diagram, these route to the CommandCenter API or the Legacy API depending on the path.

Phase 5 — On-Behalf-Of Token Exchange per Downstream API#

Here's where authorization gets granular. For each downstream API, the BFF performs an OBO exchange with Entra ID: it presents the user's access token as a jwt-bearer assertion (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer, requested_token_use=on_behalf_of) plus its own client credentials, and requests a token scoped specifically for that API.

The critical properties this buys you:

  • Audience isolation. API 1 receives a token with audience=API 1; API 2 gets its own with audience=API 2. A token stolen from or leaked by one service is useless against another — there is no shared "god token" circulating internally.
  • Preserved user identity. The OBO token still represents the original user, not the BFF's service identity. Downstream APIs can apply per-user authorization without trusting custom headers or implicit claims.
  • Least-privilege scoping. Each token carries only the scopes that API needs (e.g., Resource1.Read), so the blast radius of any single token is minimal.

Phase 6 — Downstream Validation and Policy Enforcement#

Each API independently performs full JWT validation — signature against Entra ID's signing keys, issuer, its own audience, expiry — and then evaluates its authorization policies (required scopes or app roles) before serving the resource. This is defense in depth: the APIs don't trust the BFF blindly; they verify cryptographic proof of both the caller's legitimacy and the user's entitlements on every request. In ASP.NET Core terms, this is the standard AddJwtBearer + policy-based authorization setup on each service.

Finally, the BFF can aggregate or transform responses from multiple APIs before returning a single result to the SPA — a natural fit for the gateway aggregation that YARP enables.

Why This Design Holds Together#

Mapping it back onto the architecture diagram, the security story is now complete and consistent:

Concern Where it's solved
Token exposure to the browser Eliminated — token handler pattern, HttpOnly cookie only
CSRF state parameter + cookie SameSite policies at the BFF
Code interception PKCE, even on the confidential client
Lateral token reuse between APIs Per-audience OBO tokens
User identity at the backend OBO preserves the user principal end-to-end
Token refresh Refresh token held server-side; the SPA never deals with expiry

The net effect: the SPA is a pure UI with no security machinery, the BFF is the single OAuth-aware confidential client, Entra ID is the sole token authority, and every internal API enforces its own audience-bound, scope-checked authorization. Identity flows from the user's login all the way to the CommandCenter and Legacy APIs without a single bearer token ever crossing into the browser.