BFF Security Framework: Documentation for Duende's Backend for Frontend (BFF) framework, used to secure browser-based frontends (e.g. SPAs with React, Vue, Angular, or Blazor applications) with ASP.NET Core backends ----- # Backend For Frontend (BFF) Security Framework > A comprehensive security framework for securing browser-based frontends with ASP.NET Core backends The Duende.BFF (Backend For Frontend) security framework packages the necessary components to secure browser-based frontends (e.g., SPAs or Blazor applications) with ASP.NET Core backends. Duende.BFF is a library for building services that comply with the BFF pattern and solve security and identity problems in browser-based applications such as SPAs and Blazor-based applications. It is used to create a backend host that is paired with a frontend application. This backend is called the Backend For Frontend (BFF) host, and is responsible for all the OAuth and OIDC protocol interactions. It completely implements the latest recommendations from the IETF regarding security for browser-based applications. It offers the following functionality: * Protection from Token Extraction attacks * Built-in CSRF Attack protection * Server Side OAuth 2.0 Support * Multi-frontend support (Introduced in V4) * User Management APIs * Back-channel logout * Securing access to both local and external APIs by serving as a reverse proxy. * Server side Session State Management * Blazor Authentication State Management * Open Telemetry support (Introduced in V4) Duende.BFF is free for development, testing and personal projects, but production use requires a license. Special offers may apply. The source code for the BFF framework can be found on GitHub. Builds are distributed through NuGet. Also check out the samples. [GitHub Repository ](https://github.com/DuendeSoftware/products/tree/main/bff)View the source code for this library on GitHub. [NuGet Package ](https://www.nuget.org/packages/Duende.BFF)View the package on NuGet.org. ## Getting Started [Section titled “Getting Started”](#getting-started) If you’re upgrading from a previous version, please check our [upgrade guides](/bff/upgrading). If you’re starting a new BFF project, consider the following startup guides: * [Single frontend BFF](/bff/getting-started/single-frontend/) * [Multi-frontend BFF](/bff/getting-started/multi-frontend/) * [Blazor](/bff/getting-started/blazor/) ## Do I Need BFF? [Section titled “Do I Need BFF?”](#do-i-need-bff) If you’re building a browser-based application (SPA or Blazor WASM) that needs to call authenticated APIs, BFF is the recommended security architecture. This section helps you decide. ### BFF Pattern vs. Token-in-Browser [Section titled “BFF Pattern vs. Token-in-Browser”](#bff-pattern-vs-token-in-browser) | Concern | BFF Pattern | Token-in-Browser | | ----------------------------- | ------------------------------------------------- | ---------------------------------------------------------------- | | **Token storage** | Server-side only — browser never sees tokens | Tokens in `localStorage` or `sessionStorage` | | **XSS token theft** | Not possible — no tokens in browser memory | High risk — any injected script can steal tokens | | **Third-party cookie issues** | Not affected — uses first-party session cookies | Silent renewal via `prompt=none` breaks in Safari/Firefox/Chrome | | **CSRF exposure** | Mitigated with `X-CSRF` header + SameSite cookies | Not applicable (tokens sent as `Authorization` header) | | **Session revocation** | Server can forcibly end sessions | No server-side control; wait for token expiry | | **Complexity** | Slightly more infrastructure (BFF host required) | Simpler initial setup, but security is harder to get right | | **IETF recommendation** | ✅ Recommended (OAuth 2.0 for Browser-Based Apps) | ❌ Implicit grant deprecated; token-in-browser discouraged | Security implications of NOT using BFF Storing access tokens in the browser (e.g., `localStorage`, `sessionStorage`, or JavaScript memory) exposes them to theft via XSS attacks and supply-chain attacks through compromised npm packages. Once stolen, an attacker can use the token independently of the user’s browser — there is no way to detect or stop this. Additionally, OIDC silent login (`prompt=none`) relies on third-party cookies, which are blocked by Safari, Firefox, and increasingly Chrome. This means session management in token-in-browser apps will silently break for a growing proportion of users. See [Threats Against Browser-based Applications](#threats-against-browser-based-applications) below for the full threat model. ## Background [Section titled “Background”](#background) Single-Page Applications (SPAs) are increasingly common, offering rich functionality within the browser. Front-end development has rapidly evolved with new frameworks and changing browser security requirements. Consequently, best practices for securing these applications have also shifted dramatically. While implementing OAuth logic directly in the browser was once considered acceptable, this is no longer recommended. Storing any authentication state in the browser (such as access tokens) has proven to be inherently risky (see Threats against browser based applications). Because of this, the IETF is currently recommending delegating all authentication logic to a server-based host via a Backend-For-Frontend pattern as the preferred approach to securing modern web applications. ### The Backend For Frontend Pattern [Section titled “The Backend For Frontend Pattern”](#the-backend-for-frontend-pattern) The BFF pattern (Backend-For-Frontend) pattern states that every browser based application should also have a server side application that handles all authentication requirements, including performing authentication flows and securing access to APIs. The server will now expose http endpoints that the browser can use to login, logout or interrogate the active session. With this, the browser based application can trigger an authentication flow by redirecting to a URL, such as /bff/login. Once the authentication process is completed, the server places a secure authentication cookie in the browser. This cookie is then used to authenticate all subsequent requests, until the user is logged out again. The BFF should expose all APIs that the front-end wants to access securely. So it can either host APIs locally, or act as a reverse proxy towards external APIs. With this approach, the browser based application will not have direct access to the access token. So if the browser based application is compromised, for example with XSS attacks, there is no risk of the attacker stealing the access tokens. As the name of this pattern already implies, the BFF backend is the (only) Backend for the Frontend. They should be considered part of the same application. It should only expose the APIs that the front-end needs to function. ### 3rd party cookies [Section titled “3rd party cookies”](#3rd-party-cookies) In recent years, several browsers (notably Safari and Firefox) have started to block 3rd party cookies. Chrome is planning to do the same in the future. While this is done for valid privacy reasons, it also limits some of the functionality a browser based application can provide. A couple of particularly notable OIDC flows that don’t work for SPAs when third party cookies are blocked are OIDC Session Management and OIDC Silent Login via the prompt=none parameter. ### CSRF protection [Section titled “CSRF protection”](#csrf-protection) There is one thing to keep an eye out for with this pattern, and that’s Cross Site Request Forgery (CSRF). The browser automatically sends the authentication cookie for safe-listed cross-origin requests, which exposes the application to CORS Attacks. Fortunately, this threat can easily be mitigated by a BFF solution by requiring a custom header to be passed along. See more on CORS protection. ### The BFF Framework in an application architecture [Section titled “The BFF Framework in an application architecture”](#the-bff-framework-in-an-application-architecture) The following diagram illustrates how the Duende BFF Security Framework fits into a typical application architecture. ``` flowchart TD subgraph Browser SPA["Browser-Based Application"] CookieJar["🍪 Cookie Jar"] end subgraph BFF["BFF Host"] AuthEndpoints["Authentication
Endpoints"] SessionMgmt["Session
Management"] CookieAuth["Cookie Authorization"] CSRF["CSRF Protection"] Proxy["Proxy to
External APIs"] LocalAPIs["Local APIs"] SessionStore[("Server-Side
Session Storage")] end IdP["Identity Provider"] ExternalAPIs["External APIs"] SPA -->|"login / logout"| AuthEndpoints AuthEndpoints -->|"Set-Cookie"| CookieJar CookieJar -->|"Auth cookie"| CookieAuth AuthEndpoints <-->|"redirect"| IdP AuthEndpoints --> SessionMgmt SessionMgmt --> SessionStore CookieAuth --> CSRF CSRF --> Proxy CSRF --> LocalAPIs Proxy -->|"Bearer token"| ExternalAPIs SessionMgmt -->|"Acquire tokens"| IdP ExternalAPIs -->|"Validate tokens"| IdP ``` The browser based application runs inside the browser’s secure sandbox. It can be built using any type of front-end technology, such as via Vanilla-JS, React, Vue, WebComponents, Blazor, etc. When the user wants to log in, the app can redirect the browser to the authentication endpoints. This will trigger an OpenID Connect authentication flow, at the end of which, it will place an authentication cookie in the browser. This cookie has to be an HTTP Only Same Site and Secure cookie. This makes sure that the browser application cannot get the contents of the cookie, which makes stealing the session much more difficult. The browser will now automatically add the authentication cookie to all calls to the BFF, so all calls to the APIs are secured. This means that embedded (local) APIs are already automatically secured. The app cannot access external Api’s directly, because the authentication cookie won’t be sent to 3rd party applications. To overcome this, the BFF can proxy requests through the BFF host, while exchanging the authentication cookie for a bearer token that’s issued from the identity provider. This can be configured to include or exclude the user’s credentials. As mentioned earlier, the BFF needs protection against CSRF attacks, because of the nature of using authentication cookies. While .net has various built-in methods for protecting against CSRF attacks, they often require a bit of work to implement. The easiest way to protect (just as effective as the .Net provided security mechanisms) is just to require the use of a custom header. The BFF Security framework by default requires the app to add a custom header called x-csrf=1 to the application. Just the fact that this header must be present is enough to protect the BFF from CSRF attacks. ### Logical and Physical Sessions [Section titled “Logical and Physical Sessions”](#logical-and-physical-sessions) When implemented correctly, a user will think of their time interacting with a solution as *“one session”* also known as the **“logical session”**. The user should not be concerned with the steps developers take to provide a seamless experience. Users want to use the app, get their tasks completed, and log out happy. ``` sequenceDiagram actor Alice box logical session participant App end Alice->>App: /login App->>Alice: /account ``` So while the user will only see (and care about) a single session, it’s entirely possible that there will be multiple physical sessions active. For most distributed applications, including those implemented with BFF, **sessions are managed independently by each component of an application architecture.** This means that there are **N+1** physical sessions possible, where **N** is the number of sessions for each service in your solution, and the **+1** being the session managed on the BFF host. Since we are focusing on ASP.NET Core, those sessions typically are stored using the Cookie Authentication handler features of .NET. ``` sequenceDiagram actor Alice box App session participant App end box Service 1 session participant Service 1 end box Service N... session participant Service N... end Alice->>App: /login App->>Alice: /account App->>Service 1: request App->>Service N...: N... request ``` The separation allows each service to manage its session to its specific needs. While it can depend on your requirements, we find most developers want to coordinate the physical session lifetimes, creating a more predictable logical session. If that is your case, we recommend you first start by turning each physical session into a more powerful [server-side session](/bff/fundamentals/session/server-side-sessions/). Server-side sessions are instances that are persisted to data storage and allow for visibility into currently active sessions and better management techniques. Let’s take a look at the advantages of server-side sessions. Server-side sessions at each component allows for: * Receiving back channel logout notifications * Forcibly end a user’s session of that node * Store and view information about a session lifetime * Coordinate sessions across an application’s components * Different claims data Server-side sessions at IdentityServer allow for more powerful features: * Receive back channel logout notifications from upstream identity providers in a federation * Forcibly end a user’s session at IdentityServer * Global inactivity timeout across SSO apps and session coordination * Coordinate sessions to registered clients Keep in mind the distinctions between logical and physical sessions, and you will better understand the interplay between elements in your solution. ### Threats Against Browser-based Applications [Section titled “Threats Against Browser-based Applications”](#threats-against-browser-based-applications) Let’s look at some of the common ways browser-based apps are typically attacked and what their consequences would be. #### Token theft [Section titled “Token theft”](#token-theft) Often, malicious actors are trying to steal access tokens. In this paragraph, we’ll look into several techniques how this is often done and what the consequences are. But it’s important to note that all these techniques rely on the browser-based application having access to the access token. Therefore, these attacks can be prevented by implementing the BFF pattern. ##### Script injection attacks [Section titled “Script injection attacks”](#script-injection-attacks) The most common way malicious actors steal access tokens is by injecting malicious JavaScript code into the browser. This can happen in many different ways. Script injection attacks or supply chain attacks (via compromised NPM packages or cloud-hosted scripts) are just some examples. Since the malicious code runs in the same security sandbox as the application’s code, it has exactly the same privileges as the application code. This means there is no way to securely store and handle access tokens in the browser. There have been attempts to place the code that accesses and uses web tokens in more highly isolated storage areas, such as Web Workers, but these attempts have also been proven to be vulnerable to token exfiltration attacks, so they are not suitable as an alternative. If the browser-based application has access to your access token, so can malicious actors. ##### Other ways of compromising browser security [Section titled “Other ways of compromising browser security”](#other-ways-of-compromising-browser-security) Injecting code is not the only way that browser security can be broken. Sometimes the browser sandbox itself is under attack. Browsers attempt to provide a secure environment in which web pages and their scripts can safely be loaded and executed in isolation. On many occasions, this browser sandbox has been breached by exploits. A recent example is the POC from Google on Browser-Based Spectre Attacks. By bypassing the security sandbox, the attackers are able to read the memory from your application and steal the access tokens. The best way to protect yourself from this is not having any access tokens stored in the application’s memory at all by following the BFF pattern. ##### Consequences of token theft [Section titled “Consequences of token theft”](#consequences-of-token-theft) Once an attacker is able to inject malicious code, there are a number of things the attacker can do. At a minimum, the attacker can take over the current user’s session and in the background perform malicious actions under the credentials of the user. This would only be possible as long as the user has the application open, which limits how long the attacker can misuse the session. It’s worse if the attacker is able to extract the authentication token. The attacker can now access the application directly from his own computer, as long as the access token is valid. For this reason, it’s recommended to keep access token lifetimes short. If the attacker is also able to acquire the refresh token or worse, is able to request new tokens, then the attacker can use the credentials indefinitely. ##### Attacks at OAuth Implicit Grant [Section titled “Attacks at OAuth Implicit Grant”](#attacks-at-oauth-implicit-grant) Sometimes there are vulnerabilities discovered even in the protocols that are underlying most of the web’s security. As a result, these protocols are constantly evolving and updated to reflect the latest knowledge and known vulnerabilities. One example of this is OAuth Implicit grant. This was once a recommended pattern and many applications have implemented this since. However, in recent years it’s become clear that this protocol is no longer deemed secure and in the words of the IETF: > Browser-based clients MUST use the Authorization Code grant type and MUST NOT use the Implicit grant type to obtain access tokens #### CSRF Attacks [Section titled “CSRF Attacks”](#csrf-attacks) Cookie-based authentication (when using Secure and HTTP Only cookies) effectively prevents browser-based token stealing attacks. But this approach is vulnerable to a different type of attack, namely CSRF attacks. This is similar but different from CORS attacks which lies in the definition of what the browser considers a Site vs an Origin and what kind of request a browser considers ‘safe’ for Cross Origin requests. ##### Origins and Sites [Section titled “Origins and Sites”](#origins-and-sites) To a browser, a [site](https://developer.mozilla.org/en-US/docs/Glossary/Site) is defined as TLD (top-level domain - 1). So, a single segment under a top-level domain, such as example in `example.co.uk`, where `co.uk` is the top-level domain. Any subdomain under that (so `site1.example.co.uk` and `www.example.co.uk`) are considered to be from the same site. Contrast this to an origin, which is the scheme + hostname + port. In the previous example, the origins would be `https://example.co.uk` and `https://www.example.co.uk`. The site is the same, but the origin is different. Browsers have built-in control when cookies should be sent. For example, by setting [SameSite=strict](https://owasp.org/www-community/SameSite), the browser will only send along cookies if you are navigating within the same **site** (not origins). Browsers also have built-in **Cross Origin** protection. Most requests that go across different origins (not sites) will by default be subjected to CORS protection. This means that the server needs to say if the requests are safe to use cross-origin. The exclusion to this are requests that the browser considers safe. The following diagram (created based on this article [Wikipedia](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)) shows this quite clearly: ``` flowchart TD; A[JavaScript makes a cross-domain XHR call] --> B{Is it a GET or HEAD?}; subgraph cors-safe B -->|Yes| X[Make actual XHR]; B -->|No| C{Is it a POST?}; C -->|Yes| E{Is the content-type standard?}; C -->|No| D[Make OPTIONS call to server with all custom details]; E -->|No| D; E -->|Yes| F{Are there custom HTTP headers?}; F -->|No| X; F -->|Yes| D; end subgraph cors-verify D --> G{Did server respond with appropriate Access-Control-* headers?}; G -->|No| H[ERROR]; end G -->|Yes| X; style cors-safe fill:#d9ead3,stroke:#6aa84f; style cors-verify fill:#f4cccc,stroke:#cc0000; ``` So some requests, like regular GET or POSTs with a standard content type are NOT subject to CORS validation, but others (IE: deletes or requests with a custom HTTP header) are. ##### CSRF Attack inner workings [Section titled “CSRF Attack inner workings”](#csrf-attack-inner-workings) CSRF attacks exploit the fact that browsers automatically send authentication cookies with requests to the same [site](https://developer.mozilla.org/en-US/docs/Glossary/Site). Should an attacker trick a user that’s logged in to an application into visiting a malicious website, that browser can make malicious requests to the application under the credentials of the user. Same Site cookies already drastically reduce the attack surface because they ensure the browser only sends the cookies when the user is on the same site. So a user logged in to an application at app.company.com will not be vulnerable when visiting malicious-site.com. However, the application can still be at risk. Should other applications running under different subdomains of the same site be compromised, then you are still vulnerable to CSRF attacks. Luring a user to a compromised site under a subdomain will bypass this Same Site protection and leave the application still vulnerable to CSRF attacks. Unfortunately, compromised applications running under different subdomains is a common attack vector, not to be underestimated. ##### Protection against CSRF Attacks [Section titled “Protection against CSRF Attacks”](#protection-against-csrf-attacks) Many frameworks, including [dotnet](https://learn.microsoft.com/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-9.0), have built-in protection against CSRF attacks. These mitigations require you to make certain changes to your application, such as embedding specific form fields in your application which needs to be re-submitted or reading a specific cookie value. While these protections are effective, there is a simpler and more straight forward solution to preventing any CSRF attack. The trick is to require a custom header on the APIs that you wish to protect. It doesn’t matter what that custom header is or what the value is, for example, some-header=1. The browser-based application now MUST send this header along with every request. However, if a page on the malicious subdomain wants to call this API, it also has to add this custom header. This custom header now triggers a CORS Preflight check. This pre-flight check will fail because it detects that the request is cross-origin. Now the API developer has to develop a CORS policy that will protect against CORS attacks. So, effective CSRF attack protection relies on these pillars: 1. Using **Same-Site=strict** Cookies 2. Requiring a specific header to be sent on every API request (IE: x-csrf=1) 3. having a cors policy that restricts the cookies only to a list of white-listed **origins**. ##### Session Hijacking [Section titled “Session Hijacking”](#session-hijacking) In session hijacking, a malicious actor somehow gets access to the user’s session cookie and is then able to exploit it by effectively cloning the session. Before HTTPS was widespread, session hijacking was a common occurrence, especially when using public Wi-Fi networks. However, since SSL connections are pretty much widespread, this has become more difficult. Not impossible, because there have been cases where trusted certificate authorities have been compromised. Even if SSL is not compromised, there are other ways for malicious actors to hijack the session. For example, if the user’s computer is compromised then browser security can still be bypassed. There have also been occurrences of session hijacking where (malicious) helpdesk employees asked for ‘har’ files (which are effectively complete request traces, including the authentication cookies), which were then used to hijack sessions. Right now, it’s very difficult to completely protect against this type of attack. However, there are interesting new standards being discussed, such as Device Bound Session Credentials. This standard aims to make sure that a session is cryptographically bound to a single device. Even if stolen, it can’t be used by a different device. ## See Also [Section titled “See Also”](#see-also) * [Duende IdentityServer](/identityserver/) — The authorization server BFF authenticates against for OpenID Connect flows * [Access Token Management](/accesstokenmanagement/) — Token lifecycle library used by BFF for automatic token refresh and caching ----- # Architecture > Overview of BFF host architecture, including authentication, session management, and integration with ASP.NET Core components A BFF host is an ASP.NET Core application that acts as a security proxy between the browser and your backend APIs. Understanding the key architectural decisions up front will save you significant rework later. ## How the BFF Fits Into Your System [Section titled “How the BFF Fits Into Your System”](#how-the-bff-fits-into-your-system) The following diagram shows how the BFF protects browser-based applications: ``` flowchart TD subgraph Browser SPA["Browser-Based Application"] CookieJar["🍪 Cookie Jar"] end subgraph BFF["BFF Host"] AuthEndpoints["Authentication
Endpoints"] SessionMgmt["Session
Management"] CookieAuth["Cookie Authorization"] CSRF["CSRF Protection"] Proxy["Proxy to
External APIs"] LocalAPIs["Local APIs"] SessionStore[("Server-Side
Session Storage")] end IdP["Identity Provider"] ExternalAPIs["External APIs"] SPA -->|"login / logout"| AuthEndpoints AuthEndpoints -->|"Set-Cookie"| CookieJar CookieJar -->|"Auth cookie"| CookieAuth AuthEndpoints <-->|"redirect"| IdP AuthEndpoints --> SessionMgmt SessionMgmt --> SessionStore CookieAuth --> CSRF CSRF --> Proxy CSRF --> LocalAPIs Proxy -->|"Bearer token"| ExternalAPIs SessionMgmt -->|"Acquire tokens"| IdP ExternalAPIs -->|"Validate tokens"| IdP ``` The BFF sits between the browser and everything else. The browser only ever holds a **session cookie** — it never sees tokens. The BFF exchanges that cookie for bearer tokens when forwarding requests to downstream APIs. ## Architectural Decisions [Section titled “Architectural Decisions”](#architectural-decisions) ### Decision 1: Where Does Your UI Live? [Section titled “Decision 1: Where Does Your UI Live?”](#decision-1-where-does-your-ui-live) The simplest setup hosts both the UI assets and the BFF from the **same origin**. This makes cookies same-site, eliminates CORS, and avoids [third-party cookie blocking](/bff/architecture/third-party-cookies/). You can also run the frontend on a **separate origin** (e.g. a Vite dev server, a CDN) and point it at the BFF via CORS. This is more complex but enables independent deployment. [UI Hosting ](/bff/architecture/ui-hosting/)Full comparison of same-origin vs. separate-origin hosting ### Decision 2: Cookie-Only vs. Server-Side Sessions [Section titled “Decision 2: Cookie-Only vs. Server-Side Sessions”](#decision-2-cookie-only-vs-server-side-sessions) By default, the BFF stores the entire session in the cookie. This is simple and stateless but has limits: cookie size, no server-side revocation. With **server-side sessions**, the cookie holds only a session ID. The server stores the session state (typically in a database or distributed cache). This enables: * Forced logout across all sessions * Back-channel logout from the identity provider * Querying active sessions [Server-Side Sessions ](/bff/fundamentals/session/server-side-sessions/)Configure server-side session storage for scalability and revocation ### Decision 3: How Do You Expose APIs? [Section titled “Decision 3: How Do You Expose APIs?”](#decision-3-how-do-you-expose-apis) | API Pattern | When to Use | | ----------------------- | ------------------------------------------------------------------------------------------------ | | **Local API** | Business logic hosted inside the BFF process itself. Lowest latency, no token forwarding needed. | | **Remote API (direct)** | External microservice. BFF forwards the request with a bearer token attached. | | **Remote API (YARP)** | External microservice with complex routing rules. BFF uses YARP as the reverse proxy. | [API Types ](/bff/fundamentals/apis/)Decision flowchart for choosing the right API pattern ### Decision 4: Single Frontend vs. Multi-Frontend [Section titled “Decision 4: Single Frontend vs. Multi-Frontend”](#decision-4-single-frontend-vs-multi-frontend) Each BFF instance is tied to **one** browser-based application and **one** OIDC client registration. If you have multiple frontends (e.g. a customer portal and an admin app), run separate BFF instances with separate client IDs. They can share infrastructure (same process, different routes) but should not share session state or token storage. [Common Configurations ](/bff/fundamentals/options/#common-configurations)Multi-frontend configuration example ### Decision 5: Blazor or JavaScript? [Section titled “Decision 5: Blazor or JavaScript?”](#decision-5-blazor-or-javascript) Both are supported, but have different integration patterns: * **JavaScript SPAs** interact with the BFF via `/bff/user`, `/bff/login`, `/bff/logout`, and API endpoints * **Blazor** uses built-in `AuthenticationStateProvider` integration and can call APIs server-side (no token forwarding from browser) [Blazor Fundamentals ](/bff/fundamentals/blazor/)Blazor-specific guidance for rendering modes, data access, and auth state ## Trust Boundaries [Section titled “Trust Boundaries”](#trust-boundaries) ``` flowchart TD subgraph Browser["Browser (untrusted)"] B1["Holds session cookie only
(HttpOnly, Secure, SameSite)"] B2["Never sees access or refresh tokens"] end subgraph BFF["BFF Host (trusted server)"] BFF1["Validates session cookie on every request"] BFF2["Manages access/refresh tokens in server memory or DB"] BFF3["Enforces anti-forgery (X-CSRF header) on API routes"] end subgraph IdP["Identity Provider
(e.g. IdentityServer)"] end subgraph APIs["Downstream APIs
(microservices, external)"] end Browser -->|"HTTPS + Cookie"| BFF BFF -->|"OIDC/OAuth (HTTPS)"| IdP BFF -->|"Bearer token (HTTPS)"| APIs ``` The critical security property: **tokens never cross the trust boundary into the browser**. All token operations happen server-to-server. ## Internals [Section titled “Internals”](#internals) Duende.BFF is built on top of: | Component | Role | Details | | ---------------------------- | ------------------------------------------------------ | ---------------------------------------------------- | | ASP.NET OIDC handler | Protocol processing (auth code + PKCE, token exchange) | Standard ASP.NET middleware | | ASP.NET Cookie handler | Session management and cookie issuance | Extended by BFF for server-side sessions | | Duende.AccessTokenManagement | Token storage, refresh, revocation | [Docs](/accesstokenmanagement/) | | YARP | Reverse proxy for remote APIs | [BFF YARP integration](/bff/fundamentals/apis/yarp/) | ## See Also [Section titled “See Also”](#see-also) [IdentityServer Client Configuration ](/identityserver/fundamentals/clients/)Register your BFF as a confidential OIDC client [Third-Party Cookies ](/bff/architecture/third-party-cookies/)How browser cookie restrictions affect BFF architecture [UI Hosting ](/bff/architecture/ui-hosting/)Options for hosting the frontend alongside the BFF [Middleware Pipeline ](/bff/fundamentals/middleware-pipeline/)Canonical middleware order reference ----- # Multi-frontend support > Overview on what BFF multi-frontend support is, how it works and why you would use it. BFF V4.0 introduces the capability to support multiple BFF Frontends in a single host. This helps to simplify your application landscape by consolidating multiple physical BFF Hosts into a single deployable unit. A single BFF setup consists of: 1. A browser based application, typically built using technology like React, Angular or VueJS. This is typically deployed to a Content Delivery Network (CDN). 2. A BFF host, that will take care of the OpenID Connect login flows. 3. An API surface, exposed and protected by the BFF. With the BFF Multi-frontend support, you can logically host multiple of these BFF Setups in a single host. The concept of a single frontend (with OpenID Connect configuration, an API surface and a browser based app) is now codified inside the BFF. By using a flexible frontend selection mechanism (using Hosts or Paths to distinguish), it’s possible to create very flexible setups. The BFF dynamically configures the aspnet core authentication pipeline according to recommended practices. For example, when doing Host based routing, it will configure the cookies using the most secure settings and with the prefix [`__Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie). Frontends can be added or removed dynamically from the system, without having to restart the system. You can do this via configuration (for example by modifying a configuration file) or programmatically. ## A Typical Example [Section titled “A Typical Example”](#a-typical-example) Consider an enterprise that hosts multiple browser based applications. Each of these applications is developed by a separate team and as such, has its own deployment schedule. There are some internal-facing applications that are exclusively used by internal employees. These internal employees are all present in Microsoft Entra ID, so these internal-facing applications should directly authenticate against Microsoft Entra ID. These applications also use several internal APIs, that due to the sensitivity, should not be accessible by external users. However, they also use some of the more common APIs. These apps are only accessible via an internal DNS name, such as `https://app1.internal.example.com`. There are also several public facing applications, that are used directly by customers. These users should be able to log in using their own identity, via providers like Google, Twitter, or others. This authentication process is handled by Duende IdentityServer. There is constant development ongoing on these applications and it’s not uncommon for new applications to be introduced. There should be single sign-on across all these public facing applications. They are all available on the same domain name, but use path based routing to distinguish themselves, such as `https://app.example.com/app1` There is also a partner portal. This partner portal can only be accessed by employees of the partners. Each partner should be able to bring their own identity provider. This is implemented using the [Dynamic Providers](/identityserver/ui/login/dynamicproviders/) feature of Duende IdentityServer. This setup, with multiple frontends, each having different authentication requirements and different API surfaces, is now supported by the BFF. Each frontend can either rely on the global configuration or override (parts of) this configuration, such as the identity provider or the Client ID and Client Secret to use. It’s also possible to dynamically add or remove frontends, without restarting the BFF host. ## Internals [Section titled “Internals”](#internals) BFF V4 still allows you to manually configure the ASP.NET Core authentication options, by calling `.AddAuthentication().AddOpenIdConnect().AddCookies()`. However, if you wish to use the multi-frontend features, then this setup needs to become dynamic. To achieve this, the BFF automatically configures the ASP.NET Core pipeline: ``` --- title: BFF Middleware Pipeline --- flowchart TD A["FrontendSelectionMiddleware"] --> B["PathMappingMiddleware"] B --> C["OpenIdCallbackMiddleware"] C --> D["Your ASP.NET Core Pipeline"]:::app D --> E["MapRemoteRoutesMiddleware"] E --> F["ProxyIndexMiddleware"] ``` 1. `FrontendSelectionMiddleware` - This middleware performs the frontend selection by seeing which frontend’s selection criteria best matches the incoming request route. It’s possible to mix both path based routing and host based routing, so the most specific will be selected. 2. `PathMappingMiddleware` - If you use path mapping, in the selected frontend, then it will automatically map the frontend’s path so none of the subsequent middlewares know (or need to care) about this fact. 3. `OpenIdCallbackMiddleware` - To dynamically perform the OpenID Connect authentication without explicitly adding each frontend as a scheme, we inject a middleware that will handle the OpenID Connect callbacks. This only kicks in for dynamic frontends. 4. Your own applications logic is executed in this part of the pipeline. For example, calling `.UseAuthentication(), .UseRequestLogging()`, etc. After your application’s logic is executed, there are two middlewares registered as fallback routes: 5. `MapRemoteRoutesMiddleware` - This will handle any configured remote routes. Note, it will not handle plain YARP calls, only routes that are specifically added to a frontend. 6. `ProxyIndexMiddleware` - If configured, this proxies the `index.html` to start the browser based app. If you don’t want this automatic mapping of BFF middleware, you can turn it off using `BffOptions.AutomaticallyRegisterBffMiddleware`. When doing so, you’ll need to manually register and add the middlewares: ```csharp var app = builder.Build(); app.UseBffPreProcessing(); // TODO: your custom middleware goes here app.UseRouting(); app.UseBff(); app.UseBffPostProcessing(); app.Run(); ``` ## Authentication Architecture [Section titled “Authentication Architecture”](#authentication-architecture) When you use multiple frontends, you can’t rely on [manual authentication configuration](/bff/fundamentals/session/handlers/#manually-configuring-authentication). This is because each frontend requires its own scheme, and potentially its own OpenID Connect and Cookie configuration. The BFF registers a dynamic authentication scheme, which automatically configures the OpenID Connect and Cookie Scheme’s on behalf of the frontends. It does this using a custom `AuthenticationSchemeProvider` called `BffAuthenticationSchemeProvider` to return appropriate authentication schemes for each frontend. The BFF will register two schemes: * `duende-bff-oidc` * `duende-bff-cookie` Then, if there are no default authentication schemes registered, it will register ‘duende\_bff\_cookie’ schemes as the `AuthenticationOptions.DefaultScheme`, and ‘duende\_bff\_oidc’ as the `AuthenticationOptions.DefaultAuthenticateScheme` and `AuthenticationOptions.DefaultSignOutScheme`. This will ensure that calls to `Authenticate()` or `Signout()` will use the appropriate schemes. If you’re using multiple frontends, then the BFF will create dynamic schemes with the following signature: `duende_bff_oidc_[frontendname]` and `duende_bff_cookie_[frontendname]`. This ensures that every frontend can use its own OpenID Connect and Cookie settings. ----- # Third Party Cookies > Learn about the impact of third-party cookie blocking on OIDC flows and how the BFF pattern addresses these challenges If the BFF and OpenID Connect Provider (OP) are hosted on different [sites](https://developer.mozilla.org/en-US/docs/Glossary/Site), then some browsers will block cookies from being sent during navigation between those sites. Almost all browsers have the option of blocking third party cookies. Safari and Firefox are the most widely used browsers that do so by default, while Chrome is planning to do so in the future. This change is being made to protect user privacy, but it also impacts OIDC flows traditionally used by SPAs. A couple of particularly notable OIDC flows that don’t work for SPAs when third party cookies are blocked are [OIDC Session Management](https://openid.net/specs/openid-connect-session-1_0.html) and [OIDC Silent Login via the prompt=none parameter](https://openid.net/specs/openid-connect-core-1_0.html#authrequest). ## Session Management [Section titled “Session Management”](#session-management) OIDC Session Management allows a client SPA to monitor the session at the OP by reading a cookie from the OP in a hidden iframe. If third party cookie blocking prevents the iframe from seeing that cookie, the SPA will not be able to monitor the session. The BFF solves this problem using [OIDC back-channel logout](/bff/fundamentals/session/management/back-channel-logout/). The BFF is able to operate server side, and is therefore able to have a back channel to the OP. When the session ends at the OP, it can send a back-channel message to the BFF, ending the session at the BFF. ## Silent Login [Section titled “Silent Login”](#silent-login) OIDC Silent Login allows a client application to start its session without needing any user interaction if the OP has an ongoing session. The main benefit is that a SPA can load in the browser and then start a session without navigating away from the SPA for an OIDC flow, preventing the need to reload the SPA. Similarly to OIDC Session Management, OIDC Silent Login relies on a hidden iframe, though in this case, the hidden iframe makes requests to the OP, passing the *prompt=none* parameter to indicate that user interaction isn’t sensible. If that request includes the OP’s session cookie, the OP can respond successfully and the application can obtain tokens. But if the request does not include a session - either because no session has been started or because the cookie has been blocked - then the silent login will fail, and the user will have to be redirected to the OP for an interactive login. ### BFF With A Federation Gateway [Section titled “BFF With A Federation Gateway”](#bff-with-a-federation-gateway) The BFF supports silent login from the SPA with the /bff/silent-login [endpoint](/bff/fundamentals/session/management/silent-login/). This endpoint is intended to be invoked in an iframe and issues a challenge to login non-interactively with *prompt=none*. Just as in a traditional SPA, this technique will be disrupted by third party cookie blocking when the BFF and OP are third parties. If you need silent login with a third party OP, we recommend that you use the [Federation Gateway](/identityserver/ui/federation/) pattern. In the federation gateway pattern, one identity provider (the gateway) federates with other remote identity providers. Because the client applications only interact with the gateway, the implementation details of the remote identity providers are abstracted. In this case, we shield the client application from the fact that the remote identity provider is a third party by hosting the gateway as a first party to the client. This makes the client application’s requests for silent login always first party. ### Alternatives [Section titled “Alternatives”](#alternatives) Alternatively, you can accomplish a similar goal (logging in without needing to initially load the SPA, only to redirect away from it) by detecting that the user is not authenticated in the BFF and issuing a challenge before the site is ever loaded. This approach is not typically our first recommendation, because it makes allowing anonymous access to parts of the UI difficult and because it requires *samesite=lax* cookies (see below). ----- # UI Hosting > A guide exploring different UI hosting strategies and their benefits when using Backend For Frontend (BFF) systems When building modern web applications, selecting the right hosting strategy for your UI assets is crucial for optimizing performance, simplifying deployment, and ensuring seamless integration with Backend For Frontend (BFF) systems. This guide explores various hosting approaches and their benefits. ## Hosting Options for the UI [Section titled “Hosting Options for the UI”](#hosting-options-for-the-ui) There are several options for hosting the UI assets when using a BFF. * Host the assets within the BFF host using the static file middleware * Host the UI and BFF separately on subdomains of the same site and use CORS to allow cross-origin requests * Serves the index page of the UI from the BFF host, and all other assets are loaded from another domain, such as a CDN ### Serving SPA assets from BFF host [Section titled “Serving SPA assets from BFF host”](#serving-spa-assets-from-bff-host) Hosting the UI together with the BFF is the simplest choice, as requests from the front end to the backend will automatically include the authentication cookie and not require CORS headers. This makes the BFF and the front-end application a single deployable unit. Below shows a graphical overview of what that would look like: ``` flowchart LR subgraph Browser["Browser: https://application.url"] app["app"] end subgraph BFF["BFF Application"] endpoints["BFF endpoints
local / remote API endpoints"] static["Static files middleware"] end subgraph FS["Local Filesystem"] index["index.html"] scripts["script_assets.js"] images["images"] end app -->|"cookie"| endpoints app --> static static --> FS ``` If you create a BFF host using our templates, the UI will be hosted in this way: Terminal ```bash dotnet new duende-bff-remoteapi # or dotnet new duende-bff-localapi ``` Many frontend applications require a build process, which complicates the use of the static file middleware at development time. Visual Studio includes SPA templates that start up a SPA and proxy requests to it during development. Samples of Duende.BFF that take this approach using [React](/bff/samples#reactjs-frontend) and [Angular](/bff/samples#angular-frontend) are available. Microsoft’s templates are easy-to-use at dev time from Visual Studio. They allow you to run the solution, and the template proxies requests to the front end for you. At deploy time, that proxy is removed and the static assets of the site are served by the static file middleware. ### Host The UI Separately [Section titled “Host The UI Separately”](#host-the-ui-separately) You may want to host the UI outside the BFF. At development time, UI developers might prefer to run the frontend outside of Visual Studio (e.g., using the node cli). You might also want to have separate deployments of the frontend and the BFF, and you might want your static UI assets hosted on a CDN. Below is a schematic overview of what that would look like: ``` flowchart LR subgraph Browser["Browser: https://application.url"] app["app"] end subgraph BFF["BFF Application (https://bff.url)"] endpoints["BFF endpoints
local / remote API endpoints"] end subgraph CDN["CDN"] index["index.html"] scripts["script_assets.js"] images["images"] end app -->|"cookie + CORS"| endpoints app -->|"load assets"| CDN ``` The browser accesses the application via the BFF. The BFF proxies the calls to index.html to the CDN. The browser can then download all static assets from the CDN, but then use the BFF (and it’s API’s and user management API’s) secured by the authentication cookie as normal. Effectively, this turns your front-end and BFF Host into two separately deployable units. You’ll need to ensure that the two components are hosted on subdomains of the same domain so that [third party cookie blocking](/bff/architecture/third-party-cookies/) doesn’t prevent the frontend from including cookies in its requests to the BFF host. In order for this architecture to work, the following things are needed: * To make sure that client side routing works, there should be a catch-all route configured that proxies calls to the index.html. Once the index.html is served, the front-end will take over the application specific routing. * The API’s hosted by the BFF and the applications API’s should be excluded from this catch-all routing. However, they should not be visited by the browser directly. * The CDN needs to be configured to allow CORS requests from the application’s origin. * In order to include the auth cookie in those requests, the frontend code will have to [declare that it should send credentials](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included) using the *credentials: “include”* option. A sample of this approach is [available](/bff/samples#separate-host-for-ui). ### Serve The Index Page From The BFF Host [Section titled “Serve The Index Page From The BFF Host”](#serve-the-index-page-from-the-bff-host) Lastly, you could serve the index page of the SPA from the BFF, but have all the other static assets hosted on another host (presumably a CDN). This technique makes the UI and BFF have exactly the same origin, so the authentication cookie will be sent from the frontend to the BFF automatically, and third party cookie blocking and the SameSite cookie attribute won’t present any problems. The following diagram shows how that would work: ``` flowchart LR subgraph Browser["Browser: https://application.url"] app["app"] end subgraph BFF["BFF Application"] endpoints["BFF endpoints
local / remote API endpoints"] proxy["proxy"] end subgraph CDN["CDN (https://the.cdn)"] index["index.html"] scripts["script_assets.js"] images["images"] end app -->|"cookie"| endpoints app -->|"initial request"| proxy proxy -->|"proxy index.html"| CDN app -->|"load assets"| CDN ``` Setting this up for local development takes a bit of effort, however. As you make changes to the frontend, the UI’s build process might generate a change to the index page. If it does, you’ll need to arrange for the index page being served by the BFF host to reflect that change. Additionally, the front end will need to be configurable so that it is able to load its assets from other hosts. The mechanism for doing so will vary depending on the technology used to build the frontend. For instance, Angular includes a number of [deployment options](https://angular.io/guide/deployment) that allow you to control where it expects to find assets. The added complexity of this technique is justified when there is a requirement to host the front end on a different site (typically a CDN) from the BFF. ----- # Diagnostics > Overview of Duende Backend for Frontend (BFF) diagnostic capabilities including logging and OpenTelemetry integration to assist with monitoring and troubleshooting ## Logging [Section titled “Logging”](#logging) Duende Backend for Frontend (BFF) offers several diagnostics possibilities. It uses the standard logging facilities provided by ASP.NET Core, so you don’t need to do any extra configuration to benefit from rich logging functionality, including support for multiple logging providers. See the Microsoft [documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging) for a good introduction on logging. BFF follows the standard logging levels defined by the .NET logging framework, and uses the Microsoft guidelines for when certain log levels are used. For general information on how to configure logging in Duende products, see our [Logging Fundamentals](/general/logging/) guide. ### Configuration [Section titled “Configuration”](#configuration) Logs are typically written under the `Duende.Bff` category, with more concrete categories for specific components. To get detailed logs from the BFF middleware with the `Microsoft.Extensions.Logging` framework, you can configure your `appsettings.json` to enable `Debug` level logs for the `Duende.Bff` namespace: appsettings.json ```json { "Logging": { "LogLevel": { "Default": "Information", "Duende.Bff": "Debug" } } } ``` ## OpenTelemetry v4.0 [Section titled “OpenTelemetry ”v4.0](#opentelemetry) OpenTelemetry provides a single standard for collecting and exporting telemetry data, such as metrics, logs, and traces. To start emitting OpenTelemetry data in Duende Backend for Frontend (BFF), you need to: * add the OpenTelemetry libraries to your BFF host and client applications * start collecting traces and metrics from the various BFF sources (and other sources such as ASP.NET Core, the `HttpClient`, etc.) The following configuration adds the OpenTelemetry configuration to your service setup, and exports data to an [OTLP exporter](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-with-otel): Program.cs ```csharp var openTelemetry = builder.Services.AddOpenTelemetry(); openTelemetry.ConfigureResource(r => r .AddService(builder.Environment.ApplicationName)); openTelemetry.WithMetrics(metrics => { metrics.AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddRuntimeInstrumentation() .AddMeter(BffMetrics.MeterName); }); openTelemetry.WithTracing(tracing => { tracing.AddSource(builder.Environment.ApplicationName) .AddAspNetCoreInstrumentation() // Uncomment the following line to enable gRPC instrumentation // (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) //.AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); }); openTelemetry.UseOtlpExporter(); ``` ## Metrics [Section titled “Metrics”](#metrics) OpenTelemetry metrics are run-time measurements typically used to show graphs on a dashboard, to inspect overall application health, or to set up monitoring rules. The BFF host emits metrics collected through the `Duende.Bff` meter (meter name: `BffMetrics.MeterName`). Add it to your OpenTelemetry configuration with `.AddMeter(BffMetrics.MeterName)`. ### Session Metrics [Section titled “Session Metrics”](#session-metrics) | Metric Name | Type | Description | | ----------------- | ------- | --------------------------------------------------------------- | | `session.started` | Counter | Number of new sessions started (user logins) | | `session.ended` | Counter | Number of sessions ended (logouts, expiry, back-channel logout) | These counters can be used to track login/logout rates and detect unusual session activity (e.g., a spike in `session.ended` could indicate a back-channel logout sweep). ### Example: Prometheus Query [Section titled “Example: Prometheus Query”](#example-prometheus-query) If you are exporting metrics to Prometheus, the following PromQL queries can be useful: ```promql # Login rate over 5 minutes rate(session_started_total[5m]) # Logout rate over 5 minutes rate(session_ended_total[5m]) # Ratio of logouts to logins (high ratio may indicate session problems) rate(session_ended_total[5m]) / rate(session_started_total[5m]) ``` ## Distributed Tracing [Section titled “Distributed Tracing”](#distributed-tracing) BFF participates in distributed tracing via ASP.NET Core’s standard `ActivitySource` integration. When you configure `AddAspNetCoreInstrumentation()` and `AddHttpClientInstrumentation()` in your OpenTelemetry setup, the following BFF operations will appear as spans in your traces: * Incoming requests to BFF management endpoints (`/bff/login`, `/bff/logout`, `/bff/user`, etc.) * Outgoing HTTP requests made by the BFF when proxying to remote APIs * Token refresh calls to the identity provider (via `Duende.AccessTokenManagement`) ### Complete OpenTelemetry Setup [Section titled “Complete OpenTelemetry Setup”](#complete-opentelemetry-setup) Program.cs ```csharp var openTelemetry = builder.Services.AddOpenTelemetry(); openTelemetry.ConfigureResource(r => r .AddService(builder.Environment.ApplicationName)); openTelemetry.WithMetrics(metrics => { metrics .AddAspNetCoreInstrumentation() // HTTP request metrics .AddHttpClientInstrumentation() // Outgoing HTTP call metrics .AddRuntimeInstrumentation() // .NET runtime metrics (GC, threadpool, etc.) .AddMeter(BffMetrics.MeterName); // BFF-specific session metrics }); openTelemetry.WithTracing(tracing => { tracing .AddSource(builder.Environment.ApplicationName) .AddAspNetCoreInstrumentation() // Trace incoming requests .AddHttpClientInstrumentation(); // Trace outgoing HTTP calls (token refresh, proxied API calls) }); // Export to an OTLP-compatible backend (Jaeger, Zipkin, Grafana Tempo, etc.) openTelemetry.UseOtlpExporter(); ``` ### Multi-Frontend Tracing [Section titled “Multi-Frontend Tracing”](#multi-frontend-tracing) When using multiple frontends, log messages and traces include a `frontend` scope property identifying which frontend the activity belongs to. This allows you to filter traces by frontend in your observability backend. ## Diagnostics Endpoint [Section titled “Diagnostics Endpoint”](#diagnostics-endpoint) The BFF also includes a `/bff/diagnostics` endpoint for development-time troubleshooting. It returns the current user and client access tokens. See [Diagnostics Endpoint](/bff/fundamentals/session/management/diagnostics/) for details. ## See Also [Section titled “See Also”](#see-also) [Logging Fundamentals ](/general/logging/)General Duende logging configuration [OpenTelemetry .NET Documentation ](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-with-otel)Setting up OTLP export [Troubleshooting ](/bff/troubleshooting/)Common BFF issues and their solutions ----- # BFF Extensibility > Overview of all extensibility points in Duende.BFF for customizing session management, management endpoints, HTTP forwarding, and token storage. Duende.BFF is designed to be extended at multiple layers. Most production applications will use the defaults, but each area has well-defined extension points for when you need to go beyond the defaults. ## Extensibility Points [Section titled “Extensibility Points”](#extensibility-points) | Area | What You Can Customize | Detail Page | | ------------------------ | ----------------------------------------------------------------------------------- | ---------------------------------------------------- | | **Management Endpoints** | Login, logout, user info, back-channel logout, diagnostics, silent login processing | [Management Endpoints](#management-endpoints) | | **Session Store** | Where server-side session data is persisted (custom database, cache, etc.) | [Session Management](/bff/extensibility/sessions/) | | **HTTP Forwarder** | Custom HTTP clients, request/response transformations for proxied calls | [HTTP Forwarder](/bff/extensibility/http-forwarder/) | | **Token Management** | Token storage backend, per-route token retrieval (delegation, impersonation) | [Token Management](/bff/extensibility/tokens/) | ## Management Endpoints [Section titled “Management Endpoints”](#management-endpoints) Each BFF management endpoint has a corresponding interface that you can implement to customize its behavior. In v4, the pattern is to map a custom route at the same path and call the default endpoint implementation, allowing you to add logic before and after default processing. | Endpoint | Default Path | Interface (v4) | Interface (v3) | Detail | | ------------------- | ------------------- | ---------------------------- | --------------------------- | --------------------------------------------------------------------------------------- | | Login | `/bff/login` | `ILoginEndpoint` | `ILoginService` | [Login Extensibility](/bff/extensibility/management/login/) | | Logout | `/bff/logout` | `ILogoutEndpoint` | `ILogoutService` | [Logout Extensibility](/bff/extensibility/management/logout/) | | User | `/bff/user` | `IUserEndpoint` | `IUserService` | [User Extensibility](/bff/extensibility/management/user/) | | Silent Login | `/bff/silent-login` | `ISilentLoginEndpoint` | `ISilentLoginService` | [Silent Login Extensibility](/bff/extensibility/management/silent-login/) | | Back-Channel Logout | `/bff/backchannel` | `IBackchannelLogoutEndpoint` | `IBackchannelLogoutService` | [Back-Channel Logout Extensibility](/bff/extensibility/management/back-channel-logout/) | | Diagnostics | `/bff/diagnostics` | `IDiagnosticsEndpoint` | `IDiagnosticsService` | [Diagnostics Extensibility](/bff/extensibility/management/diagnostics/) | ### General Pattern (v4) [Section titled “General Pattern (v4)”](#general-pattern-v4) All management endpoint customizations in v4 follow the same pattern: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.LoginPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before the default processing var endpoint = context.RequestServices.GetRequiredService(); await endpoint.ProcessRequestAsync(context, ct); // Custom logic after the default processing }); ``` ## Session Store [Section titled “Session Store”](#session-store) By default, BFF uses either an in-memory store or Entity Framework Core for server-side sessions. To use a different storage backend (Redis, custom database, etc.), implement `IUserSessionStore`: ```csharp builder.Services.AddBff() .AddServerSideSessions(); ``` See [Session Management Extensibility](/bff/extensibility/sessions/) for the full interface and implementation guidance. ## HTTP Forwarder [Section titled “HTTP Forwarder”](#http-forwarder) When using `MapRemoteBffApiEndpoint`, BFF uses a default HTTP client and a default set of request/response transformations. You can customize: * **The HTTP client** — implement `IForwarderHttpClientFactory` to use a proxy, custom certificates, etc. * **Request/response transformations** — add custom headers, modify paths, or replace the default transformer entirely. See [HTTP Forwarder Extensibility](/bff/extensibility/http-forwarder/) for details. ## Token Management [Section titled “Token Management”](#token-management) BFF’s token management (powered by `Duende.AccessTokenManagement`) can be extended in two ways: * **Custom token store** — implement `IUserTokenStore` to store tokens outside of the session cookie or server-side session. * **Per-route token retrieval** — implement `IAccessTokenRetriever` for scenarios like token exchange or impersonation, where different API routes need different tokens. ```csharp app.MapRemoteBffApiEndpoint("/api/delegated", new Uri("https://api.example.com")) .WithAccessToken(RequiredTokenType.User) .WithAccessTokenRetriever(); ``` See [Token Management Extensibility](/bff/extensibility/tokens/) for details. ## See Also [Section titled “See Also”](#see-also) [Configuration Options ](/bff/fundamentals/options/)Settings that control BFF behavior without custom code [Troubleshooting ](/bff/troubleshooting/)Common issues and their solutions ----- # HTTP Forwarder > Learn how to customize the HTTP forwarding behavior in BFF by providing custom HTTP clients and request/response transformations You can customize the HTTP forwarder behavior in two ways * provide a customized HTTP client for outgoing calls * provide custom request/response transformation ## Custom HTTP Clients [Section titled “Custom HTTP Clients”](#custom-http-clients) By default, Duende.BFF will create and cache an HTTP client per configured route or local path. This invoker is set up like this: ```csharp var client = new HttpMessageInvoker(new SocketsHttpHandler { UseProxy = false, AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false }); ``` If you want to customize the HTTP client you can implement the `IForwarderHttpClientFactory` interface (from YARP’s `Yarp.ReverseProxy.Forwarder` namespace), e.g.: ```csharp public class MyInvokerFactory : IForwarderHttpClientFactory { public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context) { return new HttpMessageInvoker(new SocketsHttpHandler { // this API needs a proxy UseProxy = true, Proxy = new WebProxy("https://myproxy"), AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false }); } } ``` …and override our registration: ```csharp services.AddSingleton(); ``` ## Custom Transformations When Using Direct Forwarding [Section titled “Custom Transformations When Using Direct Forwarding”](#custom-transformations-when-using-direct-forwarding) The method `MapRemoteBffApiEndpoint` uses default transformations that: * removes the cookie header from the forwarded request * removes local path from the forwarded request * adds the access token to the original request If you wish to change or extend this behavior, you can do this for a single mapped endpoint or for all mapped API endpoints. ### Changing The Transformer For A Single Mapped Endpoint [Section titled “Changing The Transformer For A Single Mapped Endpoint”](#changing-the-transformer-for-a-single-mapped-endpoint) This code block shows an example of how you can extend the default transformers with an additional custom transform. * Duende BFF v4 ```csharp app.MapRemoteBffApiEndpoint("/local", new Uri("https://target/"), context => { // If you want to extend the existing behavior, then you must call the default builder: DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken("/local", context); // You can also add custom transformers, such as this one that adds an additional header context.AddRequestHeader("custom", "with value"); }); ``` The default transform builder performs these transforms: ```csharp context.AddRequestHeaderRemove("Cookie"); context.AddPathRemovePrefix(pathMatch); context.AddBffAccessToken(pathMatch); ``` * Duende BFF v3 ```csharp app.MapRemoteBffApiEndpoint("/local", "https://target/", context => { // If you want to extend the existing behavior, then you must call the default builder: DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken("/local", context); // You can also add custom transformers, such as this one that adds an additional header context.AddRequestHeader("custom", "with value"); }); ``` The default transform builder performs these transforms: ```csharp context.AddRequestHeaderRemove("Cookie"); context.AddPathRemovePrefix(localPath); context.AddBffAccessToken(localPath); ``` For more information, also see the [YARP documentation on transforms](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/transforms?view=aspnetcore-9.0) ### Changing The Default Transformer [Section titled “Changing The Default Transformer”](#changing-the-default-transformer) You can change the default transformer builder delegate by registering one in the services collection: * Duende BFF v4 ```csharp BffYarpTransformBuilder builder = (pathMatch, context) => { // If you want to extend the existing behavior, then you must call the default builder: DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken(pathMatch, context); // You can also add custom transformers, such as this one that adds an additional header context.AddResponseHeader("added-by-custom-default-transform", "some-value"); }; services.AddSingleton(builder); ``` * Duende BFF v3 ```csharp BffYarpTransformBuilder builder = (localPath, context) => { // If you want to extend the existing behavior, then you must call the default builder: DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken(localPath, context); // You can also add custom transformers, such as this one that adds an additional header context.AddResponseHeader("added-by-custom-default-transform", "some-value"); }; services.AddSingleton(builder); ``` ## Changing The Forwarder Request Configuration [Section titled “Changing The Forwarder Request Configuration”](#changing-the-forwarder-request-configuration) You can also modify the forwarder request configuration, either globally or per mapped path. This can be useful if you want to tweak things like activity timeouts. ```csharp // Register a forwarder config globally: services.AddSingleton(new ForwarderRequestConfig() { ActivityTimeout = TimeSpan.FromMilliseconds(100) }); // Or modify one on a per mapped route basis: app.MapRemoteBffApiEndpoint("/local", new Uri("https://target/"), requestConfig: new ForwarderRequestConfig() { ActivityTimeout = TimeSpan.FromMilliseconds(100) }); ``` ----- # BFF Management Endpoints Extensibility The behavior of each [management endpoint](/bff/fundamentals/session/management) is defined in a service. When you add Duende.BFF to the service container, a default implementation for every management endpoint gets registered. You can add your own implementation by overriding the default after calling `AddBff()`. * V4 The following endpoints are registered in the service container: ```csharp // management endpoints builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); ``` The management endpoint services all inherit from the `IBffEndpoint`, which provides a general-purpose mechanism to add custom logic to the endpoints. IBffEndpoint.cs ```csharp public interface IBffEndpoint { Task ProcessRequestAsync(HttpContext context, CancellationToken ct = default); } ``` You can customize the behavior of the endpoints by implementing the appropriate interface. The [default implementations](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal) can serve as a starting point for your own implementation. If you want to extend the default behavior of a management endpoint, you can add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.LoginPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 ```csharp // management endpoints builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); ``` The management endpoint services all inherit from the `IBffEndpointService`, which provides a general-purpose mechanism to add custom logic to the endpoints. IBffEndpointService.cs ```csharp public interface IBffEndpointService { Task ProcessRequestAsync(HttpContext context); } ``` You can customize the behavior of the endpoints either by implementing the appropriate interface or by extending the default implementation of that interface. In many cases, extending the default implementation is preferred, as this allows you to keep most of the default behavior by calling the base `ProcessRequestAsync` from your derived class. Several of the default endpoint service implementations also define virtual methods that can be overridden to customize their behavior with more granularity. ----- # BFF Back-Channel Logout Endpoint Extensibility The back-channel logout endpoint has several extensibility points organized into two interfaces. The `IBackchannelLogoutEndpoint` is the top-level abstraction that processes requests to the endpoint. This service can be used to add custom request processing logic or to change how it validates incoming requests. When the back-channel logout endpoint receives a valid request, it revokes sessions using the `ISessionRevocationService`. Caution In BFF V3, the `IBackchannelLogoutEndpoint` interface is called `IBackchannelLogoutService` instead. ## Request Processing [Section titled “Request Processing”](#request-processing) * V4 You can customize the behavior of the back-channel logout endpoint by implementing the `ProcessRequestAsync` method of the `IBackchannelLogoutEndpoint` interface. The [default implementation](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal/DefaultBackchannelLogoutEndpoint.cs) can serve as a starting point for your own implementation. If you want to extend the default behavior of the back-channel logout endpoint, you can instead add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.BackChannelLogoutPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 `ProcessRequestAsync` is the top-level function called in the endpoint service `DefaultBackchannelLogoutService`, and can be used to add arbitrary logic to the endpoint. For example, you could take whatever actions you need before normal processing of the request like this: ```csharp public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) { // Custom logic here return base.ProcessRequestAsync(context); } ``` ## Session Revocation [Section titled “Session Revocation”](#session-revocation) The back-channel logout service will call the registered session revocation service to revoke the user session when it receives a valid logout token. To customize the revocation process, implement the `ISessionRevocationService`. ----- # BFF Diagnostics Endpoint Extensibility The BFF diagnostics endpoint can be customized by implementing the `IDiagnosticsEndpoint`. Caution In BFF V3, the `IDiagnosticsEndpoint` interface is called `IDiagnosticsService` instead. ## Request Processing [Section titled “Request Processing”](#request-processing) * V4 You can customize the behavior of the diagnostics endpoint by implementing the `ProcessRequestAsync` method of the `IDiagnosticsEndpoint` interface. The [default implementation](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal/DefaultDiagnosticsEndpoint.cs) can serve as a starting point for your own implementation. If you want to extend the default behavior of the diagnostics endpoint, you can instead add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.DiagnosticsPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 `ProcessRequestAsync` is the top-level function called in the endpoint service `DefaultDiagnosticsService`, and can be used to add arbitrary logic to the endpoint. For example, you could take whatever actions you need before normal processing of the request like this: ```csharp public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) { // Custom logic here return base.ProcessRequestAsync(context); } ``` ----- # BFF Login Endpoint Extensibility The BFF login endpoint has extensibility points in two interfaces. The `ILoginEndpoint` is the top-level abstraction that processes requests to the endpoint. This service can be used to add custom request processing logic. The `IReturnUrlValidator` ensures that the `returnUrl` parameter passed to the login endpoint is safe to use. Caution In BFF V3, the `ILoginEndpoint` interface is called `ILoginService` instead. ## Request Processing [Section titled “Request Processing”](#request-processing) * V4 You can customize the behavior of the login endpoint by implementing the `ProcessRequestAsync` method of the `ILoginEndpoint` interface. The [default implementation](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal/DefaultLoginEndpoint.cs) can serve as a starting point for your own implementation. If you want to extend the default behavior of the login endpoint, you can instead add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.LoginPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 `ProcessRequestAsync` is the top-level function called in the endpoint service `DefaultLoginService`, and can be used to add arbitrary logic to the endpoint. For example, you could take whatever actions you need before normal processing of the request like this: ```csharp public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) { // Custom logic here return base.ProcessRequestAsync(context); } ``` ## Return URL Validation [Section titled “Return URL Validation”](#return-url-validation) To prevent open redirector attacks, the `returnUrl` parameter to the login endpoint must be validated. You can customize this validation by implementing the `IReturnUrlValidator` interface. The default implementation enforces that return URLs are local. ----- # BFF Logout Endpoint Extensibility The BFF logout endpoint has extensibility points in two interfaces. The `ILogoutEndpoint` is the top-level abstraction that processes requests to the endpoint. This service can be used to add custom request processing logic. The `IReturnUrlValidator` ensures that the `returnUrl` parameter passed to the logout endpoint is safe to use. Caution In BFF V3, the `ILogoutEndpoint` interface is called `ILogoutService` instead. ## Request Processing [Section titled “Request Processing”](#request-processing) * V4 You can customize the behavior of the logout endpoint by implementing the `ProcessRequestAsync` method of the `ILogoutEndpoint` interface. The [default implementation](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal/DefaultLogoutEndpoint.cs) can serve as a starting point for your own implementation. If you want to extend the default behavior of the logout endpoint, you can instead add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.LogoutPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 `ProcessRequestAsync` is the top-level function called in the endpoint service `DefaultLogoutService`, and can be used to add arbitrary logic to the endpoint. For example, you could take whatever actions you need before normal processing of the request like this: ```csharp public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) { // Custom logic here return base.ProcessRequestAsync(context); } ``` ## Return URL Validation [Section titled “Return URL Validation”](#return-url-validation) To prevent open redirector attacks, the `returnUrl` parameter to the logout endpoint must be validated. You can customize this validation by implementing the `IReturnUrlValidator` interface. The default implementation enforces that return URLs are local. ----- # BFF Silent Login Endpoint Extensibility The BFF silent login endpoint can be customized by implementing the `ISilentLoginEndpoint`. Caution In BFF V3, the `ISilentLoginEndpoint` interface is called `ISilentLoginService` instead. Danger The silent login endpoint has been marked as obsolete in BFF V4 and will be removed in a future version. To handle silent login in the future, pass the `prompt=none` parameter on to the login endpoint instead. ## Request Processing [Section titled “Request Processing”](#request-processing) * V4 You can customize the behavior of the silent login endpoint by implementing the `ProcessRequestAsync` method of the `ISilentLoginEndpoint` interface. The [default implementation](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal/DefaultSilentLoginEndpoint.cs) can serve as a starting point for your own implementation. If you want to extend the default behavior of the silent login endpoint, you can instead add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.SilentLoginPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 `ProcessRequestAsync` is the top-level function called in the endpoint service `DefaultSilentLoginService`, and can be used to add arbitrary logic to the endpoint. For example, you could take whatever actions you need before normal processing of the request like this: ```csharp public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) { // Custom logic here return base.ProcessRequestAsync(context); } ``` ----- # BFF Silent Login Callback Extensibility The BFF silent login callback endpoint can be customized by implementing the `ISilentLoginCallbackEndpoint`. Caution In BFF V3, the `ISilentLoginCallbackEndpoint` interface is called `ISilentLoginCallbackService` instead. ## Request Processing [Section titled “Request Processing”](#request-processing) * V4 You can customize the behavior of the silent login callback endpoint by implementing the `ProcessRequestAsync` method of the `ISilentLoginCallbackEndpoint` interface. The [default implementation](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal/DefaultSilentLoginCallbackEndpoint.cs) can serve as a starting point for your own implementation. If you want to extend the default behavior of the silent login callback endpoint, you can instead add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.SilentLoginCallbackPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 `ProcessRequestAsync` is the top-level function called in the endpoint service `DefaultSilentLoginCallbackService`, and can be used to add arbitrary logic to the endpoint. For example, you could take whatever actions you need before normal processing of the request like this: ```csharp public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) { // Custom logic here return base.ProcessRequestAsync(context); } ``` ----- # BFF User Endpoint Extensibility The BFF user endpoint can be customized by implementing the `IUserEndpoint`. Caution In BFF V3, the `IUserEndpoint` interface is called `IUserService` instead. ## Request Processing [Section titled “Request Processing”](#request-processing) * V4 You can customize the behavior of the user endpoint by implementing the `ProcessRequestAsync` method of the `IUserEndpoint` interface. The [default implementation](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal/DefaultUserEndpoint.cs) can serve as a starting point for your own implementation. If you want to extend the default behavior of the user endpoint, you can instead add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.UserPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 `ProcessRequestAsync` is the top-level function called in the endpoint service `DefaultUserService`, and can be used to add arbitrary logic to the endpoint. For example, you could take whatever actions you need before normal processing of the request like this: ```csharp public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) { // Custom logic here return base.ProcessRequestAsync(context); } ``` ### Enriching User Claims [Section titled “Enriching User Claims”](#enriching-user-claims) There are several ways how you can enrich the claims for a specific user, depending on where the required data comes from. #### Claims Transformations [Section titled “Claims Transformations”](#claims-transformations) To enrich claims for a user, you can implement a custom `IClaimsTransformation`. Claims transformation executes as part of the authentication process. ```csharp services.AddScoped(); public class CustomClaimsTransformer : IClaimsTransformation { public Task TransformAsync(ClaimsPrincipal principal) { var identity = (ClaimsIdentity)principal.Identity; if (!identity.HasClaim(c => c.Type == "custom_claim")) { identity.AddClaim(new Claim("custom_claim", "your_value")); } return Task.FromResult(principal); } } ``` See the [Claims Transformation](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims?view=aspnetcore-9.0) topic in the ASP.NET Core documentation for more information. #### User Endpoint Claims Enricher v4.0 [Section titled “User Endpoint Claims Enricher ”v4.0](#user-endpoint-claims-enricher) User claims can be enriched by implementing the `IUserEndpointClaimsEnricher` interface. This interface is specific to the user endpoint and runs after authentication. Because this runs within the user endpoint request, you can access the current HTTP context to retrieve the user’s access token. We recommend using the [`GetUserAccessTokenAsync`](/accesstokenmanagement/web-apps/#http-context-extension-methods) extension method from `Duende.AccessTokenManagement.OpenIdConnect`, as it will automatically handle refreshing the token if it has expired. Program.cs ```csharp builder.Services.AddTransient(); ``` CustomUserEndpointClaimsEnricher.cs ```csharp using Duende.Bff; using Duende.Bff.Endpoints; using Duende.AccessTokenManagement.OpenIdConnect; using Microsoft.AspNetCore.Authentication; public class CustomUserEndpointClaimsEnricher : IUserEndpointClaimsEnricher { private readonly IHttpContextAccessor _httpContextAccessor; public CustomUserEndpointClaimsEnricher(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public async Task> EnrichClaimsAsync( AuthenticateResult authenticateResult, IReadOnlyList claims, CancellationToken ct = default) { var newClaims = claims.ToList(); // Get the access token using the extension method // This will automatically handle token refreshing if needed var token = await _httpContextAccessor.HttpContext.GetUserAccessTokenAsync(cancellationToken: ct); if (!string.IsNullOrEmpty(token.AccessToken)) { // Call external API using the access token // ... } // Add custom claims newClaims.Add(new ClaimRecord("custom_data", "some value")); return newClaims; } } ``` ----- # Session Management > Configure and implement custom server-side session storage and lifecycle management through IUserSessionStore interface Server-side sessions enable secure and efficient storage of session data, allowing flexibility through custom implementations of the `IUserSessionStore` interface. This ensures adaptability to various storage solutions tailored to your application’s needs. ## User Session Store [Section titled “User Session Store”](#user-session-store) If using the server-side sessions feature, you need a store for the session data. An Entity Framework Core based implementation of this store is provided. If you wish to use some other type of store, can implement the `IUserSessionStore` interface: * Duende BFF v4 ```csharp /// /// User session store /// public interface IUserSessionStore { /// /// Retrieves a user session /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task GetUserSessionAsync(UserSessionKey key, CancellationToken ct = default); /// /// Creates a user session /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task CreateUserSessionAsync(UserSession session, CancellationToken ct = default); /// /// Updates a user session /// /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task UpdateUserSessionAsync(UserSessionKey key, UserSessionUpdate session, CancellationToken ct = default); /// /// Deletes a user session /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task DeleteUserSessionAsync(UserSessionKey key, CancellationToken ct = default); /// /// Queries user sessions based on the filter. /// /// The partition key to use /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task> GetUserSessionsAsync(PartitionKey partitionKey, UserSessionsFilter filter, CancellationToken ct = default); /// /// Deletes user sessions based on the filter. /// /// The partition key /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task DeleteUserSessionsAsync(PartitionKey partitionKey, UserSessionsFilter filter, CancellationToken ct = default); } ``` Do not store `UserSession` directly Your `IUserSessionStore` implementation is expected to implement custom code to roundtrip the data from the user session to the underlying storage mechanism. You should not rely on existing serializers, such as `System.Text.Json` or `Newtonsoft.Json`, to serialize the `UserSession` object. * Duende BFF v3 ```csharp /// /// User session store /// public interface IUserSessionStore { /// /// Retrieves a user session /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task GetUserSessionAsync(string key, CancellationToken cancellationToken = default); /// /// Creates a user session /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task CreateUserSessionAsync(UserSession session, CancellationToken cancellationToken = default); /// /// Updates a user session /// /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task UpdateUserSessionAsync(string key, UserSessionUpdate session, CancellationToken cancellationToken = default); /// /// Deletes a user session /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task DeleteUserSessionAsync(string key, CancellationToken cancellationToken = default); /// /// Queries user sessions based on the filter. /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task> GetUserSessionsAsync(UserSessionsFilter filter, CancellationToken cancellationToken = default); /// /// Deletes user sessions based on the filter. /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task DeleteUserSessionsAsync(UserSessionsFilter filter, CancellationToken cancellationToken = default); } ``` Once you have an implementation, you can register it when you enable server-side sessions: Program.cs ```csharp builder.Services.AddBff() .AddServerSideSessions(); ``` ## User Session Store Cleanup [Section titled “User Session Store Cleanup”](#user-session-store-cleanup) The `IUserSessionStoreCleanup` interface is used to model cleaning up expired sessions. ```csharp /// /// User session store cleanup /// public interface IUserSessionStoreCleanup { /// /// Deletes expired sessions /// Task DeleteExpiredSessionsAsync(CancellationToken cancellationToken = default); } ``` ----- # Token Management > Learn how to customize token storage and management in the BFF framework, including HTTP client configuration and per-route token retrieval The token management library does essentially two things: * stores access and refresh tokens in the current session * refreshes access tokens automatically at the token service when needed Both aspects can be customized. ### Token service communication [Section titled “Token service communication”](#token-service-communication) The token management library uses a named HTTP client from the HTTP client factory for all token service communication. You can provide a customized HTTP client yourself using the well-known name after calling `AddBff`: ```csharp builder.Services.AddHttpClient( ClientCredentialsTokenManagementDefaults.BackChannelHttpClientName, configureClient => { // ... }); ``` ### Custom Token Storage [Section titled “Custom Token Storage”](#custom-token-storage) We recommend that you use the default storage mechanism, as this will automatically be compatible with the Duende.BFF server-side sessions. If you do not use server-side sessions, then the access and refresh token will be stored in the protected session cookie. If you want to change this, you can take over token storage completely. This would involve two steps * turn off the `SaveTokens` flag on the OpenID Connect handler and handle the relevant events manually to store the tokens in your custom store * implement and register the `Duende.AccessTokenManagement.IUserTokenStore` interface The interface is responsible to storing, retrieving and clearing tokens for the automatic token management: ```csharp public interface IUserTokenStore { /// /// Stores tokens /// /// User the tokens belong to /// /// Extra optional parameters /// Task StoreTokenAsync( ClaimsPrincipal user, UserToken token, UserTokenRequestParameters? parameters = null, CancellationToken token = default); /// /// Retrieves tokens from store /// /// User the tokens belong to /// Extra optional parameters /// access and refresh token and access token expiration Task GetTokenAsync( ClaimsPrincipal user, UserTokenRequestParameters? parameters = null, CancellationToken token = default); /// /// Clears the stored tokens for a given user /// /// User the tokens belong to /// Extra optional parameters /// Task ClearTokenAsync( ClaimsPrincipal user, UserTokenRequestParameters? parameters = null, CancellationToken token = default); } ``` ### Per-route Customized Token Retrieval [Section titled “Per-route Customized Token Retrieval”](#per-route-customized-token-retrieval) The token store defines how tokens are retrieved globally. However, you can add custom logic that changes the way that access tokens are retrieved on a per-route basis. For example, you might need to exchange a token to perform delegation or impersonation for some API calls, depending on the remote API. The interface that describes this extension point is the `IAccessTokenRetriever`. ```csharp /// /// Retrieves access tokens /// public interface IAccessTokenRetriever { /// /// Asynchronously gets the access token. /// /// Context used to retrieve the token. /// A task that contains the access token result, which is an /// object model that can represent various types of tokens (bearer, dpop), /// the absence of an optional token, or an error. Task GetAccessTokenAsync(AccessTokenRetrievalContext context, CancellationToken ct = default); } ``` You can implement this interface yourself or extend the `DefaultAccessTokenRetriever`. The `AccessTokenResult` class represents the result of this operation. It is an abstract class with concrete implementations that represent successfully retrieving a bearer token (`BearerTokenResult`), successfully retrieving a DPoP token (`DPoPTokenResult`), failing to find an optional token (`NoAccessTokenResult`), which is not an error, and failure to retrieve a token (`AccessTokenRetrievalError`). Your implementation of GetAccessTokenAsync should return one of those types. Implementations of the `IAccessTokenRetriever` can be added to endpoints when they are mapped using the `WithAccessTokenRetriever` extension method: ```csharp app.MapRemoteBffApiEndpoint( "/api/impersonation", new Uri("https://api.example.com/endpoint/requiring/impersonation") ).WithAccessToken(RequiredTokenType.User) .WithAccessTokenRetriever(); ``` The `GetAccessTokenAsync` method will be invoked on every call to APIs that use the access token retriever. If retrieving the token is an expensive operation, you may need to cache it. It is up to your retriever code to perform caching. ----- # Securing and Accessing API Endpoints > Learn about the different types of APIs in a BFF architecture and how to secure and access them properly A frontend application using the BFF pattern can call two types of APIs: embedded (local) APIs, and proxied remote APIs. ## Choosing an API Approach [Section titled “Choosing an API Approach”](#choosing-an-api-approach) ``` flowchart TD Q1{"Is the API only used
by this frontend?"} Q2{"Do you need load balancing,
service discovery, or
complex routing/transforms?"} Local["✅ Embedded (Local) API
Host the API inside the BFF itself"] Remote["✅ Remote API — Direct Forwarding
MapRemoteBffApiEndpoint()"] Yarp["✅ YARP Integration
Full YARP configuration with BFF extensions"] Q1 -->|Yes| Local Q1 -->|No| Q2 Q2 -->|Yes| Yarp Q2 -->|No| Remote ``` Use the table below for additional guidance on token requirements: | Scenario | Recommended approach | | ----------------------------------------------------------- | -------------------------------------------------- | | API is only used by this frontend | [Embedded (Local) API](local/) | | API is shared by multiple clients or deployed separately | [Remote API — Direct Forwarding](remote/) | | Complex routing, load balancing, or transforms are needed | [YARP](yarp/) | | API requires the logged-in user’s token | Remote or YARP with `RequiredTokenType.User` | | API uses machine-to-machine (client credentials) auth | Remote or YARP with `RequiredTokenType.Client` | | API is publicly accessible (no auth required) | Remote with `RequiredTokenType.None` | | API should use user token if logged in, anonymous otherwise | Remote or YARP with `RequiredTokenType.UserOrNone` | ## Embedded (Local) APIs [Section titled “Embedded (Local) APIs”](#embedded-local-apis) These APIs are embedded inside the BFF and typically exist to support the BFF’s frontend; they are not shared with other frontends or services. See [Embedded APIs](local/) for more information. ## Proxying Remote APIs [Section titled “Proxying Remote APIs”](#proxying-remote-apis) These APIs are deployed on a different host than the BFF, which allows them to be shared between multiple frontends or (more generally speaking) multiple clients. These APIs can only be called via the BFF host acting as a proxy. You can use [Direct Forwarding](remote/) for most scenarios. If you have more complex requirements, you can also directly interact with [YARP](yarp/). ## See Also [Section titled “See Also”](#see-also) * [Token Management](/bff/fundamentals/tokens/) — How BFF attaches access tokens to outgoing API calls * [Access Token Management](/accesstokenmanagement/) — The underlying token lifecycle library * [IdentityServer API Resources](/identityserver/fundamentals/resources/api-resources/) — Configuring scopes for your APIs ----- # Embedded (Local) APIs > Documentation about Embedded (Local) APIs in BFF, including self-contained APIs and those using managed access tokens, along with securing endpoints and configuration details. An *Embedded API* (or local API) is an API located within the BFF host. Embedded APIs are implemented with the familiar ASP.NET abstractions of API controllers or Minimal API endpoints. There are two styles of Embedded APIs: * Self-contained Embedded APIs * Embedded APIs that Make Requests using Managed Access Tokens #### Self-Contained Embedded APIs [Section titled “Self-Contained Embedded APIs”](#self-contained-embedded-apis) These APIs reside within the BFF and don’t make HTTP requests to other APIs. They access data controlled by the BFF itself, which can simplify the architecture of the system by reducing the number of APIs that must be deployed and managed. They are suitable for scenarios where the BFF is the sole consumer of the data. If you require data accessibility from other applications or services, this approach is probably not suitable. #### Embedded APIs That Make Requests Using Managed Access Tokens [Section titled “Embedded APIs That Make Requests Using Managed Access Tokens”](#embedded-apis-that-make-requests-using-managed-access-tokens) Alternatively, you can make the data available as a service and make HTTP requests to that service from your BFF’s Embedded endpoints. The benefits of this style of Embedded Endpoint include: * Your frontend’s network access can be simplified into an aggregated call for the specific data that it needs, which reduces the amount of data that must be sent to the client. * Your BFF endpoint can expose a subset of your remote APIs so that they are called in a more controlled manner than if the BFF proxied all requests to the endpoint. * Your BFF endpoint can include business logic to call the appropriate endpoints, which simplifies your front end code. Your Embedded endpoints can leverage services like the HTTP client factory and Duende.BFF [token management](/bff/fundamentals/tokens/) to make the outgoing calls. The following is a simplified example showing how Embedded endpoints can get managed access tokens and use them to make requests to remote APIs. Program.cs ```csharp app.MapGet("/myApi", async (IHttpClientFactory httpClientFactory, HttpContext context) => { var id = context.Request.Query["id"]; // create HTTP client var client = httpClientFactory.CreateClient(); // get current user access token and set it on HttpClient var token = await context.GetUserAccessTokenAsync(); client.SetBearerToken(token); // call remote API var response = await client.GetAsync($"https://remoteServer/remoteApi?id={id}"); // maybe process response and return to frontend return Results.Text(await response.Content.ReadAsStringAsync()); }); ``` The example above is simplified to demonstrate the way that you might obtain a token. Embedded endpoints will typically enforce constraints on the way the API is called, aggregate multiple calls, or perform other business logic. Embedded endpoints that merely forward requests from the frontend to the remote API may not be needed at all. Instead, you could proxy the requests through the BFF using either the [simple http forwarder](/bff/fundamentals/apis/remote/) or [YARP](/bff/fundamentals/apis/yarp/). ## Securing Embedded API Endpoints [Section titled “Securing Embedded API Endpoints”](#securing-embedded-api-endpoints) Regardless of the style of data access used by an Embedded API, it must be protected against threats such as [CSRF (Cross-Site Request Forgery)](https://developer.mozilla.org/en-US/docs/Glossary/CSRF) attacks. To defend against such attacks and ensure that only the frontend can access these endpoints, we recommend implementing two layers of protection. #### SameSite Cookies [Section titled “SameSite Cookies”](#samesite-cookies) [The SameSite cookie attribute](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value) is a feature of modern browsers that restricts cookies so that they are only sent to pages originating from the [site](https://developer.mozilla.org/en-US/docs/Glossary/Site) where the cookie was originally issued. This is a good first layer of defense, but makes the assumption that you can trust all subdomains of your site. All subdomains within a registrable domain are considered the same site for purposes of SameSite cookies. Thus, if another application hosted on a subdomain within your site is infected with malware, it can make CSRF attacks against your application. #### Anti-forgery Header [Section titled “Anti-forgery Header”](#anti-forgery-header) We recommend requiring an additional custom header on API endpoints, for example: ```plaintext GET /endpoint x-csrf: 1 ``` The value of the header is not important, but its presence, combined with the cookie requirement, triggers a CORS preflight request for cross-origin calls. This effectively isolates the caller to the same origin as the backend, providing a robust security guarantee. Additionally, API endpoints should handle scenarios where the session has expired or authorization fails without triggering an authentication redirect to the upstream identity provider. Instead, they should return Ajax-friendly status codes. ## Setup [Section titled “Setup”](#setup) ### Adding Anti-forgery Protection [Section titled “Adding Anti-forgery Protection”](#adding-anti-forgery-protection) Duende.BFF can automate the pre-processing step of requiring the custom anti-forgery header. To do so, first add the BFF middleware to the pipeline, and then decorate your endpoints to indicate that they should receive BFF pre-processing. 1. **Add Middleware to the pipeline** Add the BFF middleware to the pipeline by calling `UseBff`. Note that the middleware must be placed before the authorization middleware, but after routing. Program.cs ```csharp app.UseAuthentication(); app.UseRouting(); app.UseBff(); app.UseAuthorization(); // map endpoints ``` 2. **Decorate Endpoints** Endpoints that require the pre- and post-processing described above must be decorated with a call to `AsBffApiEndpoint()`. For Minimal API endpoints, you can apply BFF pre- and post-processing when they are mapped. ```csharp app.MapPost("/foo", context => { // ... }) .RequireAuthorization() // no anonymous access .AsBffApiEndpoint(); // BFF pre/post processing ``` For MVC controllers, you can similarly apply BFF pre- and post-processing to controller actions when they are mapped. ```csharp app.MapControllers() .RequireAuthorization() // no anonymous access .AsBffApiEndpoint(); // BFF pre/post processing ``` Alternatively, you can apply the `[BffApi]` attribute directly to the controller or action. ```csharp [Route("myApi")] [BffApi] public class MyApiController : ControllerBase { // ... } ``` ### Disabling Anti-forgery Protection [Section titled “Disabling Anti-forgery Protection”](#disabling-anti-forgery-protection) Disabling anti-forgery protection is possible but not recommended. Antiforgery protection defends against CSRF attacks, so opting out may cause security vulnerabilities. However, if you are defending against CSRF attacks with some other mechanism, you can opt out of Duende.BFF’s CSRF protection. Depending on the version of Duende.BFF, use one of the following approaches. For *version 1.x*, set the `requireAntiForgeryCheck` parameter to `false` when adding the endpoint. For example: Program.cs ```csharp // MVC controllers app.MapControllers() .RequireAuthorization() // WARNING: Disabling antiforgery protection may make // your APIs vulnerable to CSRF attacks .AsBffApiEndpoint(requireAntiforgeryCheck: false); // simple endpoint app.MapPost("/foo", context => { // ... }) .RequireAuthorization() // WARNING: Disabling antiforgery protection may make // your APIs vulnerable to CSRF attacks .AsBffApiEndpoint(requireAntiforgeryCheck: false); ``` On MVC controllers and actions you can set the `RequireAntiForgeryCheck` as a flag in the `BffApiAttribute`, like this: ```csharp [Route("sample")] // WARNING: Disabling antiforgery protection may make // your APIs vulnerable to CSRF attacks [BffApi(requireAntiForgeryCheck: false)] public class SampleApiController : ControllerBase { /* ... */ } ``` In *version 2.x and 3.x*, use the `SkipAntiforgery` fluent API when adding the endpoint. For example: Program.cs ```csharp // MVC controllers app.MapControllers() .RequireAuthorization() .AsBffApiEndpoint() // WARNING: Disabling antiforgery protection may make // your APIs vulnerable to CSRF attacks .SkipAntiforgery(); // simple endpoint app.MapPost("/foo", context => { /* ... */ }) .RequireAuthorization() .AsBffApiEndpoint() // WARNING: Disabling antiforgery protection may make // your APIs vulnerable to CSRF attacks .SkipAntiforgery(); ``` MVC controllers and actions can use the `BffApiSkipAntiforgeryAttribute` (which is independent of the `BffApiAttribute`), like this: ```csharp [Route("sample")] // WARNING: Disabling antiforgery protection may make // your APIs vulnerable to CSRF attacks [BffApiSkipAntiforgeryAttribute] public class SampleApiController : ControllerBase { /* ... */ } ``` ### Skipping Response Handling [Section titled “Skipping Response Handling”](#skipping-response-handling) By default, when BFF pre/post-processing is enabled on an endpoint (via `.AsBffApiEndpoint()`), the BFF framework intercepts authentication challenge and forbid responses. Instead of returning a 302 redirect to the identity provider (which is not useful for API calls), it converts them to API-friendly status codes: * **Challenge** (unauthenticated) → returns **401** (instead of 302 redirect) * **Forbid** (unauthorized) → returns **403** (instead of 302 redirect) If you want to opt out of this behavior and let the default authentication response handling occur (e.g., if your endpoint needs to trigger a redirect), you can use the `SkipResponseHandling()` extension method: ```csharp app.MapGet("/my-endpoint", context => { /* ... */ }) .AsBffApiEndpoint() .SkipResponseHandling(); ``` For MVC controllers, you can use the `[BffApiSkipResponseHandling]` attribute: ```csharp [Route("my-endpoint")] [BffApi] [BffApiSkipResponseHandling] public class MyController : ControllerBase { /* ... */ } ``` ----- # Proxying Remote APIs > Learn how to configure and secure remote API access through BFF using HTTP forwarding and token management. A *Remote API* is an API that is deployed separately from the BFF host. Remote APIs use access tokens to authenticate and authorize requests, but the frontend does not possess an access token to make requests to remote APIs directly. Instead, all access to remote APIs is proxied through the BFF, which authenticates the frontend using its authentication cookie, gets the appropriate access token, and forwards the request to the Remote API with the token attached. There are two different ways to set up Remote API proxying in Duende.BFF. This page describes the built-in simple HTTP forwarder. Alternatively, you can integrate Duende.BFF with Microsoft’s [YARP](/bff/fundamentals/apis/yarp/) reverse proxy, which allows for more complex reverse proxy features provided by YARP combined with the security and identity features of Duende.BFF. ## Direct HTTP Forwarding [Section titled “Direct HTTP Forwarding”](#direct-http-forwarding) Duende.BFF’s direct HTTP forwarder maps routes in the BFF to a remote API surface. It uses [Microsoft YARP](https://github.com/microsoft/reverse-proxy) internally, but is much simpler to configure than YARP. The intent is to provide a developer-centric and simplified way to proxy requests from the BFF to remote APIs when more complex reverse proxy features are not needed. These routes receive automatic anti-forgery protection and integrate with automatic token management. To enable this feature, add a reference to the [`Duende.BFF.Yarp` NuGet package](https://www.nuget.org/packages/Duende.BFF.Yarp), add the remote APIs service to the service provider, and then add the remote endpoint mappings. #### Add Remote API Service to Service Provider [Section titled “Add Remote API Service to Service Provider”](#add-remote-api-service-to-service-provider) To use the HTTP forwarder, register it in the service provider: Program.cs ```csharp builder.Services.AddBff() .AddRemoteApis(); ``` #### Map Remote APIs [Section titled “Map Remote APIs”](#map-remote-apis) Use the `MapRemoteBffApiEndpoint` extension method to describe how to map requests coming into the BFF to remote APIs. `MapRemoteBffApiEndpoint` takes two parameters: the base path of requests that will be mapped externally, and the address to the external API where the requests will be mapped. The `MapRemoteBffApiEndpoint` extension method maps a path and all sub-paths below it. The intent is to allow easy mapping of groups of URLs. For example, you can set up mappings for the `/users`, `/users/{userId}`, `/users/{userId}/books`, and `/users/{userId}/books/{bookId}` endpoints without having to explicitly include all of them: * V4 Program.cs ```csharp app.MapRemoteBffApiEndpoint("/api/users", new Uri("https://remoteHost/users")) .WithAccessToken(RequiredTokenType.User); ``` * V3 Program.cs ```csharp app.MapRemoteBffApiEndpoint("/api/users", new Uri("https://remoteHost/users")) .WithAccessToken(TokenType.User); ``` The `WithAccessToken` method can be added to [specify token requirements](#access-token-requirements) for the remote API. The BFF will automatically forward the correct access token to the remote API, which will be scoped to the client application, the user, or either. ## Securing Remote APIs [Section titled “Securing Remote APIs”](#securing-remote-apis) Remote APIs typically require access control and must be protected against threats such as [CSRF (Cross-Site Request Forgery)](https://developer.mozilla.org/en-US/docs/Glossary/CSRF) attacks. To provide access control, you can specify authorization policies on the mapped routes and configure them with access token requirements. To defend against CSRF attacks, you should use SameSite cookies to authenticate calls from the frontend to the BFF. As an additional layer of defense, APIs mapped with `MapRemoteBffApiEndpoint` are automatically protected with an anti-forgery header. #### SameSite cookies [Section titled “SameSite cookies”](#samesite-cookies) [The SameSite cookie attribute](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value) is a feature of modern browsers that restricts cookies so that they are only sent to pages originating from the [site](https://developer.mozilla.org/en-US/docs/Glossary/Site) where the cookie was originally issued. This prevents CSRF attacks, because cross site requests will no longer implicitly include the user’s credentials. This is a good first layer of defense but makes the assumption that you can trust all subdomains of your site. All subdomains within a registrable domain are considered the same site for purposes of SameSite cookies. Thus, if another application hosted on a subdomain within your site is infected with malware, it can make CSRF attacks against your application. #### Anti-forgery header [Section titled “Anti-forgery header”](#anti-forgery-header) Remote APIs mapped in the BFF always require an additional custom header on API endpoints. For example: ```text GET /endpoint x-csrf: 1 ``` The value of the header is not important, but its presence, combined with the cookie requirement, triggers a CORS preflight request for cross-origin calls. This effectively isolates the caller to the same origin as the backend, providing a robust security guarantee. #### Require authorization [Section titled “Require authorization”](#require-authorization) The `MapRemoteBffApiEndpoint` method returns the appropriate type to integrate with the ASP.NET Core authorization system. You can attach authorization policies to remote endpoints using the `WithAccessToken` extension method, just as you would for a standard ASP.NET core endpoint created with `MapGet`. The authorization middleware will then enforce that policy before forwarding requests on that route to the remote endpoint. #### Access token requirements [Section titled “Access token requirements”](#access-token-requirements) Remote APIs sometimes allow anonymous access but usually require an access token, and the type of access token (user or client) will vary as well. You can specify access token requirements via the `WithAccessToken` extension method. Its `RequiredTokenType` parameter has five options: * **`None`** No token is required. * **`User`** A valid user access token is required and will be forwarded to the remote API. A user access token is an access token obtained during an OIDC flow (or subsequent refresh), and is associated with a particular user. User tokens are obtained when the user initially logs in, and will be automatically refreshed using a refresh token when they expire. * **`Client`** A valid client access token is required and will be forwarded to the remote API. A client access token is an access token obtained through the client credentials flow, and is associated with the client application, not any particular user. Client tokens can be obtained even if the user is not logged in. * **`UserOrClient`** Either a valid user access token or a valid client access token (as fallback) is required and will be forwarded to the remote API. * **`UserOrNone`** A valid user access token will be forwarded to the remote API when logged in. No access token will be sent when not logged in, and no OIDC flow is challenged to get an access token. ----- # YARP extensions > Integration of Duende.BFF with Microsoft's YARP reverse proxy, including token management and anti-forgery protection features. Duende.BFF integrates with Microsoft’s full-featured reverse proxy [YARP](https://microsoft.github.io/reverse-proxy/). YARP includes many advanced features such as load balancing, service discovery, and session affinity. It also has its own extensibility mechanism. Duende.BFF includes YARP extensions for token management and anti-forgery protection so that you can combine the security and identity features of `Duende.BFF` with the flexible reverse proxy features of YARP. ## Adding YARP [Section titled “Adding YARP”](#adding-yarp) To enable Duende.BFF’s YARP integration, add a reference to the *Duende.BFF.Yarp* NuGet package to your project and add YARP and the BFF’s YARP extensions to DI: ```csharp builder.Services.AddBff(); // adds YARP with BFF extensions var yarpBuilder = services.AddReverseProxy() .AddBffExtensions(); ``` ## Configuring YARP [Section titled “Configuring YARP”](#configuring-yarp) YARP is most commonly configured by a config file. The following simple example forwards a local URL to a remote API: ```json { "ReverseProxy": { "Routes": { "todos": { "ClusterId": "cluster1", "Match": { "Path": "/todos/{**catch-all}" } } }, "Clusters": { "cluster1": { "Destinations": { "destination1": { "Address": "https://API.mycompany.com/todos" } } } } } } ``` See the Microsoft [documentation](https://microsoft.github.io/reverse-proxy/articles/config-files.html) for the complete configuration schema. Another option is to configure YARP in code using the in-memory config provider included in the BFF extensions for YARP. The above configuration as code would look like this: ```csharp yarpBuilder.LoadFromMemory( new[] { new RouteConfig() { RouteId = "todos", ClusterId = "cluster1", Match = new() { Path = "/todos/{**catch-all}" } } }, new[] { new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destination1", new() { Address = "https://API.mycompany.com/todos" } }, } } }); ``` ## Token Management [Section titled “Token Management”](#token-management) Duende.BFF’s YARP extensions provide access token management and attach user or client access tokens automatically to proxied API calls. To enable this, add metadata with the name *Duende.Bff.Yarp.TokenType* to the route or cluster configuration: ```json { "ReverseProxy": { "Routes": { "todos": { "ClusterId": "cluster1", "Match": { "Path": "/todos/{**catch-all}" }, "Metadata": { "Duende.Bff.Yarp.TokenType": "User" } } } } } ``` Similarly to the [simple HTTP forwarder](/bff/fundamentals/apis/remote/#access-token-requirements), the allowed values for the token type are `None`, `User`, `Client`, `UserOrClient`, and `UserOrNone`. Routes that set the `Duende.Bff.Yarp.TokenType` metadata **require** the given type of access token. If it is unavailable (for example, if the `User` token type is specified but the request to the BFF is anonymous), then the proxied request will not be sent, and the BFF will return an HTTP 401: Unauthorized response. If you are using the code config method, call the `WithAccessToken` extension method to achieve the same thing: ```csharp yarpBuilder.LoadFromMemory( new[] { new RouteConfig() { RouteId = "todos", ClusterId = "cluster1", Match = new RouteMatch { Path = "/todos/{**catch-all}" } }.WithAccessToken(RequiredTokenType.User) }, // rest omitted ); ``` Again, the `WithAccessToken` method causes the route to require the given type of access token. If it is unavailable, the proxied request will not be made and the BFF will return an HTTP 401: Unauthorized response. ## Optional User Access Tokens [Section titled “Optional User Access Tokens”](#optional-user-access-tokens) You can attach user access tokens optionally using the `UserOrNone` token type. This causes the user’s access token to be sent with the proxied request when the user is logged in, but makes the request anonymously when the user is not logged in. In configuration, set the `Duende.Bff.Yarp.TokenType` metadata to `UserOrNone`: ```json { "ReverseProxy": { "Routes": { "todos": { "ClusterId": "cluster1", "Match": { "Path": "/todos/{**catch-all}" }, "Metadata": { "Duende.Bff.Yarp.TokenType": "UserOrNone" } } } } } ``` If you are using the code config method, call the `WithAccessToken` extension method with `RequiredTokenType.UserOrNone`: ```csharp yarpBuilder.LoadFromMemory( new[] { new RouteConfig() { RouteId = "todos", ClusterId = "cluster1", Match = new RouteMatch { Path = "/todos/{**catch-all}" } }.WithAccessToken(RequiredTokenType.UserOrNone) }, // rest omitted ); ``` ### Anti-forgery Protection [Section titled “Anti-forgery Protection”](#anti-forgery-protection) Duende.BFF’s YARP extensions can also add anti-forgery protection to proxied API calls. Anti-forgery protection defends against CSRF attacks by requiring a custom header on API endpoints, for example: ```plaintext GET /endpoint x-csrf: 1 ``` The value of the header is not important, but its presence, combined with the cookie requirement, triggers a CORS preflight request for cross-origin calls. This effectively isolates the caller to the same origin as the backend, providing a robust security guarantee. You can add the anti-forgery protection to all YARP routes by calling the `AsBffApiEndpoint` extension method: ```csharp app.MapReverseProxy() .AsBffApiEndpoint(); // or shorter app.MapBffReverseProxy(); ``` If you need more fine-grained control over which routes should enforce the anti-forgery header, you can also annotate the route configuration by adding the `Duende.Bff.Yarp.AntiforgeryCheck` metadata to the route config: ```json { "ReverseProxy": { "Routes": { "todos": { "ClusterId": "cluster1", "Match": { "Path": "/todos/{**catch-all}" }, "Metadata": { "Duende.Bff.Yarp.AntiforgeryCheck": "true" } } } } } ``` This is also possible in code: ```csharp yarpBuilder.LoadFromMemory( new[] { new RouteConfig() { RouteId = "todos", ClusterId = "cluster1", Match = new RouteMatch { Path = "/todos/{**catch-all}" } }.WithAntiforgeryCheck() }, // rest omitted ); ``` To enforce the presence of the anti-forgery headers, you need to add a middleware to the YARP pipeline: Program.cs ```csharp app.MapReverseProxy(proxyApp => { proxyApp.UseAntiforgeryCheck(); }); ``` ----- # BFF Security Framework Blazor Support > Overview of integrating the Duende BFF Security Framework with Blazor applications for secure authentication and authorization. Microsoft’s Blazor framework helps developers build rich, interactive web applications using C# and .NET. While Blazor is well-suited for rich web UIs, it introduces unique challenges around secure authentication and authorization — especially when rendering happens both on the server and in the browser. The Duende BFF Security Framework addresses these challenges by keeping access tokens on the server and providing a unified authentication state across Blazor’s rendering modes. ## Architecture [Section titled “Architecture”](#architecture) A BFF-backed Blazor app has three elements: the **backend** (server-side logic and APIs), the **frontend** (the Blazor application), and the **client** (the browser). The BFF host acts as the combined backend and frontend host: ``` flowchart LR Client[Client / Browser] subgraph BFF Host Backend[Backend APIs] Frontend[Blazor Frontend] end Client <--> Frontend Frontend <--> Backend ``` For a detailed architecture diagram and explanation, see the [Architecture overview](/bff/architecture/). ## Where to Go Next [Section titled “Where to Go Next”](#where-to-go-next) | Topic | Description | | ----------------------------------------------------------------------- | -------------------------------------------------- | | [Rendering Modes & BFF](/bff/fundamentals/blazor/rendering-modes/) | Which Blazor rendering modes work with BFF and why | | [Data Access Patterns](/bff/fundamentals/blazor/data-access/) | How to securely call APIs from Blazor components | | [Getting Started: Blazor](/bff/getting-started/blazor/) | Step-by-step setup guide for a Blazor BFF app | | [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/) | Persistent session storage for Blazor apps | | [Token Management](/bff/fundamentals/tokens/) | How BFF manages access tokens for Blazor | ## See Also [Section titled “See Also”](#see-also) * [Access Token Management](/accesstokenmanagement/) * [Blazor Server token management](/accesstokenmanagement/blazor-server/) * [IdentityServer Quickstarts](/identityserver/quickstarts/0-overview/) ----- # Blazor Data Access Patterns > How to securely access local and remote APIs from Blazor components using the BFF security framework. Depending on your Blazor rendering mode, you need different strategies for accessing data from components. The BFF security framework provides a consistent model: tokens never leave the server, and browser components access data through BFF-hosted endpoints secured with the authentication cookie. ## Overview [Section titled “Overview”](#overview) ``` flowchart LR WASM["Blazor WASM Component
(browser)"] Server["BFF Host
(server)"] Remote["Remote API
(external)"] DB["Database
(server-side)"] WASM -- "cookie + X-CSRF header" --> Server Server -- "access token" --> Remote Server -- "direct access" --> DB ``` For server-side rendering, components access data directly (database, services). For WASM rendering, components make HTTP calls to BFF-hosted endpoints which handle token attachment. ## Embedded (Local) APIs [Section titled “Embedded (Local) APIs”](#embedded-local-apis) An Embedded API is hosted within the BFF itself. It lives within the server’s security boundary, so no token needs to be passed to the browser. ### Defining the Abstraction [Section titled “Defining the Abstraction”](#defining-the-abstraction) Use an interface to abstract between server and client implementations: Shared/IDataAccessor.cs ```csharp public interface IDataAccessor { Task GetData(); } public record Data(string Value); ``` ### Server Implementation [Section titled “Server Implementation”](#server-implementation) Server/ServerDataAccessor.cs ```csharp internal class ServerDataAccessor : IDataAccessor { public Task GetData() { // Access data directly (database, cache, etc.) return Task.FromResult(new[] { new Data("example") }); } } ``` Register the server implementation and expose it as a BFF endpoint: Server/Program.cs ```csharp builder.Services.AddSingleton(); // ... app.MapGet("/some_data", async (IDataAccessor dataAccessor) => await dataAccessor.GetData()) .RequireAuthorization() .AsBffApiEndpoint(); ``` ### Client (WASM) Implementation [Section titled “Client (WASM) Implementation”](#client-wasm-implementation) On the client, use an `HttpClient` that routes through the BFF host: Client/Program.cs ```csharp builder.Services.AddBffBlazorClient() .AddLocalApiHttpClient(); // Register the concrete implementation with the abstraction builder.Services.AddSingleton(sp => sp.GetRequiredService()); ``` Client/HttpClientDataAccessor.cs ```csharp internal class HttpClientDataAccessor(HttpClient client) : IDataAccessor { public async Task GetData() => await client.GetFromJsonAsync("/some_data") ?? throw new JsonException("Failed to deserialize"); } ``` ## Secured Remote APIs [Section titled “Secured Remote APIs”](#secured-remote-apis) If your BFF needs to proxy requests to a remote API (one that requires a bearer token), configure a remote endpoint on the server and access it from the client via the BFF proxy. ### Server-side Proxy Setup [Section titled “Server-side Proxy Setup”](#server-side-proxy-setup) Server/Program.cs ```csharp app.MapRemoteBffApiEndpoint("/remote-apis/data", new Uri("https://api.example.com/data")) .WithAccessToken(RequiredTokenType.User); ``` Also register an `HttpClient` that attaches the user access token for use in Embedded API endpoints: ```csharp builder.Services.AddUserAccessTokenHttpClient("backend", configureClient: client => client.BaseAddress = new Uri("https://api.example.com/")); ``` ### Client-Side Access [Section titled “Client-Side Access”](#client-side-access) Client/Program.cs ```csharp builder.Services.AddBffBlazorClient(); builder.Services.AddRemoteApiHttpClient("backend"); builder.Services.AddTransient(sp => sp.GetRequiredService().CreateClient("backend")); ``` The diagram below shows the full flow: ``` sequenceDiagram participant WASM as Blazor WASM participant BFF as BFF Host participant API as Remote API WASM->>BFF: GET /remote-apis/data (cookie + X-CSRF) BFF->>BFF: Validate session & get access token BFF->>API: GET /data (Bearer token) API-->>BFF: 200 OK + data BFF-->>WASM: 200 OK + data ``` ## Auto-Rendering Mode [Section titled “Auto-Rendering Mode”](#auto-rendering-mode) In Interactive Auto mode, a component may render on the server first, then transition to WASM. Use the interface-based abstraction pattern from above: inject `IDataAccessor` in your component, and register both `ServerDataAccessor` (for server rendering) and `HttpClientDataAccessor` (for WASM rendering). ```razor @* Component works identically in server and WASM rendering modes *@ @inject IDataAccessor DataAccessor @if (items == null) {

Loading...

} else { @foreach (var item in items) {

@item.Value

} } @code { private Data[]? items; protected override async Task OnInitializedAsync() { items = await DataAccessor.GetData(); } } ``` ## See Also [Section titled “See Also”](#see-also) [Embedded (Local) APIs ](/bff/fundamentals/apis/local/)Full reference for BFF-hosted endpoints [Proxying Remote APIs ](/bff/fundamentals/apis/remote/)Direct forwarding to upstream services [YARP Integration ](/bff/fundamentals/apis/yarp/)Advanced reverse proxy configuration [Rendering Modes & BFF ](/bff/fundamentals/blazor/rendering-modes/)Which Blazor modes need BFF [Getting Started: Blazor ](/bff/getting-started/blazor/)Full setup walkthrough ----- # Blazor Rendering Modes & BFF > Learn which Blazor rendering modes are compatible with the BFF security pattern and why. Blazor supports [several rendering modes](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-9.0#render-modes). The BFF pattern is only applicable to modes where code runs in the browser (client context), because that is where the risk of token exposure exists. ## Rendering Mode Compatibility [Section titled “Rendering Mode Compatibility”](#rendering-mode-compatibility) | Mode | Description | Renders In | Interactive | Use BFF? | | --------------------------- | ------------------------------------------------------------- | ---------------- | ----------- | -------- | | **Static Server** | Static server-side rendering (SSR) | Server | ❌ | ❌ | | **Interactive Server** | Interactive SSR using Blazor Server and WebSockets | Server | ✅ | ❌ | | **Interactive WebAssembly** | Client-side rendering (CSR) using Blazor WASM | Browser | ✅ | ✅ | | **Interactive Auto** | Starts as Interactive Server, switches to WASM after download | Server → Browser | ✅ | ✅ | ## Static Server [Section titled “Static Server”](#static-server) Caution BFF is not necessary for Static Server rendering. Standard ASP.NET Core authentication patterns apply. Static Server renders Blazor components as plain HTML with no client-side interactivity. Because all rendering happens on the server, tokens never reach the browser. Use standard ASP.NET Core cookie authentication instead of BFF. You may still want to use the `AuthenticationStateProvider` for accessing user claims in components. ## Interactive Server [Section titled “Interactive Server”](#interactive-server) Caution BFF is not typically necessary for Interactive Server rendering. All component interactivity is managed server-side via WebSockets (SignalR). In Interactive Server mode, Blazor components run on the server and push UI updates to the browser over a WebSocket connection. Because no application code runs in the browser, tokens remain server-side naturally. You can still use [Session Management](/bff/fundamentals/session/) features of BFF if you want server-side session control, but the BFF security pattern itself is not required. ## Interactive WebAssembly [Section titled “Interactive WebAssembly”](#interactive-webassembly) In Interactive WebAssembly mode, the Blazor runtime and your application code are downloaded to and executed in the browser. This means: * Your components run in the same JavaScript sandbox as the rest of the page * Any access token stored in the WASM memory is potentially accessible to injected scripts * You must never expose access tokens to WASM components The BFF pattern solves this by keeping tokens on the server. WASM components call BFF-hosted API endpoints (using the authentication cookie), and the BFF attaches the access token server-side before forwarding to remote APIs. See the [Getting Started: Blazor guide](/bff/getting-started/blazor/) for setup instructions. ## Interactive Auto [Section titled “Interactive Auto”](#interactive-auto) Interactive Auto combines Interactive Server and Interactive WebAssembly: rendering starts on the server but switches to client-side WASM on subsequent visits after the Blazor bundle is downloaded. Because your application may be running in the browser at any time, you cannot rely on server-side-only token handling. The BFF pattern ensures tokens remain server-side regardless of which rendering mode is active. ## Authentication State [Section titled “Authentication State”](#authentication-state) The `AuthenticationState` contains information about the currently logged-in user, including management claims like the logout URL. Blazor uses `AuthenticationStateProvider` implementations to make authentication state available to components: * **On the server**: The BFF’s `AddServerManagementClaimsTransform` enriches the claims with the logout URL. * **On the client (WASM)**: The `BffClientAuthenticationStateProvider` polls `/bff/user` to keep the client in sync with the server session. This also notifies the frontend if the session is terminated server-side (e.g., back-channel logout). ## Server-Side Token Store [Section titled “Server-Side Token Store”](#server-side-token-store) Blazor Server applications stream content over a WebSocket, so there is often no `HttpContext` available during component execution. This means: * You cannot use `HttpContext` extension methods in Blazor Server components * The normal mechanism to attach tokens to `HttpClient` calls does not work without special setup When you register `AddBlazorServer()`, BFF automatically registers the `ServerSideTokenStore` and Duende.AccessTokenManagement integration so that token management works correctly in Blazor Server. For more details, see [Blazor Server token management](/accesstokenmanagement/blazor-server/). ## See Also [Section titled “See Also”](#see-also) [Data Access Patterns ](/bff/fundamentals/blazor/data-access/)How to call APIs from Blazor components [Getting Started: Blazor ](/bff/getting-started/blazor/)Full walkthrough setup guide [Troubleshooting ](/bff/troubleshooting/)Common Blazor BFF issues ----- # Production Deployment > Guide for deploying Duende BFF to production, covering load balancing, data protection, health checks, cookie domain configuration, and monitoring. This page covers the production-specific concerns you need to address before deploying a BFF host. For middleware pipeline order, see [Middleware Pipeline](/bff/fundamentals/middleware-pipeline/). ## Load Balancing and Sticky Sessions [Section titled “Load Balancing and Sticky Sessions”](#load-balancing-and-sticky-sessions) The BFF uses ASP.NET Core’s Data Protection to encrypt and sign session cookies. In a multi-instance (load-balanced) deployment, **all instances must share the same Data Protection key ring** — otherwise cookies issued by one instance cannot be decrypted by another, causing random logout on failover. ### Shared Key Storage [Section titled “Shared Key Storage”](#shared-key-storage) Configure Data Protection to store keys in a shared location accessible to all instances: ```csharp // Using Azure Blob Storage + Azure Key Vault builder.Services.AddDataProtection() .PersistKeysToAzureBlobStorage(connectionString, "data-protection", "keys.xml") .ProtectKeysWithAzureKeyVault(keyIdentifier, credential); // Using a network file share or a database builder.Services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\dp-keys")) .ProtectKeysWithCertificate(certificate); ``` Do not use in-memory keys in production The default in-memory key ring is regenerated on every restart. Any existing sessions are invalidated when the process restarts or when traffic is routed to a new instance. See also: [Data Protection](/general/data-protection/) ### Server-Side Sessions (Recommended for Multi-Instance) [Section titled “Server-Side Sessions (Recommended for Multi-Instance)”](#server-side-sessions-recommended-for-multi-instance) With cookie-only sessions, every instance must share Data Protection keys. With **server-side sessions**, the cookie only holds an opaque session ID — the session payload is stored in a shared database. This is simpler to operate because: * Key ring only needs to be consistent (not necessarily shared) — the cookie just holds an ID * Sessions can be inspected and revoked server-side * Cookie size is minimized [Server-Side Sessions ](/bff/fundamentals/session/server-side-sessions/)Setup guide for database-backed session storage ### Sticky Sessions [Section titled “Sticky Sessions”](#sticky-sessions) If you cannot use server-side sessions and cannot share a Data Protection key ring, configure your load balancer to route each user consistently to the same instance (“sticky sessions” / session affinity). This is a last resort — prefer shared key storage. ## Health Check Endpoints [Section titled “Health Check Endpoints”](#health-check-endpoints) Expose a health check endpoint so your load balancer and orchestrator (Kubernetes, etc.) can detect unhealthy instances: ```csharp builder.Services.AddHealthChecks(); // In your pipeline (after UseRouting): app.MapHealthChecks("/health"); ``` For a more complete health check that validates downstream dependencies (database, token endpoint reachability): ```csharp builder.Services.AddHealthChecks() .AddDbContextCheck() // EF Core session store .AddUrlGroup(new Uri("https://idp.example.com/.well-known/openid-configuration"), name: "identity-provider"); app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = _ => false }); app.MapHealthChecks("/health/ready", new HealthCheckOptions()); ``` * `/health/live` — liveness: is the process running? (no dependency checks) * `/health/ready` — readiness: are all dependencies reachable? (fail this to take the instance out of rotation) ## Cookie Domain Configuration [Section titled “Cookie Domain Configuration”](#cookie-domain-configuration) By default, the BFF session cookie is scoped to the exact host. If you need the cookie to work across subdomains (e.g. `app.example.com` and `api.example.com`): ```csharp builder.Services.AddAuthentication() .AddCookie(options => { options.Cookie.Domain = ".example.com"; // Note leading dot options.Cookie.SameSite = SameSiteMode.Strict; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; }); ``` Scope cookies as narrowly as possible Setting a broad cookie domain (`.example.com`) means the cookie is sent to all subdomains, including any you don’t control. Only use this when architecturally required, and ensure all subdomains are trusted. For split-host deployments (frontend on `app.example.com`, BFF on `bff.example.com`), you will need to set the cookie domain AND configure CORS. See [Separate Host for UI](/bff/architecture/ui-hosting/) and the [SplitHosts sample](/bff/samples/). ## Reverse Proxy Configuration [Section titled “Reverse Proxy Configuration”](#reverse-proxy-configuration) The BFF is typically deployed behind a reverse proxy (NGINX, Azure Application Gateway, AWS ALB, etc.). Configure ASP.NET Core to trust forwarded headers: ```csharp builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; // Restrict to your proxy's IP to prevent header spoofing: options.KnownProxies.Add(IPAddress.Parse("10.0.0.100")); }); // Must be the FIRST middleware: app.UseForwardedHeaders(); ``` Without this, `HttpContext.Request.Scheme` will be `http` even when the client uses HTTPS, which causes: * The OIDC redirect URI to be `http://...` (rejected by the identity provider) * The session cookie’s `Secure` flag to have no effect * `HttpContext.Request.Host` to reflect the internal host, breaking the OIDC redirect ## Monitoring and Alerting [Section titled “Monitoring and Alerting”](#monitoring-and-alerting) BFF emits OpenTelemetry metrics and traces. See [Diagnostics](/bff/diagnostics/) for the full list of metric names and activity sources. ### Key Metrics to Alert On [Section titled “Key Metrics to Alert On”](#key-metrics-to-alert-on) | Metric | Alert Condition | Likely Cause | | ----------------------------------------- | ------------------- | ---------------------------------------------------------------------------- | | `session.started` | Sudden drop to 0 | Data Protection key mismatch, pod restart without shared keys | | `session.ended` | Unexpected spike | Back-channel logout sweep, session store purge, Data Protection key rotation | | `session.ended` / `session.started` ratio | Sustained ratio > 1 | Sessions ending faster than starting — investigate IdP or store issues | | HTTP 5xx on `/bff/*` endpoints | Any sustained spike | BFF host error — check logs | ### Recommended Alerts [Section titled “Recommended Alerts”](#recommended-alerts) ```plaintext # Prometheus-style alert examples # Metric names are converted from dot notation to underscores by Prometheus. # No new sessions — potential Data Protection key mismatch alert: BffNoNewSessions expr: rate(session_started_total[5m]) == 0 for: 5m # Abnormal session churn alert: BffSessionChurn expr: rate(session_ended_total[5m]) / rate(session_started_total[5m]) > 2 for: 5m ``` ## See Also [Section titled “See Also”](#see-also) [Middleware Pipeline ](/bff/fundamentals/middleware-pipeline/)Correct middleware ordering for production [Server-Side Sessions ](/bff/fundamentals/session/server-side-sessions/)Recommended for multi-instance deployments [Diagnostics ](/bff/diagnostics/)Metrics and distributed tracing reference [Separate Host for UI ](/bff/architecture/ui-hosting/)Split-host deployment patterns ----- # Middleware Pipeline > The correct ASP.NET Core middleware order for Duende BFF applications, with explanations of what each component does and common misconfiguration pitfalls. Getting the middleware pipeline order right is critical for BFF to function correctly. Placing middleware in the wrong order can silently disable security features with no obvious error message. ## Canonical Pipeline Order [Section titled “Canonical Pipeline Order”](#canonical-pipeline-order) Program.cs ```csharp var app = builder.Build(); // 1. Forwarded headers (if behind a reverse proxy) app.UseForwardedHeaders(); // 2. HTTPS redirection app.UseHttpsRedirection(); // 3. Static files (serve before auth to avoid unnecessary overhead) app.UseStaticFiles(); // 4. Routing — must come before UseBff and UseAuthorization app.UseRouting(); // 5. Authentication — must come before UseBff app.UseAuthentication(); // 6. BFF middleware — must come AFTER UseAuthentication and UseRouting, // but BEFORE UseAuthorization app.UseBff(); // 7. Authorization app.UseAuthorization(); // 8. Map your endpoints app.MapGet("/api/data", () => Results.Ok("hello")) .RequireAuthorization() .AsBffApiEndpoint(); app.Run(); ``` ## Why Order Matters [Section titled “Why Order Matters”](#why-order-matters) Each middleware in the pipeline can only see the work done by the middleware before it. Here’s why each position is required: | Position | Middleware | Why Here | | ---------------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------- | | Before `UseBff` | `UseRouting()` | BFF needs the endpoint route resolved to know which endpoints require anti-forgery protection | | Before `UseBff` | `UseAuthentication()` | BFF reads the authenticated user from the `HttpContext`; without this, the user is always null | | After `UseAuthentication`, before `UseAuthorization` | `UseBff()` | BFF anti-forgery checks run here; placing it after `UseAuthorization` silently disables them | | After `UseBff` | `UseAuthorization()` | Authorization decisions depend on BFF’s pre-processing having already run | Silent failure — no error if pipeline is wrong If `UseBff()` is placed **after** `UseAuthorization()`, anti-forgery enforcement is **silently disabled** — no exception is thrown and no log warning is emitted by default. Always verify pipeline order when debugging authentication issues. The `EnforceBffMiddleware` option (enabled by default) adds a check that throws at startup if the BFF management endpoints are called without the BFF middleware being present. However, this does not catch all ordering mistakes. ## BFF v4 — Automatic Middleware Registration [Section titled “BFF v4 — Automatic Middleware Registration”](#bff-v4--automatic-middleware-registration) In BFF v4, when `AutomaticallyRegisterBffMiddleware` is enabled (the default), the middleware components are registered automatically. You still need to call `UseBff()` yourself in the correct position, but the frontend selection, path mapping, OpenID Connect callbacks, and static file proxying middlewares are added automatically. If you need full control over the pipeline, disable automatic registration: ```csharp builder.Services.AddBff(options => { options.AutomaticallyRegisterBffMiddleware = false; }); ``` Then register each component manually: ```csharp // Before Authentication: app.UseForwardedHeaders(); app.UseBffPreProcessing(); // Frontend selection, path mapping, OIDC callbacks app.UseAuthentication(); app.UseRouting(); // The main BFF middleware (anti-forgery): app.UseBff(); app.UseAuthorization(); // After endpoint mapping: app.UseBffPostProcessing(); // Management endpoints, remote API handling, static file proxying ``` ## Blazor Pipeline Order [Section titled “Blazor Pipeline Order”](#blazor-pipeline-order) Blazor applications need a slightly different order to accommodate Blazor’s own middleware: ```csharp app.UseRouting(); app.UseAuthentication(); // BFF must come after UseAuthentication app.UseBff(); app.UseAuthorization(); // Blazor's anti-forgery protection (separate from BFF's anti-forgery) app.UseAntiforgery(); // In v3, also add: // app.MapBffManagementEndpoints(); app.MapRazorComponents() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode(); ``` ## Common Mistakes [Section titled “Common Mistakes”](#common-mistakes) | Mistake | Symptom | Fix | | ---------------------------------------- | --------------------------------------------------- | --------------------------------------------------- | | `UseBff()` after `UseAuthorization()` | Anti-forgery silently disabled; `401` on API calls | Move `UseBff()` before `UseAuthorization()` | | Missing `UseAuthentication()` | All users appear anonymous; no redirect to login | Add `app.UseAuthentication()` before `app.UseBff()` | | Missing `UseRouting()` before `UseBff()` | Anti-forgery checks don’t apply correctly to routes | Add `app.UseRouting()` before `app.UseBff()` | | `.AsBffApiEndpoint()` missing | API returns `302` redirect instead of `401` | Add `.AsBffApiEndpoint()` to each API endpoint | ## See Also [Section titled “See Also”](#see-also) [Getting Started: Single Frontend ](/bff/getting-started/single-frontend/)Complete setup example with correct pipeline [Getting Started: Blazor ](/bff/getting-started/blazor/)Blazor-specific pipeline setup [Troubleshooting ](/bff/troubleshooting/)Diagnosing anti-forgery and authentication failures [Configuration Options ](/bff/fundamentals/options/)AutomaticallyRegisterBffMiddleware and related settings ----- # Multi-Frontend > Documentation for multi-frontend support in BFF The Backend For Frontend pattern basically states that there should be a single backend for each frontend. While for some applications / architectures this makes a lot of sense, because there is a 1-to-1 mapping between the API surface and the browser based application, for some other architectures this may not be useful. Especially in micro-service based architectures, where there are many backend APIs and multiple frontends using these APIs, having a dedicated backend service for each frontend introduces quite a lot of operational overhead. To overcome this issue, a single BFF instance can support multiple frontends. Each frontend you configure can: * Define its own OpenID Connect configuration * Define its own Cookie settings * Define its own API surface * Be identified either via path based routing and/or host selection. Adding additional frontends to the BFF has very little impact on the performance on the BFF itself, but keep in mind that the traffic for all the frontends is proxied through the BFF. ## Authentication Configuration [Section titled “Authentication Configuration”](#authentication-configuration) When you use multiple frontends, you can’t rely on [manual authentication configuration](/bff/fundamentals/session/handlers/#manually-configuring-authentication). This is because each frontend requires its own scheme, and potentially its own OpenID Connect and Cookie configuration. Instead, you should rely on [automatic authentication configuration](/bff/fundamentals/session/handlers/#automatic-authentication-configuration). Below is an example on how to configure multiple frontends. ```csharp var bffBuilder = builder.Services .AddBff(); bffBuilder .ConfigureOpenIdConnect(options => { // These are the default values for all frontends. options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api"); options.Scope.Add("offline_access"); options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; }); .AddFrontends( // This frontend will use the default authentication options new BffFrontend(BffFrontendName.Parse("default-frontend")), // This frontend uses most of the same authentication options, new BffFrontend(BffFrontendName.Parse("with-path")) .MapToPath("/with-path") .WithOpenIdConnectOptions(opt => { // but overrides the clientid and client secret. opt.ClientId = "different-client-id"; opt.ClientSecret = "different secret"; }) .WithCookieOptions(opt => { // and overrides the cookie options to use 'lax' cookies. opt.Cookie.SameSite = SameSiteMode.Lax; })); ``` The order in which configuration is applied is 1. programmatic default options (if any) 2. default options from configuration (if any) 3. frontend specific options (if any) Each frontend can have custom OpenID Connect configuration and Cookie Configuration. This can both be configured programmatically as via [Configuration](configuration/). ## Frontend Selection [Section titled “Frontend Selection”](#frontend-selection) Each request to a frontend has to be uniquely defined by either its path, its host or a combination of the two. If you specify neither, then it’s considered the default frontend. Frontends are matched using the following algorithm: 1. **Selection by both host and path:** If there is a frontend that matches both the host AND has the most specific match to a path, it’s selected. 2. **Selection by host only:** Then, if there is a frontend with only hosts configured and it matches the path, it’s selected. 3. **Selection by path only:** Then, if there is a frontend with a matching path specified, it’s selected. 4. **Default frontend:** Then, if there is a default frontend configured, it’s selected. In summary, the most specific match will be selected. ### Implicit Frontend Disabled [Section titled “Implicit Frontend Disabled”](#implicit-frontend-disabled) When you don’t add any frontends, BFF creates an implicit default frontend. This allows BFF to function correctly in single frontend mode. As soon as you add a frontend, this implicit frontend is disabled. If you want to use both explicitly matching frontends (on host headers or paths) and a default (fallback) frontend, you should explicitly add this default frontend. ## Adding A Frontend During Startup [Section titled “Adding A Frontend During Startup”](#adding-a-frontend-during-startup) The simplest way to add frontends is during startup. ```csharp services .AddBff() .AddFrontends(new BffFrontend(BffFrontendName.Parse("frontend1"))); ``` You can call `AddFrontends` with multiple frontends in one go, or call it multiple times. ## Adding / Updating A Frontend Dynamically At Runtime [Section titled “Adding / Updating A Frontend Dynamically At Runtime”](#adding--updating-a-frontend-dynamically-at-runtime) If you want to manipulate the frontends at runtime, you can do so via the `IFrontendCollection` interface. ```csharp var frontends = app.Services.GetRequiredService(); frontends.AddOrUpdate(new BffFrontend(name)); frontends.Remove(name); ``` ## Defining The API Surface [Section titled “Defining The API Surface”](#defining-the-api-surface) A frontend can define its own API surface, by specifying remote APIs. ```csharp var frontend = new BffFrontend(BffFrontendName.Parse("frontend1")) .WithRemoteApis( // map the local path /path to the remote api new RemoteApi("/some_path", new Uri("https://remote-api"))) // You can also configure various options, such as the type of token, // retrieval parameters, etc.. new RemoteApi("/with_options", new Uri("https://remote-api"))) .WithAccessToken(RequiredTokenType.UserOrClient), .WithAccessTokenRetriever(), .WithUserAccessTokenParameters(new BffUserAccessTokenParameters { Resource = Resource.Parse("urn:isolated-api") })); ``` See the topic on [Token Management](/bff/fundamentals/tokens/) for more information about the various token management options. ## Handling SPA Static Assets [Section titled “Handling SPA Static Assets”](#handling-spa-static-assets) BFF can be configured to handle the static file assets that are typically used when developing Single-Page Application (SPA)-based app. ### Proxying Only `index.html` [Section titled “Proxying Only index.html”](#proxying-only-indexhtml) When deploying a multi-frontend BFF, it makes most sense to have the frontend’s configured with an `index.html` file that is retrieved from a Content Delivery Network (CDN). This can be done in various ways. For example, if you use Vite, you can publish static assets with a base URL configured. This will make sure that any static asset, (such as images, scripts, etc) are retrieved directly from the CDN for best performance. ```csharp var frontend = new BffFrontend(BffFrontendName.Parse("frontend1")) .WithCdnIndexHtmlUrl(new Uri("https://my_cdn/some_app/index.html")) ``` When you do this, the BFF automatically wires up a catch-all route that serves the `index.html` for that specific frontend. See [Serve the index page from the BFF host](/bff/architecture/ui-hosting/#serve-the-index-page-from-the-bff-host) for more information. #### Transforming the `index.html` [Section titled “Transforming the index.html”](#transforming-the-indexhtml) If you need to modify the `index.html` before it is served to the client (for example, to inject frontend-specific configuration or environment variables), you can implement the `IIndexHtmlTransformer` interface: ```csharp public class MyIndexHtmlTransformer : IIndexHtmlTransformer { public Task Transform(string indexHtml, BffFrontend frontend, CancellationToken ct = default) { // Inject a frontend-specific config script tag var transformed = indexHtml.Replace( "", $""); return Task.FromResult(transformed); } } ``` Register the transformer in the service collection: ```csharp services.AddSingleton(); ``` The transformer is called after the `index.html` is fetched from the CDN and before it is cached. The cache duration is controlled by the [`IndexHtmlDefaultCacheDuration`](/bff/fundamentals/options/#cdn--static-assets) option. ### Proxying All Static Assets [Section titled “Proxying All Static Assets”](#proxying-all-static-assets) When developing a Single-Page Application (SPA), it’s very common to use a development webserver such as Vite. While Vite can publish static assets with a base URL, this doesn’t work well during development. The best development experience can be achieved by configuring the BFF to proxy all static assets from the development server: ```csharp var frontend = new BffFrontend(BffFrontendName.Parse("frontend1")) .WithProxiedStaticAssets(new Uri("https://localhost:3000")); // https://localhost:3000 would be the URL of your development web server. ``` While this can also be done in production, it will proxy all static assets through BFF. This will increase the bandwidth consumed by the BFF and reduce the overall performance of your application. ### Proxying Assets Based On Environment [Section titled “Proxying Assets Based On Environment”](#proxying-assets-based-on-environment) If you’re using a local development server during development and a CDN in production, you can configure asset proxying as follows: ```csharp // In this example, the environment name from the application builder is used to determine // if we're running in production or not. var runningInProduction = () => builder.Environment.EnvironmentName == Environments.Production; // Then, when configuring the frontend, you can switch when the static assets will be proxied. new BffFrontend(BffFrontendName.Parse("default-frontend")) .WithBffStaticAssets(new Uri("https://localhost:5010/static"), useCdnWhen: runningInProduction); ``` ----- # BFF Multi-Frontend Configuration > Documentation for managing BFF multi-frontend configuration It’s possible to configure frontends for the BFF via `IConfiguration`. This enables dynamic loading / changing of frontends, including their OpenID Connect configuration, BFF Configuration, and Remote APIs. ```csharp var bffConfig = new ConfigurationBuilder() .AddJsonFile(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "BffConfig.json"), optional: false, reloadOnChange: true) services .AddBff() .LoadConfiguration(bffConfig); ``` The configuration supports dynamic reloading (so any new frontend added / removed is immediately reflected). ### BffConfiguration [Section titled “BffConfiguration”](#bffconfiguration) * `defaultOidcSettings` OIDC settings applied globally to all frontends unless overridden.\ Type: OidcConfiguration object ([see below](#oidcconfiguration-json-properties)). * `defaultCookieSettings` Cookie settings applied globally to all frontends unless overridden.\ Type: CookieConfiguration object ([see below](#cookieconfiguration-json-properties)). * `frontends` Dictionary of frontend configurations.\ Each key is a frontend name, and the value is a BffFrontendConfiguration object ([see below](#bfffrontendconfiguration-json-properties)). *** ### BffFrontendConfiguration JSON Properties [Section titled “BffFrontendConfiguration JSON Properties”](#bfffrontendconfiguration-json-properties) * `cdnIndexHtmlUrl` The `index.html` that should be used for this frontend (usually on a CDN). When using this property, a fallback route will be created that only proxies the `index.html`. Other static assets are supposed to be retrieved directly from the CDN by the browser. Example: `"https://cdn.yourapp.com/some_app/index.html"` * `staticAssetsUrl` The URL where all static assets can be found. This registers a fallback route that will proxy all static assets from this URL. This is usually used during development, when you’re using a development web server such as Vite. Example: `"https://localhost:3000/"` * `matchingPath` The path prefix for requests routed to this frontend.\ Example: `"/from-config"` * `matchingHostHeader` The host to match for this frontend. Example: `"https://localhost:5005"` * `oidc` OIDC settings specific to this frontend.\ Type: OidcConfiguration object ([see below](#oidcconfiguration-json-properties)). * `cookies` Cookie settings specific to this frontend.\ Type: CookieConfiguration object ([see below](#cookieconfiguration-json-properties)). * `remoteApis` Remote APIs for this frontend. Type: RemoteApiConfiguration object. ([see below](#remoteapiconfiguration-json-properties)). ### RemoteApiConfiguration JSON Properties [Section titled “RemoteApiConfiguration JSON Properties”](#remoteapiconfiguration-json-properties) * `pathMatch` String. The local path that will be used to access the remote API.\ Example: `"/api/user-token"` * `targetUri` String. The target URI of the remote API.\ Example: `"https://localhost:5010"` * `requiredTokenType` String. The token requirement for accessing the remote API.\ Possible values: `"None"`, `"User"`, `"Client"`, `"UserOrClient"`, `"UserOrNone"`\ Default: `"User"` * `tokenRetrieverTypeName` String. The type name of the access token retriever to use for this remote API. * `userAccessTokenParameters` Object. Parameters for retrieving a user access token ([see below](#useraccesstokenparameters-json-properties)). * `activityTimeout` String. How long a request is allowed to remain idle between operations before being canceled.\ Use C# `TimeSpan` serialization format, e.g. `"00:01:40"` for 100 seconds. * `allowResponseBuffering` Boolean. Allows write buffering when sending a response back to the client (if supported by the server).\ Note: Enabling this can break server-sent events (SSE) scenarios. *** ### UserAccessTokenParameters JSON Properties [Section titled “UserAccessTokenParameters JSON Properties”](#useraccesstokenparameters-json-properties) * `signInScheme` String. The scheme used for signing in the user (typically the cookie authentication scheme).\ Example: `"Cookies"` * `challengeScheme` String. The authentication scheme to be used for challenges.\ Example: `"OpenIdConnect"` * `forceRenewal` Boolean. Whether to force renewal of the access token. * `resource` String. The resource for which the access token is requested.\ Example: `"https://api.example.com"` ### OidcConfiguration JSON Properties [Section titled “OidcConfiguration JSON Properties”](#oidcconfiguration-json-properties) * `clientId` The client ID of the OpenID Connect client. * `clientSecret` The client secret of the OpenID Connect client. * `callbackPath` The path or URI to which the OpenID Connect client will redirect after authentication. * `authority` The authority URI, typically the issuer or identity provider endpoint. * `responseType` The response type that the OpenID Connect client will request. * `responseMode` The response mode that the OpenID Connect client will use to return the authentication response. * `mapInboundClaims` Boolean. Whether to map inbound claims from the OpenID Connect provider to the user’s claims in the application. * `saveTokens` Boolean. Whether to save the tokens received from the OpenID Connect provider. * `scope` Array of strings. The scopes that the OpenID Connect client will request from the provider. * `getClaimsFromUserInfoEndpoint` Boolean. Whether to retrieve claims from the UserInfo endpoint of the OpenID Connect provider. ### CookieConfiguration JSON Properties [Section titled “CookieConfiguration JSON Properties”](#cookieconfiguration-json-properties) * `httpOnly` Boolean. Indicates whether the cookie is inaccessible by client-side script. Defaults to true. * `sameSite` String. The SameSite attribute of the cookie. Defaults to `"Strict"`.\ Possible values: `"None"`, `"Lax"`, `"Strict"` * `securePolicy` String. The policy used to determine if the cookie is sent only over HTTPS.\ Possible values: `"Always"`, `"None"`, `"SameAsRequest"` * `name` String. The name of the cookie. * `maxAge` String. The max-age for the cookie. Example: “0:01:00 for 1 minute * `path` String. The cookie path. The BFF will configure the default values for this property. Example: `"/"` * `domain` String. The domain to associate the cookie with. The BFF will configure the default values for this property.\ Example: `"example.com"` ### Example [Section titled “Example”](#example) ```json { "defaultOidcSettings": { "clientId": "global-client", "authority": "https://login.example.com" }, "defaultCookieSettings": null, "frontends": { "some_frontend": { "cdnIndexHtmlUrl": "https://localhost:5005/static/index.html", "matchingPath": "/from-config", "oidc": { "clientId": "frontend1-client", "scope": ["openid", "profile", "email"] }, "remoteApis": [ { "pathMatch": "/todos", "targetUri": "https://localhost:5020/todos/", "requiredTokenType": "User" } ] } } } ``` ----- # Configuration Options > Comprehensive guide to configuring Duende BFF framework including general settings, paths, session management, and API options The `Duende.BFF.BffOptions` allows to configure several aspects of the BFF framework. You set the options at startup time: ```csharp builder.Services.AddBff(options => { // configure options here... }) ``` ## Common Configurations [Section titled “Common Configurations”](#common-configurations) The sections below show complete, annotated options blocks for the most common deployment scenarios. The full reference for every option follows after. ### Production Deployment [Section titled “Production Deployment”](#production-deployment) ```csharp builder.Services.AddBff(options => { // Required for production options.LicenseKey = builder.Configuration["Duende:LicenseKey"]; // Revoke refresh tokens on logout (default: true — keep enabled) options.RevokeRefreshTokenOnLogout = true; // Log out all sessions for a user when back-channel logout is received // Set to true if you want global logout across devices options.BackchannelLogoutAllUserSessions = false; // Session cleanup (v4+): call .AddSessionCleanupBackgroundProcess() instead options.SessionCleanupInterval = TimeSpan.FromMinutes(10); }) // Use Entity Framework for production-grade session storage .AddServerSideSessions() .AddEntityFrameworkServerSideSessions(options => { options.UseSqlServer(connectionString); }); ``` ### Development with a Separate Frontend (Split Host) [Section titled “Development with a Separate Frontend (Split Host)”](#development-with-a-separate-frontend-split-host) When your SPA is served by a separate dev server (e.g., Vite on `localhost:3000`) and the BFF is on a different port: ```csharp builder.Services.AddBff(options => { // Allow the separate frontend origin to use silent login options.AllowedSilentLoginReferers = ["https://localhost:3000"]; }) .ConfigureCookies(options => { // Lax is required when the IDP is on a different site than the BFF options.Cookie.SameSite = SameSiteMode.Lax; }); // Allow CORS requests from the dev server builder.Services.AddCors(options => { options.AddPolicy("DevSpa", policy => policy.WithOrigins("https://localhost:3000") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials()); }); ``` ### Multi-Frontend with Per-Frontend OIDC [Section titled “Multi-Frontend with Per-Frontend OIDC”](#multi-frontend-with-per-frontend-oidc) ```csharp builder.Services.AddBff() // Global OIDC defaults (overridden per-frontend where needed) .ConfigureOpenIdConnect(options => { options.Authority = "https://login.example.com"; options.ClientId = "shared-client"; options.ClientSecret = "secret"; options.SaveTokens = true; options.Scope.Add("offline_access"); }) // Register named frontends .AddFrontends( new BffFrontend(BffFrontendName.Parse("main-app")) .WithCdnIndexHtmlUrl(new Uri("https://cdn.example.com/app/index.html")), new BffFrontend(BffFrontendName.Parse("admin-app")) .MapToPath("/admin") .WithOpenIdConnectOptions(opt => { // Admin frontend uses a different client ID opt.ClientId = "admin-client"; opt.ClientSecret = "admin-secret"; }) .WithCdnIndexHtmlUrl(new Uri("https://cdn.example.com/admin/index.html")) ); ``` ## General [Section titled “General”](#general) * **`EnforceBffMiddleware`** Enables checks in the user management endpoints that ensure that the BFF middleware has been added to the pipeline. Since the middleware performs important security checks, this protects from accidental configuration errors. You can disable this check if it interferes with some custom logic you might have. Defaults to true. * **`LicenseKey`** This sets the license key for Duende.BFF. A license key is required for production deployments. See [licensing](/general/licensing/) for details about how to configure the license key. * **`AnonymousSessionResponse`** (added in 2.0) This sets the response status code behavior on the [user endpoint](/bff/fundamentals/session/management/user/) to either return 401 or 200 with a *null* payload when the user is anonymous. * **`DiagnosticsEnvironments`** The ASP.NET environment names that enable the diagnostics endpoint. Defaults to “Development”. * **`BackChannelHttpHandler`** A HTTP message handler that’s used to configure backchannel communication. Typically used during testing. Configuring this will automatically configure the BackChannelHttpHandler property in *OpenIDConnectOptions* and also set it as the primary http message handler for retrieving the index.html. * **`AutomaticallyRegisterBffMiddleware`** (added in 4.0) When using BFF V4 with multiple frontends, several middlewares are automatically added to the pipeline (frontend selection, path mapping, OpenID Connect callbacks, management endpoints, and static file proxying). If you need full control over the middleware pipeline, set this to `false` and register the middleware manually: ```csharp builder.Services.AddBff(options => { options.AutomaticallyRegisterBffMiddleware = false; }); ``` When disabled, you must call `UseBffPreProcessing()` early in the pipeline (before authentication) and `UseBffPostProcessing()` at the end: ```csharp app.UseForwardedHeaders(); app.UseBffPreProcessing(); // Frontend selection, path mapping, OpenID callbacks app.UseAuthentication(); app.UseRouting(); app.UseBff(); // Anti-forgery checks app.UseAuthorization(); // map your endpoints here... app.UseBffPostProcessing(); // Management endpoints, remote API handling, static file proxying ``` You can also use the individual middleware methods for even more granular control: * `UseBffFrontendSelection()` — Selects the current frontend based on host/path matching * `UseBffPathMapping()` — Adjusts `PathBase` and `Path` for the selected frontend * `UseBffOpenIdCallbacks()` — Handles OpenID Connect callback requests * `UseBffAntiForgery()` — Validates anti-forgery headers (same as `UseBff()`) * `UseBffStaticFileProxying()` — Proxies static file requests to CDN or development server * **`StaticAssetsClientName`** If BFF is configured to automatically retrieve the `index.html`, or to proxy the static assets, it needs an HTTP client to do so. With this name, you can automatically configure this HTTP client in the `HttpClientFactory`. * **`AllowedSilentLoginReferers`** For silent login to work, you normally need to have the BFF backend and the frontend on the same origin. If you have a split host scenario, meaning the backend on a different origin (but same site) as the frontend, then you can use the referer header to differentiate which browser window to post the silent login results to. This array must then contain the list of allowed referer header values. ## Paths [Section titled “Paths”](#paths) * **`LoginPath`** Sets the path to the login endpoint. Defaults to */bff/login*. * **`SilentLoginPath`** Sets the path to the silent login endpoint. Defaults to */bff/silent-login*. * **`SilentLoginCallbackPath`** Sets the path to the silent login callback endpoint. Defaults to */bff/silent-login-callback*. * **`LogoutPath`** Sets the path to the logout endpoint. Defaults to */bff/logout*. * **`UserPath`** Sets the path to the user endpoint. Defaults to */bff/user*. * **`BackChannelLogoutPath`** Sets the path to the backchannel logout endpoint. Defaults to */bff/backchannel*. * **`DiagnosticsPath`** Sets the path to the diagnostics endpoint. Defaults to */bff/diagnostics*. ## Session Management [Section titled “Session Management”](#session-management) * **`ManagementBasePath`** Base path for management endpoints. Defaults to */bff*. * **`RequireLogoutSessionId`** Flag that specifies if the `sid` claim needs to be present in the logout request as query string parameter. Used to prevent cross site request forgery. Defaults to `true`. * **`RevokeRefreshTokenOnLogout`** Specifies if the user’s refresh token is automatically revoked at logout time. Defaults to `true`. * **`BackchannelLogoutAllUserSessions`** Specifies if during backchannel logout all matching user sessions are logged out. If `true`, all sessions for the subject will be revoked. If false, just the specific session will be revoked. Defaults to `false`. * **`~~EnableSessionCleanup~~`** (removed in V4) Indicates if expired server side sessions should be cleaned up. This requires an implementation of IUserSessionStoreCleanup to be registered in the ASP.NET Core service provider. Defaults to `false`. In V4, you need to opt into this value by calling `.AddSessionCleanupBackgroundProcess()` * **`SessionCleanupInterval`** Interval at which expired sessions are cleaned up. Defaults to *10 minutes*. ## APIs [Section titled “APIs”](#apis) * **`AntiForgeryHeaderName`** Specifies the name of the header used for anti-forgery header protection. Defaults to `X-CSRF`. * **`AntiForgeryHeaderValue`** Specifies the expected value of Anti-forgery header. Defaults to `1`. * **`DPoPJsonWebKey`** Specifies the Json Web Key to use when creating DPoP proof tokens. Defaults to null, which is appropriate when not using DPoP. * **`RemoveSessionAfterRefreshTokenExpiration`** Flag that specifies if a user session should be removed after an attempt to use a Refresh Token to acquire a new Access Token fails. This behavior is only triggered when proxying requests to remote APIs with TokenType.User or TokenType.UserOrClient. Defaults to True. * **`DisableAntiForgeryCheck`** (added in V4) A delegate that determines if the anti-forgery check should be disabled for a given request. The default is not to disable anti-forgery checks. ## CDN / Static Assets [Section titled “CDN / Static Assets”](#cdn--static-assets) * **`IndexHtmlDefaultCacheDuration`** (added in V4) If you use CDN Index HTML proxying (via `BffFrontend.CdnIndexHtmlUrl`), this controls how long the fetched `index.html` is cached. Defaults to *5 minutes*. ## Diagnostics [Section titled “Diagnostics”](#diagnostics) * **`Diagnostics`** (added in V4) Options that control the way that diagnostic data is logged. This is a nested options object with the following properties: * **`LogFrequency`** — Frequency at which diagnostic summaries are logged. Defaults to *1 hour*. * **`ChunkSize`** — Max size of diagnostic data log message chunks in bytes. Defaults to *8160 bytes* (8 KB minus 32 bytes for log message formatting overhead). ## BFF Blazor Server Options [Section titled “BFF Blazor Server Options”](#bff-blazor-server-options) In the Blazor Server, you configure the `BffBlazorServerOptions` by using the `AddBlazorServer` method. ```csharp builder.Services.AddBlazorServer(opt => { // configure options here.. }) ``` The following options are available: * **`ServerStateProviderPollingInterval`** The delay, in milliseconds, between polling requests by the BffServerAuthenticationStateProvider to the /bff/user endpoint. Defaults to 5000 ms. ## BFF Blazor Client Options [Section titled “BFF Blazor Client Options”](#bff-blazor-client-options) In WASM, you configure the `BffBlazorClientOptions` using the `AddBffBlazorClient` method: ```csharp builder.Services.AddBffBlazorClient(opt => { // configure options here... }) ``` The following options are available: * **`RemoteApiPath`** The base path to use for remote APIs. * **`RemoteApiBaseAddress`** The base address to use for remote APIs. If unset (the default), the blazor hosting environment’s base address is used. * **`StateProviderBaseAddress`** The base address to use for the state provider’s calls to the /bff/user endpoint. If unset (the default), the blazor hosting environment’s base address is used. * **`WebAssemblyStateProviderPollingDelay`** The delay, in milliseconds, before the BffClientAuthenticationStateProvider will start polling the /bff/user endpoint. Defaults to 1000 ms. * **`WebAssemblyStateProviderPollingInterval`** The delay, in milliseconds, between polling requests by the BffClientAuthenticationStateProvider to the /bff/user endpoint. Defaults to 5000 ms. ## Proxy Servers and Load Balancers v4.0 [Section titled “Proxy Servers and Load Balancers ”v4.0](#proxy-servers-and-load-balancers) When your BFF is hosted behind another reverse proxy or load balancer, you’ll want to use `X-Forwarded-*` headers. BFF automatically registers the `ForwardedHeaders` middleware in the pipeline, without any additional configuration. You will need to configure which headers should be considered by the middleware, typically the `X-Forwarded-For` and `X-Forwarded-Proto` headers. Here’s an example of how you can configure this. Program.cs ```csharp builder.Services.Configure(options => { // Consider configuring the 'KnownProxies' and the 'AllowedHosts' to prevent IP spoofing attacks options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); ``` See [proxy servers and load balancers](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-9.0) in the Microsoft documentation for more information. ----- # Authentication & Session Management > Learn how BFF manages authentication sessions — from the initial OIDC login to server-side session storage, token lifecycle, and back-channel logout. Authentication in a BFF application flows through several layers. Understanding how these layers connect helps you configure sessions correctly and debug problems when they arise. ## How Sessions Work [Section titled “How Sessions Work”](#how-sessions-work) ``` sequenceDiagram participant Browser participant BFF participant IdP as Identity Provider Browser->>BFF: GET /bff/login BFF->>IdP: OIDC Authorization Request IdP-->>Browser: Login UI Browser->>IdP: Credentials IdP-->>BFF: Authorization Code BFF->>IdP: Token Request IdP-->>BFF: Access Token + Refresh Token + ID Token BFF->>BFF: Store tokens in session BFF-->>Browser: Set-Cookie (session cookie) Note over Browser,BFF: Session established Browser->>BFF: GET /api/data (with cookie) BFF->>BFF: Validate session BFF->>BFF: Get/refresh access token BFF-->>Browser: API response ``` ### The Session Cookie [Section titled “The Session Cookie”](#the-session-cookie) After a successful login, BFF sets an **HttpOnly, Secure, SameSite** cookie in the browser. This cookie is the browser’s proof of session — it is sent automatically on every subsequent request to the BFF host. The cookie itself is signed and encrypted by ASP.NET Core’s data protection stack. The browser never has access to the access token or refresh token. These are stored server-side. ### Cookie-Based vs. Server-Side Sessions [Section titled “Cookie-Based vs. Server-Side Sessions”](#cookie-based-vs-server-side-sessions) By default, BFF stores session state (including tokens) inside the encrypted cookie. This works but has limitations: | | Cookie-Based (default) | Server-Side Sessions | | --------------------------- | ---------------------------------------------------- | ---------------------------------- | | **Token storage** | Inside the encrypted cookie | Server-side store (DB, memory) | | **Cookie size** | Grows with claims/tokens — can hit browser 4KB limit | Fixed small size (session ID only) | | **Server-initiated logout** | Not possible | ✅ Possible | | **Back-channel logout** | Not supported | ✅ Supported | | **Session visibility** | None | ✅ Query all active sessions | | **Scale-out** | Cookie encryption keys must be shared | Session store must be shared | ### Token Lifecycle [Section titled “Token Lifecycle”](#token-lifecycle) Tokens stored in the session are managed automatically: 1. **Access token** — When an API call is made through the BFF, the access token is retrieved from the session. If it is expired or close to expiring, BFF automatically refreshes it using the refresh token. 2. **Refresh token** — Stored server-side (in the session). Revoked automatically at logout. 3. **ID token** — Used during logout to send a `id_token_hint` to the identity provider. See [Token Management](/bff/fundamentals/tokens/) for how to access tokens programmatically. ## Management Endpoints [Section titled “Management Endpoints”](#management-endpoints) The BFF exposes several HTTP endpoints for managing the user’s session. These endpoints are called by the frontend to trigger authentication flows or query session state. | Endpoint | Default Path | Purpose | | ------------------- | ------------------- | --------------------------------------------- | | Login | `/bff/login` | Start the OIDC authentication flow | | Logout | `/bff/logout` | End the session and sign out | | User | `/bff/user` | Return current user claims and session state | | Silent Login | `/bff/silent-login` | Non-interactive login (deprecated in v4) | | Back-Channel Logout | `/bff/backchannel` | Receive server-to-server logout notifications | | Diagnostics | `/bff/diagnostics` | Show current tokens (development only) | ## In This Section [Section titled “In This Section”](#in-this-section) | Page | Description | | -------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | [Authentication Handlers](/bff/fundamentals/session/handlers/) | OIDC and cookie handler configuration | | [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/) | Persistent session storage with Entity Framework or custom stores | | [OIDC Prompts](/bff/fundamentals/session/oidc-prompts/) | Controlling interactive vs. silent authentication | | [Login Endpoint](/bff/fundamentals/session/management/login/) | How to trigger login from the frontend | | [Logout Endpoint](/bff/fundamentals/session/management/logout/) | How to trigger logout and CSRF protection | | [User Endpoint](/bff/fundamentals/session/management/user/) | Reading user claims and session state | | [Back-Channel Logout](/bff/fundamentals/session/management/back-channel-logout/) | Server-initiated session termination | | [Silent Login](/bff/fundamentals/session/management/silent-login/) | Non-interactive login (deprecated) | | [Diagnostics](/bff/fundamentals/session/management/diagnostics/) | Development-time token inspection | ## See Also [Section titled “See Also”](#see-also) [IdentityServer Configuration ](/identityserver/configuration/)Configure the identity provider your BFF authenticates against [IdentityServer Clients ](/identityserver/fundamentals/clients/)Register your BFF as a confidential client [IdentityServer Server-Side Sessions ](/identityserver/ui/server-side-sessions/)Coordinate logout across all components [Access Token Management ](/accesstokenmanagement/)How tokens are refreshed when sessions are active [Troubleshooting ](/bff/troubleshooting/)Common session and authentication issues ----- # ASP.NET Core Authentication System > Learn how to configure and use ASP.NET Core authentication handlers for OpenID Connect and cookie-based session management in BFF applications To configure authentication in the BFF, you’ll need to configure both the OpenID Connect login flow and the cookie handlers. ## Automatic Authentication Configuration V4 [Section titled “Automatic Authentication Configuration ”](#automatic-authentication-configuration) In V4, a simplified mechanism for wiring up authentication has been introduced. The main purpose for the BFF is to handle the OpenID Connect login flow and to protect the APIs using Cookies. In V3, you explicitly had to configure the ASP.NET Core authentication system to enable this. In V4, this is now simplified. A call `BffBuilder.ConfigureOpenIdConnect()` will make sure that: 1. The authentication pipeline is configured with the appropriate authentication schemes. 2. The OpenID Connect pipeline is configured with default values. 3. The CookieHandler is configured using recommended practices. This can be tweaked by calling `BffBuilder.ConfigureCookies()` Below is an example on how to configure the BFF’s authentication pipeline. ```csharp services.AddBff() .ConfigureOpenIdConnect(options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api"); options.Scope.Add("offline_access"); options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; }); ``` Each frontend can have custom OpenID Connect configuration and Cookie Configuration. This can both be configured programmatically via [Configuration](/bff/fundamentals/multi-frontend/configuration/). ## Manually Configuring Authentication [Section titled “Manually Configuring Authentication”](#manually-configuring-authentication) You typically use the following two ASP.NET Core authentication handlers to implement remote authentication: * the OpenID Connect authentication handler to interact with the remote OIDC / OAuth token service, e.g. Duende IdentityServer * the cookie handler to do local session management The BFF relies on the configuration of the ASP.NET Core default authentication schemes. Both the OpenID Connect authentication handler and cookie handler need to be configured, with the ASP.NET Core authentication system default schemes specified: * `DefaultScheme` should be the cookie handler, so the BFF can do local session management; * `DefaultChallengeScheme` should be the OpenID Connect handler, so the BFF defaults to remote authentication; * `DefaultSignOutScheme` should be the OpenID Connect handler, so the BFF uses remote sign-out. A minimal configuration looks like this: ```csharp builder.Services.AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("cookie", options => { // ... }) .AddOpenIdConnect("oidc", options => { // ... }); ``` Now let’s look at some more details! ### The OpenID Connect Authentication Handler [Section titled “The OpenID Connect Authentication Handler”](#the-openid-connect-authentication-handler) The OpenID Connect (OIDC) handler connects the application to the authentication / access token system. It can be configured to use any OpenID Connect provider: [Duende IdentityServer](https://duendesoftware.com/products/identityserver/), [Microsoft Entra ID](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id), [Auth0](https://auth0.com/), [Google Cloud Identity Platform](https://cloud.google.com/identity-platform), [Amazon Cognito](https://aws.amazon.com/cognito/), and more. The exact settings to use depend on the OIDC provider and its configuration settings. We recommend to: * use authorization code flow with PKCE * use a `response_mode` of `query` since this plays nicer with `SameSite` cookies * use a strong client secret. Since the BFF can be a confidential client, it is possible to use strong client authentication like JWT assertions, JAR, or mTLS. Shared secrets work as well. * turn off inbound claims mapping * save the tokens into the authentication session so they can be automatically managed * request a refresh token using the `offline_access` scope ```csharp builder.Services.AddAuthentication().AddOpenIdConnect("oidc", options => { options.Authority = "https://demo.duendesoftware.com"; // confidential client using code flow + PKCE options.ClientId = "spa"; options.ClientSecret = "secret"; options.ResponseType = "code"; // query response type is compatible with strict SameSite mode options.ResponseMode = "query"; // get claims without mappings options.MapInboundClaims = false; options.GetClaimsFromUserInfoEndpoint = true; // save tokens into authentication session // to enable automatic token management options.SaveTokens = true; // request scopes options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("API"); // and refresh token options.Scope.Add("offline_access"); }); ``` The OIDC handler will use the default sign-in handler (the cookie handler) to establish a session after successful validation of the OIDC response. ### The Cookie Handler [Section titled “The Cookie Handler”](#the-cookie-handler) The cookie handler is responsible for establishing the session and manage authentication session related data. Things to consider: * determine the session lifetime and if the session lifetime should be sliding or absolute * it is recommended to use a cookie name [prefix](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-07#section-4.1.3) if compatible with your application * use the highest available `SameSite` mode that is compatible with your application, e.g. `strict`, but at least `lax` ```csharp builder.Services.AddAuthentication().AddCookie("cookie", options => { // set session lifetime options.ExpireTimeSpan = TimeSpan.FromHours(8); // sliding or absolute options.SlidingExpiration = false; // host prefixed cookie name options.Cookie.Name = "__Host-spa"; // strict SameSite handling options.Cookie.SameSite = SameSiteMode.Strict; }); ``` ### Choosing Between SameSite.Lax and SameSite.Strict [Section titled “Choosing Between SameSite.Lax and SameSite.Strict”](#choosing-between-samesitelax-and-samesitestrict) The [SameSite cookie](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value) is a feature of modern browsers that restricts cookies so that they are only sent to pages originating from the [site](https://developer.mozilla.org/en-US/docs/Glossary/Site) where the cookie was originally issued. This prevents CSRF attacks and helps with improving privacy, because cross-site requests will no longer implicitly include the user’s credentials. If you configure `SameSiteMode.Strict`, this means that if a user originates from an external site and is redirected or linked to the BFF application, then the authentication cookie is not sent automatically. So, the application will consider the user to be not logged in, even though there may be a valid authentication cookie in the cookie jar. If the user refreshes the page, or visits a link on your site that forces a complete page reload, then the authentication cookie will be sent along normally again. This also happens when you have an identity provider that’s hosted on a different site than the BFF, in combination with `SameSiteMode.Strict`. After successful authentication at the IdP, the user will be redirected back to the BFF site. The server will then place an authentication cookie in the browser, but the browser will not automatically include it in subsequent requests until the full page is manually reloaded by the user. This means the user appears to still be logged out, even though the cookie is there. So, if you have an Identity Provider that’s hosted under a different site than your BFF, you may want to configure your cookie policy to be `SameSiteMode.Lax`. ----- # BFF Session Management Endpoints > Overview of Duende.BFF endpoints for session management operations including login, logout, and user information retrieval Duende.BFF adds endpoints for performing typical session-management operations such as triggering login and logout and getting information about the currently logged-on user. These endpoint are meant to be called by the frontend. In addition, Duende.BFF adds an implementation of the OpenID Connect back-channel notification endpoint to overcome the restrictions of third party cookies in front-channel notification in modern browsers. You enable the endpoints by adding the relevant services into the ASP.NET Core service provider: Program.cs ```csharp // Add BFF services to DI - also add server-side session management builder.Services.AddBff(options => { // default value options.ManagementBasePath = "/bff"; }; ``` Starting with BFF v4, the BFF automatically wires up the management endpoints. If you disable this behavior (using `AutomaticallyRegisterBffMiddleware`), this is how you can map the management endpoints: Program.cs ```csharp var app = builder.Build(); // Preprocessing pipeline, which would have been automatically added to start of the request the pipeline. app.UseBffPreProcessing(); // Your logic, such as: app.UseRouting(); app.UseBff(); // post processing pipeline that would have been automatically added to the end of the request pipeline. app.UseBffPostProcessing(); app.Run(); ``` The `UsePreprocessing` method adds all handling for multiple frontend support. Alternatively, you can call these methods direct: ```csharp app.UseBffFrontendSelection(); app.UseBffPathMapping(); app.UseBffOpenIdCallbacks(); ``` `UseBffPostProcessing` adds all BFF management endpoints and handlers for proxying `index.html`. You can also map each endpoint individually by calling the various `MapBffManagementXxxEndpoint` methods, for example `endpoints.MapBffManagementLoginEndpoint()`. The following pages describe the default behavior of the management endpoints. See the [extensibility](/bff/extensibility) section for information about how to customize the behavior of the endpoints. ----- # BFF Back-Channel Logout Endpoint > Documentation for the OpenID Connect Back-Channel Logout endpoint implementation in BFF, enabling server-to-server session termination without browser involvement. The */bff/backchannel* endpoint is an implementation of the [OpenID Connect Back-Channel Logout](https://openid.net/specs/openid-connect-backchannel-1_0.html) specification. The remote identity provider can use this endpoint to end the BFF’s session via a server to server call, without involving the user’s browser. This design avoids problems with 3rd party cookies associated with front-channel logout. ## Typical Usage [Section titled “Typical Usage”](#typical-usage) The back-channel logout endpoint is invoked by the remote identity provider when it determines that sessions should be ended. IdentityServer will send back-channel logout requests if you [configure](/identityserver/reference/v8/models/client/#authentication--session-management) your client’s `BackChannelLogoutUri`. When a session ends at IdentityServer, any client that was participating in that session that has a back-channel logout URI configured will be sent a back-channel logout request. This typically happens when another application signs out. [Expiration](/identityserver/ui/server-side-sessions/session-expiration/) of [IdentityServer server side sessions](/identityserver/ui/server-side-sessions/) can also be configured to send back-channel logout requests, though this is disabled by default. ## Dependencies [Section titled “Dependencies”](#dependencies) The back-channel logout endpoint depends on [server-side sessions in the BFF](/bff/fundamentals/session/server-side-sessions/), which must be enabled to use this endpoint. Note that such server-side sessions are distinct from server-side sessions in IdentityServer. ## Revoke All Sessions [Section titled “Revoke All Sessions”](#revoke-all-sessions) Back-channel logout tokens include a sub (subject ID) and sid (session ID) claim to describe which session should be revoked. By default, the back-channel logout endpoint will only revoke the specific session for the given subject ID and session ID. Alternatively, you can configure the endpoint to revoke every session that belongs to the given subject ID by setting the `BackchannelLogoutAllUserSessions` [option](/bff/fundamentals/options/#session-management) to true. ## Customize This Endpoint [Section titled “Customize This Endpoint”](#customize-this-endpoint) To add custom request processing logic or customize session revocation behavior, see [Back-Channel Logout Endpoint Extensibility](/bff/extensibility/management/back-channel-logout/). ----- # BFF Diagnostics Endpoint > Learn about the BFF diagnostics endpoint that provides access to user and client access tokens for development testing purposes. The `/bff/diagnostics` endpoint returns the current user and client access token for testing purposes. The endpoint tries to retrieve and show current tokens. It may invoke both a refresh token flow for the user access token and a client credential flow for the client access token. To use the diagnostics endpoint, make a `GET` request to `/bff/diagnostics`. Typically, this is done in a browser to diagnose a problem during development. ## Customize This Endpoint [Section titled “Customize This Endpoint”](#customize-this-endpoint) To add custom logic to the diagnostics endpoint, see [Diagnostics Endpoint Extensibility](/bff/extensibility/management/diagnostics/). ----- # BFF Login Endpoint > Learn how to initiate authentication and handle return URLs using the BFF login endpoint in your frontend applications The */bff/login* endpoint begins the authentication process. To use it, typically javascript code will navigate away from the frontend application to the login endpoint: ```js window.location = "/login"; ``` In Blazor, instead use the `NavigationManager` to navigate to the login endpoint: ```csharp Navigation.NavigateTo($"bff/login", forceLoad: true); ``` The login endpoint triggers an authentication challenge using the default challenge scheme, which will typically use the OpenID Connect [handler](/bff/fundamentals/session/handlers/). ## Return Url [Section titled “Return Url”](#return-url) After authentication is complete, the login endpoint will redirect back to your front end application. By default, this redirect goes to the root of the application. You can use a different URL instead by including a local URL as the *returnUrl* query parameter. ```js window.location = "/login?returnUrl=/logged-in"; ``` ## Customize This Endpoint [Section titled “Customize This Endpoint”](#customize-this-endpoint) To add custom logic before or after the login endpoint processes a request, see [Login Endpoint Extensibility](/bff/extensibility/management/login/). ----- # BFF Logout Endpoint > Learn how to use the BFF logout endpoint to sign out users and handle CSRF protection in your application The */bff/logout* endpoint signs out of the appropriate ASP.NET Core [authentication schemes](/bff/fundamentals/session/handlers/) to both delete the BFF’s session cookie and to sign out from the remote identity provider. To use the logout endpoint, typically your javascript code will navigate away from your front end to the logout endpoint, similar to the login endpoint. However, unlike the login endpoint, the logout endpoint requires CSRF protection, otherwise an attacker could destroy sessions by making cross-site GET requests. The session id is used to provide this CSRF protection by requiring it as a query parameter to the logout endpoint (assuming that a session id was included during login). For convenience, the correct logout url is made available as a claim in the */bff/user* endpoint, making typical logout usage look like this: ```js var logoutUrl = userClaims["bff:logout_url"]; // assumes userClaims is the result of a call to /bff/user window.location = logoutUrl; ``` ## Return Url [Section titled “Return Url”](#return-url) After signout is complete, the logout endpoint will redirect back to your front end application. By default, this redirect goes to the root of the application. You can use a different URL instead by including a local URL as the *returnUrl* query parameter. ```js var logoutUrl = userClaims["bff:logout_url"]; window.location = `${logoutUrl}&returnUrl=/logged-out`; ``` ## Revocation Of Refresh Tokens [Section titled “Revocation Of Refresh Tokens”](#revocation-of-refresh-tokens) If the user has a refresh token, the logout endpoint can revoke it. This is enabled by default because revoking refresh tokens that will not be used anymore is generally good practice. Normally any refresh tokens associated with the current session won’t be used after logout, as the session where they are stored is deleted as part of logout. However, you can disable this revocation with the `RevokeRefreshTokenOnLogout` option. ## Customize This Endpoint [Section titled “Customize This Endpoint”](#customize-this-endpoint) To add custom logic before or after the logout endpoint processes a request, see [Logout Endpoint Extensibility](/bff/extensibility/management/logout/). ----- # BFF Silent Login Endpoint > Endpoint for non-interactive authentication using an existing session at the remote identity provider **Added in v1.2.0.** The */bff/silent-login* endpoint triggers authentication similarly to the login endpoint, but in a non-interactive way. The expected usage pattern is that the application code loads in the browser and triggers a request to the *User Endpoint*. If that indicates that there is no BFF session, then the *Silent Login Endpoint* can be requested to attempt to automatically log the user in, using an existing session at the remote identity provider. This non-interactive design relies upon the use of an *iframe* to make the silent login request. The result of the silent login request in the *iframe* will then use *postMessage* to notify the parent window of the outcome. If the result is that a session has been established, then the application logic can either re-trigger a call to the *User Endpoint*, or reload the entire page (depending on the preferred design). If the result is that a session has not been established, then the application redirects to the login endpoint to log the user in interactively. To trigger the silent login, the application code must have an *iframe* and then set its *src* to the silent login endpoint. For example in your HTML: ```html ``` And then in JavaScript: ```javascript document.querySelector('#bff-silent-login').src = '/bff/silent-login'; ``` To receive the result, the application should handle the *message* event in the browser and look for the *data.isLoggedIn* property on the event object: ```javascript window.addEventListener("message", e => { if (e.data && e.data.source === 'bff-silent-login' && e.data.isLoggedIn) { // we now have a user logged in silently, so reload this window window.location.reload(); } }); ``` ## Customize This Endpoint [Section titled “Customize This Endpoint”](#customize-this-endpoint) To add custom logic to the silent login endpoint, see [Silent Login Endpoint Extensibility](/bff/extensibility/management/silent-login/). ----- # BFF User Endpoint > The BFF user endpoint provides information about the currently authenticated user and their session status The */bff/user* endpoint returns data about the currently logged-on user and the session. It is typically invoked at application startup to check if the user has authenticated, and if so, to get profile data about the user. It can also be used to periodically query if the session is still valid. ## Output [Section titled “Output”](#output) If there is no current session, the user endpoint returns a response indicating that the user is anonymous. By default, this is a 401 status code, but this can be [configured](#anonymous-session-response-option). If there is a current session, the user endpoint returns a JSON array containing the claims in the ASP.NET Core authentication session and several BFF specific claims. For example: ```json [ { "type": "sid", "value": "173E788068FFB728806501F4F46C52D6" }, { "type": "sub", "value": "88421113" }, { "type": "idp", "value": "local" }, { "type": "name", "value": "Bob Smith" }, { "type": "bff:logout_url", "value": "/logout?sid=173E788068FFB728806501F4F46C52D6" }, { "type": "bff:session_expires_in", "value": 28799 }, { "type": "bff:session_state", "value": "q-Hl1V9a7FCZE5o-vH9qpmyVKOaeVfMQBUJLrq-lDJU.013E58C33C7409C6011011B8291EF78A" } ] ``` ## User Claims [Section titled “User Claims”](#user-claims) Since the user endpoint returns the claims that are in the ASP.NET Core session, anything that changes the session will be reflected in its output. You can customize the contents of the session via the OpenID Connect handler’s [ClaimAction](https://docs.microsoft.com/en-us/dotnet/API/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions?view=aspnetcore-7.0) infrastructure, or by using [claims transformation](https://docs.microsoft.com/en-us/dotnet/API/microsoft.aspnetcore.authentication.iclaimstransformation?view=aspnetcore-7.0). For example, if you add a [claim](/identityserver/fundamentals/claims/) to the [userinfo endpoint](/identityserver/reference/v8/endpoints/userinfo/) at IdentityServer that you would like to include in the */bff/user* endpoint, you need to add a corresponding ClaimAction in the BFF’s OpenID Connect Handler to include the claim in the BFF’s session. ## Management Claims [Section titled “Management Claims”](#management-claims) In addition to the claims in the ASP.NET Core Session, Duende.BFF adds three additional claims: **bff:session\_expires\_in** This is the number of seconds the current session will be valid for. **bff:session\_state** This is the session state value of the upstream OIDC provider that can be used for the JavaScript *check\_session* mechanism (if provided). **bff:logout\_url** This is the URL to trigger logout. If the upstream provider includes a `sid` claim, the BFF logout endpoint requires this value as a query string parameter for CSRF protection. This behavior can be configured with the `RequireLogoutSessionId` in the [options](/bff/fundamentals/options/). ## Typical Usage [Section titled “Typical Usage”](#typical-usage) To use the endpoint, make an HTTP GET request to it from your frontend javascript code. For example, your application could use the fetch API to make requests to the user endpoint like this: session.js ```js var req = new Request("/bff/user", { headers: new Headers({ "X-CSRF": "1", }), }); var resp = await fetch(req); if (resp.ok) { userClaims = await resp.json(); console.log("user logged in", userClaims); } else if (resp.status === 401) { console.log("user not logged in"); } ``` ## Cross-Site Request Forgery [Section titled “Cross-Site Request Forgery”](#cross-site-request-forgery) To protect against cross-site request forgery, you need to add a [static header](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-of-custom-request-headers) to the GET request. The header’s name and required value can be configured in the [options](/bff/fundamentals/options/). ## Anonymous Session Response Option [Section titled “Anonymous Session Response Option”](#anonymous-session-response-option) The `AnonymousSessionResponse` option allows you to change the behavior of the user endpoint to return 200 instead of 401 when the user is anonymous. If `AnonymousSessionResponse` is set to `AnonymousSessionResponse.Response200`, then the endpoint’s response will set its status code to 200 and its payload will contain the literal `null` (the response body will be the characters ‘n’, ‘u’, ‘l’, ‘l’ without quotes). ## Cookie Sliding [Section titled “Cookie Sliding”](#cookie-sliding) If your ASP.NET Core session cookie is configured to use a sliding expiration, you need to be able to query the session state without extending the session’s lifetime; a periodic check for user activity shouldn’t itself count as user activity. To prevent the call to the user endpoint from sliding the cookie, add the *slide=false* parameter to the request. site.js ```js var req = new Request("/bff/user?slide=false", { headers: new Headers({ "X-CSRF": "1", }), }); ``` ## Customize This Endpoint [Section titled “Customize This Endpoint”](#customize-this-endpoint) To add custom logic, enrich user claims, or change the claims returned by this endpoint, see [User Endpoint Extensibility](/bff/extensibility/management/user/). ----- # OpenID Connect Prompts > OpenID Connect prompt support in Duende BFF V4 OpenID Connect supports a `prompt` parameter that can be used to control the user experience as it relates to the current authentication session. Duende BFF v4 supports this parameter by forwarding it to the backing identity provider to allow for more fine-grained control during unique client interactions. This documentation outlines the `prompt` parameter support and what values you might use to achieve different outcomes. ## Prompt parameter options [Section titled “Prompt parameter options”](#prompt-parameter-options) The [OpenID Connect specification](https://openid.net/specs/openid-connect-core-1_0.html) defines an **optional** `prompt` parameter that can be used to control the user experience as it relates to the current authentication session. The following values are supported: | value | description | | ---------------- | ------------------------------------------------------------------------------------------------- | | `none` | Must not display any authentication or consent user interface | | `login` | Should prompt the user to reauthenticate | | `consent` | Should prompt the user for consent | | `select_account` | Should prompt user to choose an account given their are multiple accounts for the current session | These values can be passed to the BFF by adding them to the `prompt` query parameter to the login request URL. For example, the following request would prompt the user to reauthenticate: ```http /bff/login?prompt=login ``` The inclusion of the `prompt` parameter in the login request URL will cause the BFF to forward it to the backing identity provider at which point the identity provider will determine the appropriate user experience based on the value of the `prompt` parameter. For example, if the `prompt` parameter is set to `login`, the identity provider will prompt the user to reauthenticate. ## Scenarios and Situations [Section titled “Scenarios and Situations”](#scenarios-and-situations) The `prompt` parameter can be used in situations where additional security is required, you want to reestablish the account identity, or a high-impact action is about to be taken. For example, the following hypothetical scenarios might require the use of the `prompt` parameter: * Attempting to transfer funds from a bank account to another * A destructive action such as deleting an account * Performing an action that alters a high-value account setting such as an email address ## Silent Login Deprecation (v3 to v4) [Section titled “Silent Login Deprecation (v3 to v4)”](#silent-login-deprecation-v3-to-v4) When migrating from Duende BFF v3 to v4, you may notice deprecation warnings regarding the [silent login](/bff/fundamentals/session/management/silent-login/) feature located at the management endpoint `/silent-login`. To resolve the warning, update the silent login URL in your frontend applications to point to the login endpoint instead, including the `prompt=none` query parameter: ```diff const silentLoginPath = '/bff/silent-login'; const silentLoginPath = '/bff/login?prompt=none'; ``` By default, BFF v4 [automatically registers the management endpoints](/bff/fundamentals/session/management/). In case you opted out of the automatic registration feature, you may still need to explicitly call `app.MapBffManagementSilentLoginEndpoints()` if you are manually mapping the management endpoints. ----- # Server-Side Sessions > Learn how to implement and configure server-side sessions in BFF to manage user session data storage and enable session revocation capabilities By default, ASP.NET Core’s cookie handler will store all user session data in a protected cookie. This works very well unless cookie size or revocation becomes an issue. Duende.BFF includes all the plumbing to store your sessions server-side. The cookie will then only be used to transmit the session ID between the browser and the BFF host. This has the following advantages * the cookie size will be very small and constant - regardless how much data (e.g. token or claims) is stored in the authentication session * the session can be also revoked outside the context of a browser interaction, for example when receiving a back-channel logout notification from the upstream OpenID Connect provider ## Configuring Server-Side Sessions [Section titled “Configuring Server-Side Sessions”](#configuring-server-side-sessions) Server-side sessions can be enabled in the application’s startup: ```csharp builder.Services.AddBff() .AddServerSideSessions(); ``` The default implementation stores the session in-memory. This is useful for testing, but for production you typically want a more robust storage mechanism. We provide an implementation of the session store built with EntityFramework (EF) that can be used with any database with an EF provider (e.g. Microsoft SQL Server). You can also use a custom store. See [extensibility](/bff/extensibility/sessions/#user-session-store) for more information. ## Using Entity Framework for the Server-Side Session Store [Section titled “Using Entity Framework for the Server-Side Session Store”](#using-entity-framework-for-the-server-side-session-store) To use the EF session store, install the `Duende.BFF.EntityFramework` NuGet package: ```bash dotnet add package Duende.BFF.EntityFramework ``` Next, you can register the session store by calling `AddEntityFrameworkServerSideSessions`, like this: ```csharp var cn = _configuration.GetConnectionString("db"); builder.Services.AddBff() .AddEntityFrameworkServerSideSessions(options=> { options.UseSqlServer(cn); }); ``` The method of `AddEntityFrameworkServerSideSessions` registers the `SessionDbContext` along with a `UserSessionStore` as transient dependencies. For developers looking to take advantage of DbContext pooling or have more fine-grained control over their DbContext creation registration and creation process, you can use the `AddEntityFrameworkServerSideSessionsServices` method instead. This method registers all the required services for server-side session except for the `SessionDbContext`, which will now be managed by the DbContext pooling mechanism. ```csharp var cn = _configuration.GetConnectionString("db"); builder.Services.AddDbContextPool(opt => { // configure your db context pool options here options.UseSqlServer(cn); }); builder.Services.AddBff() .AddEntityFrameworkServerSideSessionsServices() ``` Note, you’ll still need to let the server side session store know about the `SessionDbContext` by calling `AddEntityFrameworkServerSideSessions` with the `SessionDbContext` implementation as a generic argument. ### Entity Framework Migrations [Section titled “Entity Framework Migrations”](#entity-framework-migrations) Most data stores that you might use with Entity Framework use a schema to define the structure of their data. `Duende.BFF.EntityFramework` doesn’t make any assumptions about the underlying datastore, how (or indeed even if) it defines its schema, or how schema changes are managed by your organization. For these reasons, Duende does not directly support database creation, schema changes, or data migration by publishing database scripts. You are expected to manage your database in the way your organization sees fit. Using EF migrations is one possible approach to that, which Duende facilitates by publishing entity classes in each version of `Duende.BFF.EntityFramework`. An example project that uses those entities to create migrations is [here](https://github.com/DuendeSoftware/products/tree/main/bff/migrations/UserSessionDb). To quickly create Entity Framework migrations, run the following command in the project directory that has access to Entity Framework Core’s tools: ```bash dotnet ef migrations add UserSessions -o Migrations -c SessionDbContext ``` The project must also reference the `Duende.BFF.EntityFramework` NuGet package and the `Microsoft.EntityFrameworkCore.Design` NuGet package, along with a specific database provider and its corresponding configuration, including the connection string. ## Session Store Cleanup [Section titled “Session Store Cleanup”](#session-store-cleanup) Added in v1.2.0. Abandoned sessions will remain in the store unless something removes the stale entries. * V4 If you wish to have such sessions cleaned up periodically, then you can add the session cleanup host and configure the `SessionCleanupInterval` options: Program.cs ```csharp builder.Services.AddBff(options => { options.SessionCleanupInterval = TimeSpan.FromMinutes(5); }) .AddServerSideSessions(); ``` This requires an implementation of [`IUserSessionStoreCleanup`](/bff/extensibility/sessions#user-session-store-cleanup) in the ASP.NET Core service provider. If using Entity Framework Core, then the `IUserSessionStoreCleanup` implementation is provided for you when you use `AddEntityFrameworkServerSideSessions`. You can then add the `SessionCleanupBackgroundProcess`: Program.cs ```csharp var cn = _configuration.GetConnectionString("db"); builder.Services.AddBff() .AddEntityFrameworkServerSideSessions(options => { options.UseSqlServer(cn); }) .AddSessionCleanupBackgroundProcess(); ``` * V3 If you wish to have such sessions cleaned up periodically, then you can configure the `EnableSessionCleanup` and `SessionCleanupInterval` options: Program.cs ```csharp builder.Services.AddBff(options => { options.EnableSessionCleanup = true; options.SessionCleanupInterval = TimeSpan.FromMinutes(5); }) .AddServerSideSessions(); ``` This requires an implementation of [`IUserSessionStoreCleanup`](/bff/extensibility/sessions#user-session-store-cleanup) in the ASP.NET Core service provider. If using Entity Framework Core, then the `IUserSessionStoreCleanup` implementation is provided for you when you use `AddEntityFrameworkServerSideSessions`. Just enable session cleanup: Program.cs ```csharp var cn = _configuration.GetConnectionString("db"); builder.Services.AddBff(options => { options.EnableSessionCleanup = true; }) .AddEntityFrameworkServerSideSessions(options => { options.UseSqlServer(cn); }); ``` ----- # Token Management > Learn how to manage and utilize access tokens in BFF applications for secure API communication Duende.BFF includes an automatic token management feature. This uses the access and refresh token stored in the authentication session to always provide a current access token for outgoing API calls. For most scenarios, there is no additional configuration necessary. The token management will infer the configuration and token endpoint URL from the metadata of the OpenID Connect provider. The easiest way to retrieve the current access token is to use an extension method on `HttpContext`: ```csharp var token = await HttpContext.GetUserAccessTokenAsync(); ``` You can then use the token to set it on an `HttpClient`instance: ```csharp var client = new HttpClient(); client.SetBearerToken(token); ``` We recommend to use the `HttpClientFactory` to create HTTP clients that are already aware of the token management plumbing. For this you would register a named client in your application startup e.g. like this: Program.cs ```csharp // registers HTTP client that uses the managed user access token builder.Services.AddUserAccessTokenHttpClient("apiClient", configureClient: client => { client.BaseAddress = new Uri("https://remoteServer/"); }); ``` And then retrieve a client instance like this: ```csharp app.MapGet("/myApi", async (IHttpClientFactory httpClientFactory, HttpContext context) => { // create HTTP client with automatic token management var client = httpClientFactory.CreateClient("apiClient"); // call remote API var response = await client.GetAsync("remoteApi"); // rest omitted }); ``` If you prefer to use typed clients, you can do that as well: ```csharp // registers a typed HTTP client with token management support services.AddHttpClient(client => { client.BaseAddress = new Uri("https://remoteServer/"); }).AddUserAccessTokenHandler(); ``` And then use that client, for example like this on an endpoint: ```csharp app.MapGet("/myApi", async (MyTypedClient client) => { var response = await client.GetData(); // rest omitted }); ``` The client will internally always try to use a current and valid access token. If for any reason this is not possible, the 401 status code will be returned to the caller. ### Reuse of Refresh Tokens [Section titled “Reuse of Refresh Tokens”](#reuse-of-refresh-tokens) We recommend that you configure IdentityServer to issue reusable refresh tokens to BFF clients. Because the BFF is a confidential client, it does not need one-time use refresh tokens. Reusable refresh tokens are desirable because they avoid performance and user experience problems associated with one time use tokens. See the discussion on [rotating refresh tokens](/identityserver/tokens/refresh/) and the [OAuth 2.0 Security Best Current Practice](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.2.2) for more details. ### Manually revoking refresh tokens [Section titled “Manually revoking refresh tokens”](#manually-revoking-refresh-tokens) Duende.BFF revokes refresh tokens automatically at logout time. This behavior can be disabled with the *RevokeRefreshTokenOnLogout* option. If you want to manually revoke the current refresh token, you can use the following code: ```csharp await HttpContext.RevokeUserRefreshTokenAsync(); ``` This will invalidate the refresh token at the token service. ## See Also [Section titled “See Also”](#see-also) * [Access Token Management](/accesstokenmanagement/) — The `Duende.AccessTokenManagement` library that powers BFF token refresh * [User Token Management](/accesstokenmanagement/web-apps/) — Detailed user token lifecycle documentation * [Client Credential Tokens](/accesstokenmanagement/workers/) — Machine-to-machine token management * [IdentityServer Refresh Tokens](/identityserver/tokens/refresh/) — Configuring refresh token rotation and reuse * [IdentityServer Client Configuration](/identityserver/configuration/dcr/) — Setting up confidential BFF clients * [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/) — Where tokens are stored server-side ----- # Getting started > A collection of getting started guides to start with the BFF Currently, the most recent version is v4. If you’re upgrading from a previous version, please check our [upgrade guides](/bff/upgrading). If you’re starting a new BFF project, consider the following startup guides: * [Single frontend BFF](/bff/getting-started/single-frontend/) * [Multi-frontend BFF](/bff/getting-started/multi-frontend/) * [Blazor](/bff/getting-started/blazor/) ## Applying the Duende Backend for Frontend (BFF) Security Framework [Section titled “Applying the Duende Backend for Frontend (BFF) Security Framework”](#applying-the-duende-backend-for-frontend-bff-security-framework) [YouTube video player](https://www.youtube.com/embed/6zMSwlGBmxs) ----- # Blazor Applications > A walkthrough showing how to set up and configure a BFF (Backend For Frontend) application using Blazor This quickstart walks you through how to create a BFF Blazor application. The source code for this quickstart is available on [GitHub](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/BlazorAutoRendering). ## What You’ll Build [Section titled “What You’ll Build”](#what-youll-build) By the end of this guide you will have a Blazor application (Server + WASM) that authenticates users via OpenID Connect, stores session state server-side through the BFF, and calls a weather API using a BFF-managed HTTP client — with no access tokens exposed to the browser. ## Creating the project structure [Section titled “Creating the project structure”](#creating-the-project-structure) 1. **Create a Blazor App** ```shell mkdir BlazorBffApp cd BlazorBffApp dotnet new blazor --interactivity auto --all-interactive ``` This creates a Blazor application with a Server project and a client project. 2. **Configure the BffApp Server Project** To configure the server, the first step is to add the BFF Blazor package. ```shell cd BlazorBffApp dotnet add package Duende.BFF.Blazor ``` Then configure the application to use BFF. Add this to your services: * Duende BFF v4 ```csharp // BFF setup for Blazor builder.Services.AddBff() .ConfigureOpenIdConnect(options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api"); options.Scope.Add("offline_access"); options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; }) .ConfigureCookies(options => { // Because we use an identity server that's configured on a different site // (duendesoftware.com vs localhost), we need to configure the SameSite property to Lax. // Setting it to Strict would cause the authentication cookie not to be sent after logging in. // The user would have to refresh the page to get the cookie. // Recommendation: Set it to 'strict' if your IDP is on the same site as your BFF. options.Cookie.SameSite = SameSiteMode.Lax; }) .AddServerSideSessions() // Add in-memory implementation of server-side sessions .AddBlazorServer(); // Make sure authentication state is available to all components. builder.Services.AddCascadingAuthenticationState(); builder.Services.AddAuthorization(); ``` * Duende BFF v3 ```csharp // BFF setup for Blazor (v3) builder.Services.AddBff() .AddServerSideSessions() // Add in-memory implementation of server-side sessions .AddBlazorServer(); // Configure the authentication builder.Services .AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("cookie", options => { // Configure the cookie with __Host prefix for maximum security options.Cookie.Name = "__Host-blazor"; // Because we use an identity server that's configured on a different site // (duendesoftware.com vs localhost), we need to configure the SameSite property to Lax. // Setting it to Strict would cause the authentication cookie not to be sent after logging in. // The user would have to refresh the page to get the cookie. // Recommendation: Set it to 'strict' if your IDP is on the same site as your BFF. options.Cookie.SameSite = SameSiteMode.Lax; }) .AddOpenIdConnect("oidc", options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api"); options.Scope.Add("offline_access"); options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; }); // Make sure authentication state is available to all components. builder.Services.AddCascadingAuthenticationState(); builder.Services.AddAuthorization(); ``` To configure the web app pipeline, add the following after `builder.Build()`: * Duende BFF v4 ```csharp app.UseRouting(); app.UseAuthentication(); // Add the BFF middleware which performs anti-forgery protection app.UseBff(); app.UseAuthorization(); app.UseAntiforgery(); // In v4, management endpoints (/bff/login, /bff/logout, etc.) are // registered automatically — no call to MapBffManagementEndpoints() needed. ``` * Duende BFF v3 ```csharp app.UseRouting(); app.UseAuthentication(); // Add the BFF middleware which performs anti-forgery protection app.UseBff(); app.UseAuthorization(); app.UseAntiforgery(); // In v3, management endpoints must be registered explicitly app.MapBffManagementEndpoints(); ``` ## Configuring the BffApp.Client project [Section titled “Configuring the BffApp.Client project”](#configuring-the-bffappclient-project) 1. **Configure the Client Project** To add the BFF to the client project, add the following: ```shell cd .. cd BlazorBffApp.Client dotnet add package Duende.BFF.Blazor.Client ``` Then add the following to your `Program.cs`: ```csharp builder.Services .AddBffBlazorClient(); // Provides auth state provider that polls the /bff/user endpoint builder.Services .AddCascadingAuthenticationState(); ``` Your application is ready to use BFF now. ## Configuring your application to use BFF’s features [Section titled “Configuring your application to use BFF’s features”](#configuring-your-application-to-use-bffs-features) Add the following components to your `BlazorBffApp.Client/Components` folder: 1. **LoginDisplay.razor** The following code shows a login / logout button depending on your authentication state. Note: use the logout link from the `LogoutUrl` claim, because it contains both the correct route and the session id. BlazorBffApp.Client/Components/LoginDisplay.razor ```razor @using Duende.Bff.Blazor.Client @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.Extensions.Options @rendermode InteractiveAuto @inject IOptions Options Hello, @context.User.Identity?.Name Log Out Log in Log in @code { string BffLogoutUrl(AuthenticationState context) { var logoutUrl = context.User.FindFirst(Constants.ClaimTypes.LogoutUrl); return $"{Options.Value.StateProviderBaseAddress}{logoutUrl?.Value}"; } } ``` 2. **RedirectToLogin.razor** The following code will redirect users to the identity provider for authentication. Once authentication is complete, users will be redirected back to where they came from. BlazorBffApp.Client/Components/RedirectToLogin.razor ```razor @inject NavigationManager Navigation @rendermode InteractiveAuto @code { protected override void OnInitialized() { var returnUrl = Uri.EscapeDataString("/" + Navigation.ToBaseRelativePath(Navigation.Uri)); Navigation.NavigateTo($"bff/login?returnUrl={returnUrl}", forceLoad: true); } } ``` 3. **Modifications to Routes.razor** Replace the contents of `Routes.razor` so it matches below: BlazorBffApp.Client/Routes.razor ```razor @using Microsoft.AspNetCore.Components.Authorization @using BlazorBffApp.Client.Components @if (context.User.Identity?.IsAuthenticated != true) { } else {

You (@context.User.Identity?.Name) are not authorized to access this resource.

}
``` This ensures that accessing a page that requires authorization automatically redirects the user to the identity provider for authentication. 4. **Modifications to MainLayout.razor** Modify your `MainLayout.razor` to include the `LoginDisplay`: BlazorBffApp.Client/Layout/MainLayout.razor ```razor @inherits LayoutComponentBase @using BlazorBffApp.Client.Components
@Body
An unhandled error has occurred. Reload 🗙
``` Now your application supports logging in and out. ## Exposing APIs [Section titled “Exposing APIs”](#exposing-apis) Now we’re going to expose an embedded API for weather forecasts to Blazor WebAssembly (WASM) and call it via an `HttpClient`. 1. **Configuring the Client app** Add a class called `WeatherHttpClient` to the `BlazorBffApp.Client` project: BlazorBffApp.Client/WeatherHttpClient.cs ```csharp public class WeatherHttpClient(HttpClient client) : IWeatherClient { public async Task GetWeatherForecasts() => await client.GetFromJsonAsync("WeatherForecast") ?? throw new JsonException("Failed to deserialize"); } public class WeatherForecast { public DateOnly Date { get; set; } public int TemperatureC { get; set; } public string? Summary { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } // The IWeatherClient interface abstracts between server and client implementations. public interface IWeatherClient { Task GetWeatherForecasts(); } ``` Then register this in `Program.cs`: BlazorBffApp.Client/Program.cs ```csharp builder.Services .AddBffBlazorClient() // Provides auth state provider that polls the /bff/user endpoint // Register an HTTP client configured to fetch data from the BFF host. .AddLocalApiHttpClient(); // Register the concrete implementation with the abstraction builder.Services.AddSingleton(); ``` 2. **Configuring the server** Add a class called `ServerWeatherClient` to your `BlazorBffApp` server project: BlazorBffApp/ServerWeatherClient.cs ```csharp public class ServerWeatherClient : IWeatherClient { public Task GetWeatherForecasts() { var startDate = DateOnly.FromDateTime(DateTime.Now); string[] summaries = [ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" ]; return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = startDate.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = summaries[Random.Shared.Next(summaries.Length)] }).ToArray()); } } ``` Then add an endpoint to your HTTP pipeline and register the server implementation: BlazorBffApp/Program.cs ```csharp // Register the server-side implementation builder.Services.AddSingleton(); // ... app.MapGet("/WeatherForecast", (IWeatherClient weatherClient) => weatherClient.GetWeatherForecasts()) .RequireAuthorization() .AsBffApiEndpoint(); ``` 3. **Displaying Weather Information From The API** By default, the Blazor template ships with a weather page. Change the content of `Weather.razor` to this: BlazorBffApp.Client/Pages/Weather.razor ```razor @page "/weather" @using BlazorBffApp.Client.Components @using Microsoft.AspNetCore.Authorization @rendermode InteractiveWebAssembly @attribute [Authorize] Weather ``` Now add a `WeatherComponent.razor`: BlazorBffApp.Client/Components/WeatherComponent.razor ```razor @inject IWeatherClient WeatherClient

Weather

This component demonstrates showing data.

@if (forecasts == null) {

Loading...

} else { @foreach (var forecast in forecasts) { }
Date Temp. (C) Temp. (F) Summary
@forecast.Date.ToShortDateString() @forecast.TemperatureC @forecast.TemperatureF @forecast.Summary
} @code { private WeatherForecast[]? forecasts; protected override async Task OnInitializedAsync() { forecasts = await WeatherClient.GetWeatherForecasts(); } } ``` Token availability in Blazor components Access tokens are managed server-side by the BFF host and are never available in Blazor WASM components directly. Always use `AddLocalApiHttpClient()` to create HTTP clients that route through the BFF host — never try to retrieve or pass tokens to client-side components. See the [Troubleshooting guide](/bff/troubleshooting/) if tokens appear unavailable. ## See Also [Section titled “See Also”](#see-also) * [Single Frontend Getting Started](/bff/getting-started/single-frontend/) — Simpler setup for a single SPA * [Blazor Fundamentals](/bff/fundamentals/blazor/) — Rendering modes, data access patterns, and auth state * [Local APIs](/bff/fundamentals/apis/local/) — Embedding API endpoints in the BFF host * [Token Management](/bff/fundamentals/tokens/) — How BFF handles access token refresh automatically * [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/) — Persisting sessions with Entity Framework * [Access Token Management](/accesstokenmanagement/) — The underlying token lifecycle library * [Troubleshooting](/bff/troubleshooting/) — Common Blazor BFF issues and fixes ----- # Getting Started - Multiple Frontends > A guide on how to create a BFF application with multiple frontends. Duende.BFF (Backend for Frontend) supports multiple frontends in a single BFF host. This is useful for scenarios where you want to serve several SPAs or frontend apps from the same backend, each with their own authentication and API proxying configuration. ## What You’ll Build [Section titled “What You’ll Build”](#what-youll-build) By the end of this guide you will have a single BFF host that serves multiple frontend applications, each with independently configurable OpenID Connect settings and remote API proxying. ## Setting Up A BFF Project For Multiple Frontends [Section titled “Setting Up A BFF Project For Multiple Frontends”](#setting-up-a-bff-project-for-multiple-frontends) 1. **Create A New ASP.NET Core Project** Terminal ```bash dotnet new web -n MyMultiBffApp cd MyMultiBffApp ``` 2. **Add The Duende.BFF NuGet Package** Terminal ```bash dotnet add package Duende.BFF ``` 3. **OpenID Connect Configuration** Configure OpenID Connect authentication for your BFF host. This is similar to the single frontend setup, but applies to all frontends unless overridden per frontend. Program.cs ```csharp builder.Services.AddBff() .ConfigureOpenIdConnect(options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); // Add this scope if you want to receive refresh tokens options.Scope.Add("offline_access"); }) .ConfigureCookies(options => { // Because we use an identity server that's configured on a different site // (duendesoftware.com vs localhost), we need to configure the SameSite property to Lax. // Setting it to Strict would cause the authentication cookie not to be sent after logging in. // The user would have to refresh the page to get the cookie. // Recommendation: Set it to 'strict' if your IDP is on the same site as your BFF. options.Cookie.SameSite = SameSiteMode.Lax; }); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); app.UseRouting(); // adds antiforgery protection for local APIs app.UseBff(); // adds authorization for local and remote API endpoints app.UseAuthorization(); app.Run(); ``` 4. **Configure BFF In `Program.cs`** * Static Register multiple frontends directly in code using `AddFrontends`: Program.cs ```csharp builder.Services.AddBff() .AddFrontends( new BffFrontend(BffFrontendName.Parse("default-frontend")) .WithCdnIndexHtmlUrl(new Uri("https://localhost:5005/static/index.html")), new BffFrontend(BffFrontendName.Parse("admin-frontend")) .WithCdnIndexHtmlUrl(new Uri("https://localhost:5005/admin/index.html")) ); // ...existing code for authentication, authorization, etc. ``` * From Config You can also load frontend configuration from an `IConfiguration` source, such as a JSON file: Example `bffconfig.json`: ```json { "defaultOidcSettings": null, "defaultCookieSettings": null, "frontends": { "from_config": { "cdnIndexHtmlUrl": "https://localhost:5005/static/index.html", "matchingPath": "/from-config", "oidc": { "clientId": "bff.multi-frontend.config" }, "remoteApis": [ { "pathMatch": "/api/client-token", "targetUri": "https://localhost:5010", "requiredTokenType": "Client" } ] } } } ``` Load and use the configuration in `Program.cs`: Program.cs ```csharp var bffConfig = new ConfigurationBuilder() .AddJsonFile("bffconfig.json") .Build(); builder.Services.AddBff() .LoadConfiguration(bffConfig); // ...existing code for authentication, authorization, etc. ``` 5. **Remote API Proxying** You can configure remote API proxying in two ways: * **Single YARP proxy for all frontends:** You can set up a single YARP proxy for all frontends, as shown in the [Single Frontend Guide](/bff/getting-started/single-frontend/). * **Direct proxying per frontend:** You can configure remote APIs for each frontend individually: Program.cs ```csharp builder.Services.AddBff() .AddFrontends( new BffFrontend(BffFrontendName.Parse("default-frontend")) .WithCdnIndexHtmlUrl(new Uri("https://localhost:5005/static/index.html")) .WithRemoteApis( new RemoteApi("/api/user-token", new Uri("https://localhost:5010")) ) ); ``` This allows each frontend to have its own set of proxied remote APIs. 6. **Server Side Sessions** Server side session configuration is the same as in the single frontend scenario. See the [Single Frontend Guide](/bff/getting-started/single-frontend/) for details. ## See Also [Section titled “See Also”](#see-also) * [Single Frontend Getting Started](/bff/getting-started/single-frontend/) — Simpler BFF setup for one frontend * [Multi-Frontend Fundamentals](/bff/fundamentals/multi-frontend/) — Deep-dive into multi-frontend configuration * [Remote APIs](/bff/fundamentals/apis/remote/) — Proxying calls to upstream services * [YARP Integration](/bff/fundamentals/apis/yarp/) — Advanced proxy configuration * [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/) — Persisting sessions in production * [Access Token Management](/accesstokenmanagement/) — Token lifecycle managed by BFF ----- # Getting Started - Single Frontend > A guide on how to create a BFF application with a single frontend. Duende.BFF (Backend for Frontend) is a library that helps you build secure, modern web applications by acting as a security gateway between your frontend and backend APIs. This guide will walk you through setting up a simple BFF application with a single frontend. ## What You’ll Build [Section titled “What You’ll Build”](#what-youll-build) By the end of this guide you will have an ASP.NET Core host that: * Authenticates users via OpenID Connect and stores the session server-side * Exposes secure local API endpoints with CSRF protection * Optionally proxies remote API calls with automatic token attachment ## Setting Up A BFF project [Section titled “Setting Up A BFF project”](#setting-up-a-bff-project) 1. **Create A New ASP.NET Core Project** Create a new ASP.NET Core Web Application: ```sh dotnet new web -n MyBffApp cd MyBffApp ``` 2. **Add The Duende.BFF NuGet Package** Install the Duende.BFF package: ```sh dotnet add package Duende.BFF ``` 3. **Configure BFF In `Program.cs`** Add the following to your `Program.cs`: * Duende BFF v4 ```csharp builder.Services.AddBff() .ConfigureOpenIdConnect(options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); // Add this scope if you want to receive refresh tokens options.Scope.Add("offline_access"); }) .ConfigureCookies(options => { // Because we use an identity server that's configured on a different site // (duendesoftware.com vs localhost), we need to configure the SameSite property to Lax. // Setting it to Strict would cause the authentication cookie not to be sent after logging in. // The user would have to refresh the page to get the cookie. // Recommendation: Set it to 'strict' if your IDP is on the same site as your BFF. options.Cookie.SameSite = SameSiteMode.Lax; }); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); app.UseRouting(); // adds antiforgery protection for local APIs app.UseBff(); // adds authorization for local and remote API endpoints app.UseAuthorization(); app.Run(); ``` * Duende BFF v3 ```csharp builder.Services.AddBff(); // Configure the authentication builder.Services .AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("cookie", options => { // Configure the cookie with __Host prefix for maximum security options.Cookie.Name = "__Host-blazor"; // Because we use an identity server that's configured on a different site // (duendesoftware.com vs localhost), we need to configure the SameSite property to Lax. // Setting it to Strict would cause the authentication cookie not to be sent after logging in. // The user would have to refresh the page to get the cookie. // Recommendation: Set it to 'strict' if your IDP is on the same site as your BFF. options.Cookie.SameSite = SameSiteMode.Lax; }) .AddOpenIdConnect("oidc", options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); // Add this scope if you want to receive refresh tokens options.Scope.Add("offline_access"); }); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); app.UseRouting(); // adds antiforgery protection for local APIs app.UseBff(); // adds authorization for local and remote API endpoints app.UseAuthorization(); // login, logout, user, backchannel logout... app.MapBffManagementEndpoints(); app.Run(); ``` Make sure to replace the Authority, ClientID and ClientSecret with values from your identity provider. Also consider if the scopes are correct. 4. **Adding Local APIs** If your browser-based application uses local APIs, you can add those directly to your BFF app. The BFF supports both controllers and minimal APIs to create local API endpoints. It’s important to mark up the APIs with .AsBffApiEndpoint(), because this adds CSRF protection. * Minimal Apis Program.cs ```csharp // Adds authorization for local and remote API endpoints app.UseAuthorization(); // Place your custom routes after the 'UseAuthorization()' app.MapGet("/hello-world", () => "hello-world") .AsBffApiEndpoint(); // Adds CSRF protection to the controller endpoints ``` * Controllers Program.cs ```csharp builder.Services.AddControllers(); // ... app.UseAuthorization(); // When mapping the api controllers, place this after // UseAuthorization() app.MapControllers() .RequireAuthorization() .AsBffApiEndpoint(); // This statement adds CSRF protection to the controller endpoints ``` LocalApiController.cs ```csharp [Route("hello")] public class LocalApiController : ControllerBase { [Route("world")] [HttpGet] public IActionResult SelfContained() { return Ok("hello world"); } } ``` 5. **Adding Remote APIs** If you also want to call remote api’s from your browser based application, then you should proxy the calls through the BFF. The BFF extends the capabilities of [Yarp](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/getting-started?view=aspnetcore-9.0) in order to achieve this. Terminal ```bash dotnet add package Duende.BFF.Yarp ``` * Direct forwarding Program.cs ```csharp builder.Services.AddBff() .AddRemoteApis(); // Adds the capabilities needed to perform proxying to remote APIs. // ... // Map any call (including child routes) from /api/remote to https://remote-api-address app.MapRemoteBffApiEndpoint("/api/remote", new Uri("https://remote-api-address")) .WithAccessToken(RequiredTokenType.Client); ``` * Yarp Program.cs ```csharp builder.Services.AddBff() .AddRemoteApis() // This adds the capabilities needed to perform proxying to remote api's. .AddYarpConfig(new RouteConfig() // This statement configures yarp. { RouteId = "route_id", ClusterId = "cluster_id", Match = new RouteMatch { Path = $"api/remote/{{**catch-all}}" } }, new ClusterConfig() { ClusterId = "cluster_id", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destination_1", new DestinationConfig { Address = "https://remote-api-address" } } } }); // ... app.UseAuthorization(); // Add the Yarp middleware that will proxy the requests. app.MapReverseProxy(proxyApp => { proxyApp.UseAntiforgeryCheck(); }); ``` You can also use an `IConfiguration` instead of programmatically configuring the proxy. 6. **Adding Server-Side Sessions** * In-Memory By default, Duende.BFF uses an in-memory session store. This is suitable for development and testing, but not recommended for production as sessions will be lost when the application restarts. Program.cs ```csharp builder.Services.AddBff() .AddServerSideSessions(); // Uses in-memory session store by default // ...existing code for authentication, authorization, etc. ``` * Entity Framework For production scenarios, you can use Entity Framework to persist sessions in a database. First, add the NuGet package: Terminal ```bash dotnet add package Duende.BFF.EntityFramework ``` Then configure the session store in your `Program.cs`: Program.cs ```csharp builder.Services.AddBff() .AddServerSideSessions() .AddEntityFrameworkServerSideSessions(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")); }); // ...existing code for authentication, authorization, etc. ``` You will also need to run the Entity Framework migrations to create the necessary tables. In-memory sessions are not production-ready The default in-memory session store loses all sessions on application restart and cannot be shared across multiple instances. Always use Entity Framework-backed sessions in production. ## Frontend Integration [Section titled “Frontend Integration”](#frontend-integration) With the BFF host running, your frontend (JavaScript SPA, React, Angular, etc.) needs to call a few BFF endpoints for authentication and to make API calls. Below is a minimal vanilla JavaScript pattern you can adapt. ### Check the current user session [Section titled “Check the current user session”](#check-the-current-user-session) On load, call `/bff/user` to check whether the user is logged in. This endpoint returns the user’s claims as JSON when authenticated, or a `401`/empty response when anonymous. ```javascript // Fetch the current user from the BFF session async function getUser() { const response = await fetch('/bff/user', { headers: { 'X-CSRF': '1' } }); if (response.ok) { return await response.json(); // Array of { type, value } claim objects } return null; // Not authenticated } ``` ### Login and logout links [Section titled “Login and logout links”](#login-and-logout-links) Use plain anchor tags pointing to the BFF management endpoints. Do **not** use `fetch` for these — they must trigger a full browser redirect. ```html Log in Log out ``` ```javascript // Wire up logout link with the session-bound URL from /bff/user const user = await getUser(); if (user) { const logoutUrlClaim = user.find(c => c.type === 'bff:logout_url'); document.getElementById('logout-link').href = logoutUrlClaim?.value ?? '/bff/logout'; } ``` ### Calling BFF API endpoints [Section titled “Calling BFF API endpoints”](#calling-bff-api-endpoints) Every request to a BFF API endpoint **must** include the `X-CSRF: 1` header. Without it, the BFF will reject the request with `401 Unauthorized`. ```javascript // Centralized fetch wrapper — always add the X-CSRF header async function bffFetch(url, options = {}) { const response = await fetch(url, { ...options, headers: { 'X-CSRF': '1', ...options.headers, }, }); // Redirect to login if the session has expired if (response.status === 401) { window.location.href = `/bff/login?returnUrl=${encodeURIComponent(window.location.pathname)}`; return; } return response; } // Example usage const data = await bffFetch('/api/weather'); const json = await data.json(); ``` ### Proactive session polling (optional) [Section titled “Proactive session polling (optional)”](#proactive-session-polling-optional) To detect server-initiated session termination (e.g., back-channel logout), poll `/bff/user` periodically: ```javascript // Poll every 60 seconds; redirect to login if session ends setInterval(async () => { const user = await getUser(); if (!user) { window.location.href = '/bff/login'; } }, 60_000); ``` ## See Also [Section titled “See Also”](#see-also) * [Multiple Frontends](/bff/getting-started/multi-frontend/) — Serve several SPAs from the same BFF host * [Blazor Applications](/bff/getting-started/blazor/) — BFF setup for Blazor Server and WASM * [Local APIs](/bff/fundamentals/apis/local/) — Full reference for embedded API endpoints and CSRF protection * [Remote APIs](/bff/fundamentals/apis/remote/) — Direct forwarding to upstream services * [Token Management](/bff/fundamentals/tokens/) — How BFF handles access token refresh automatically * [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/) — Persistent session configuration * [Access Token Management](/accesstokenmanagement/) — The underlying token lifecycle library used by BFF ----- # Getting Started - Templates > A guide on how to install the BFF project templates. Project templates for Duende BFF are shipped as part of the Duende .NET project templates. Refer the [templates documentation](/identityserver/overview/packaging/#templates) for more information on how to install the templates. ## Available templates [Section titled “Available templates”](#available-templates) ### BFF Remote API [Section titled “BFF Remote API”](#bff-remote-api) ```shell dotnet new duende-bff-remoteapi ``` Creates a basic JavaScript-based BFF host that configures and invokes a [remote API via the BFF proxy](/bff/fundamentals/apis/remote/). ### BFF Local API [Section titled “BFF Local API”](#bff-local-api) ```shell dotnet new duende-bff-localapi ``` Creates a basic JavaScript-based BFF host that invokes a [local API](/bff/fundamentals/apis/local/) co-hosted with the BFF. ### BFF Blazor [Section titled “BFF Blazor”](#bff-blazor) ```shell dotnet new duende-bff-blazor ``` Creates a Blazor application that [uses the interactive auto render mode](/bff/fundamentals/blazor/), and secures the application across all render modes consistently using Duende.BFF.Blazor. ----- # Backend For Frontend (BFF) Samples > A collection of sample applications demonstrating how to use the BFF security framework with different frontend technologies. This section contains a collection of clients using our BFF security framework. ## JavaScript Frontend [Section titled “JavaScript Frontend”](#javascript-frontend) This sample demonstrates a vanilla JavaScript SPA secured by the BFF. You will learn how to call `/bff/user` to retrieve session claims, wire up login/logout links, and make CSRF-protected API calls using `X-CSRF: 1` — without any JS framework dependencies. [JavaScript Frontend Sample ](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/JsBffSample)Vanilla JS SPA with BFF: session claims, login/logout, and CSRF-protected API calls ## ReactJs Frontend [Section titled “ReactJs Frontend”](#reactjs-frontend) This sample shows how to integrate React with the BFF framework. You will learn how to manage login state via `/bff/user`, protect routes based on session claims, and proxy API requests through the BFF with automatic token forwarding. [ReactJS Frontend Sample ](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/React)React SPA with BFF: session-driven auth state, protected routes, and token-forwarded API calls ## Angular Frontend [Section titled “Angular Frontend”](#angular-frontend) This sample shows how to integrate Angular with the BFF framework. You will learn how to build an Angular auth service backed by `/bff/user`, add an HTTP interceptor for the CSRF header, and handle 401 redirects gracefully. [Angular Frontend Sample ](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/Angular)Angular SPA with BFF: auth service, CSRF interceptor, and 401 redirect handling ## Vue Frontend Community [Section titled “Vue Frontend ”Community](#vue-frontend) This sample (contributed by [@Marco Cabrera](https://github.com/mck231)) shows how to integrate Vue 3 with the BFF framework. You will learn how to expose session state from `/bff/user` in a Vue composable and make authenticated API calls with CSRF protection. [Vue Frontend Sample (Community) ](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/Vue)Vue 3 SPA with BFF: session composable, CSRF-protected API calls ## Blazor WASM [Section titled “Blazor WASM”](#blazor-wasm) This sample shows how to use Blazor WebAssembly as the frontend with the BFF host. You will learn how to configure `AuthorizationMessageHandler` to forward tokens from the BFF session and call backend APIs securely from client-side Blazor code. [Blazor WASM Sample ](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/BlazorWasm)Blazor WASM with BFF: AuthorizationMessageHandler and secure API calls from the browser ## Blazor Auto Rendering [Section titled “Blazor Auto Rendering”](#blazor-auto-rendering) This sample demonstrates Blazor Auto rendering mode (server-side prerender + WASM hydration) combined with BFF authentication. You will learn how to share auth state across render modes and avoid common pitfalls with interactive components that call protected APIs. [Blazor Auto Rendering Sample ](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/BlazorAutoRendering)Blazor Auto mode with BFF: shared auth state across server-side and WASM render modes ## YARP Integration [Section titled “YARP Integration”](#yarp-integration) This sample shows how to use the Duende BFF extensions for [Microsoft YARP](https://microsoft.github.io/reverse-proxy/) to proxy API requests. You will learn how to configure YARP routes with BFF token forwarding, eliminating the need for manual `AddRemoteApis` registration. [YARP Integration Sample ](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/JsBffYarpSample)BFF with YARP: token-forwarding reverse proxy routes for remote APIs ## OpenAPI [Section titled “OpenAPI”](#openapi) This sample shows how to expose and consume an OpenAPI (Swagger) spec from a BFF-protected API. You will learn how to configure Swagger UI to authenticate via the BFF session and make test requests without needing a separate bearer token. [OpenAPI Sample ](https://github.com/DuendeSoftware/samples/tree/main/BFF/v4/OpenApi)BFF with OpenAPI: Swagger UI authenticated via BFF session cookies ## Separate Host for UI [Section titled “Separate Host for UI”](#separate-host-for-ui) This sample shows how to run the frontend (e.g. a dev Vite server) on a different origin from the BFF host and use CORS to allow cross-site session and API requests. You will learn how to configure `AllowedOrigins`, CORS policy, and cookie `SameSite` settings for split-host development and production deployments. [Separate Host for UI Sample ](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/SplitHosts)Split-host BFF: CORS configuration for frontend and backend on different origins ## Docker Hosting Community [Section titled “Docker Hosting ”Community](#docker-hosting) This sample (contributed by [@Marco Cabrera](https://github.com/mck231)) shows how to run the BFF host and IdentityServer together using Docker Compose. You will learn how to configure networking between containers, set authority URLs, and handle Data Protection key persistence in a containerized environment. [Docker Sample (Community) ](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/docker)BFF + IdentityServer in Docker Compose: container networking and Data Protection key persistence ## DPoP [Section titled “DPoP”](#dpop) This sample shows how to configure the BFF for [DPoP (Demonstrating Proof of Possession)](/identityserver/tokens/pop/) so that all tokens are sender-constrained. You will learn how to enable DPoP on both the BFF and the downstream API, preventing token replay attacks even if tokens are intercepted. [DPoP Sample ](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/DPoP)BFF with DPoP: sender-constrained tokens to prevent token replay attacks ## Token Exchange using the IAccessTokenRetriever [Section titled “Token Exchange using the IAccessTokenRetriever”](#token-exchange-using-the-iaccesstokenretriever) This sample shows how to implement a custom `IAccessTokenRetriever` that performs RFC 8693 token exchange for impersonation. When logged in as Alice you receive a token scoped to Bob, and vice versa — demonstrating how to swap or enrich tokens before they are forwarded to downstream APIs. [Token Exchange Sample ](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/TokenExchange)Custom IAccessTokenRetriever with RFC 8693 token exchange for user impersonation ## New User Onboarding with Blazor Auto Rendering Community [Section titled “New User Onboarding with Blazor Auto Rendering ”Community](#new-user-onboarding-with-blazor-auto-rendering) This sample (contributed by [@hugh-maaskant](https://github.com/hugh-maaskant)) shows how to handle a new-user onboarding flow where additional profile data is collected by the application — not the identity provider. You will learn how to intercept post-login redirects, store onboarding data in the application database, and resume the original request after onboarding completes. [New User Onboarding Sample (Community) ](https://github.com/hugh-maaskant/BlazorBffOnboarding)New user onboarding with Blazor Auto: intercept post-login, collect profile data in app DB ## Feedback [Section titled “Feedback”](#feedback) Feel free to [ask the developer community](https://github.com/DuendeSoftware/community/discussions) if you are looking for a particular sample and can’t find it here. [Developer Community Forum ](https://github.com/DuendeSoftware/community/discussions)Join the Duende Developer Community for discussions and feedback ----- # Troubleshooting > Diagnose and fix common problems with Duende BFF: anti-forgery failures, CORS errors, session expiration, YARP misconfigurations, Blazor token issues, and more. This page covers the most common problems encountered when building and operating a Duende BFF application. Each scenario is described in **symptom → cause → solution** format. *** ### Symptom: Anti-Forgery Token Validation Failures — `401 Unauthorized` with missing `X-CSRF` header [Section titled “Symptom: Anti-Forgery Token Validation Failures — 401 Unauthorized with missing X-CSRF header”](#symptom-anti-forgery-token-validation-failures--401-unauthorized-with-missing-x-csrf-header) **Cause:** The BFF enforces the presence of a custom `X-CSRF: 1` header on all API endpoints decorated with `.AsBffApiEndpoint()`. Requests that do not include this header are rejected. **Solution:** Add the `X-CSRF: 1` header to every `fetch()` call targeting a BFF API endpoint. The easiest approach is a centralized wrapper: ```javascript function bffFetch(url, options = {}) { return fetch(url, { ...options, headers: { 'X-CSRF': '1', ...options.headers, }, }); } ``` Also verify that: * `app.UseBff()` appears **after** `app.UseRouting()` and `app.UseAuthentication()`, and **before** `app.UseAuthorization()` in your middleware pipeline. * The endpoint is decorated with `.AsBffApiEndpoint()` (Minimal API) or `[BffApi]` / `.AsBffApiEndpoint()` at mapping time (MVC). See [Middleware Pipeline](/bff/fundamentals/middleware-pipeline/) for the canonical order and a table of common mistakes. Caution If `UseBff()` is placed after `UseAuthorization()`, anti-forgery enforcement is silently disabled with no error. Always verify middleware order. *** ### Symptom: CORS Errors With BFF Endpoints — failed `OPTIONS` preflight on `/bff/user` or API endpoints [Section titled “Symptom: CORS Errors With BFF Endpoints — failed OPTIONS preflight on /bff/user or API endpoints”](#symptom-cors-errors-with-bff-endpoints--failed-options-preflight-on-bffuser-or-api-endpoints) **Cause:** The BFF and the SPA are on different origins. CORS errors here are usually a sign that the BFF and frontend are not being served from the same origin, which defeats part of the BFF pattern’s security model. **Solution:** The BFF is designed to serve the frontend from the same origin. If you must host them on different origins, configure a CORS policy that explicitly allows the SPA origin and allows credentials: ```csharp builder.Services.AddCors(options => { options.AddPolicy("SpaPolicy", policy => { policy.WithOrigins("https://app.example.com") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); // Required for cookie-based auth }); }); // Must come before UseAuthentication and UseBff app.UseCors("SpaPolicy"); ``` *** ### Symptom: Session Expiration Causing Silent Failures — SPA stops receiving data, `401` with no user-facing error [Section titled “Symptom: Session Expiration Causing Silent Failures — SPA stops receiving data, 401 with no user-facing error”](#symptom-session-expiration-causing-silent-failures--spa-stops-receiving-data-401-with-no-user-facing-error) **Cause:** The BFF session (stored in the authentication cookie) has expired. BFF API endpoints return `401` instead of a redirect when the session expires, so the SPA must handle this explicitly. **Solution:** Detect `401` responses in your fetch wrapper and redirect to the BFF login endpoint: ```javascript async function bffFetch(url, options = {}) { const response = await fetch(url, { ...options, headers: { 'X-CSRF': '1', ...options.headers }, }); if (response.status === 401) { window.location.href = `/bff/login?returnUrl=${encodeURIComponent(window.location.pathname)}`; return; } return response; } ``` Also consider: * Polling `/bff/user` periodically to detect session expiry proactively. * Configuring absolute and sliding session lifetimes on the cookie handler to match your requirements. * Using [server-side sessions](/bff/fundamentals/session/server-side-sessions/) to enable server-initiated session termination. *** ### Symptom: YARP Proxy Misconfiguration — proxied requests return `404`, missing token, or bypass anti-forgery [Section titled “Symptom: YARP Proxy Misconfiguration — proxied requests return 404, missing token, or bypass anti-forgery”](#symptom-yarp-proxy-misconfiguration--proxied-requests-return-404-missing-token-or-bypass-anti-forgery) **Cause:** Common YARP configuration mistakes include: * Missing `UseAntiforgeryCheck()` in the `MapReverseProxy` pipeline. * Typos in metadata keys when using `appsettings.json` configuration. * Route patterns that don’t include `{**catch-all}` to capture sub-paths. **Solution:** Ensure `UseAntiforgeryCheck()` is explicitly included: ```csharp app.MapReverseProxy(proxyApp => { proxyApp.UseAntiforgeryCheck(); // Required — not automatic for YARP }); ``` When configuring via `appsettings.json`, metadata keys are case-sensitive: ```json "Metadata": { "Duende.Bff.Yarp.TokenType": "User", "Duende.Bff.Yarp.AntiforgeryCheck": "true" } ``` For route patterns, ensure sub-paths are captured: ```json "Match": { "Path": "/api/{**catch-all}" } ``` Caution A typo in a YARP metadata key fails silently — no token is attached and no anti-forgery check is enforced. Always test proxied routes with an authenticated request and verify the `Authorization` header reaches the upstream service. *** ### Symptom: Blazor WASM — Token Not Available in Components, exception or null when calling `GetUserAccessTokenAsync` [Section titled “Symptom: Blazor WASM — Token Not Available in Components, exception or null when calling GetUserAccessTokenAsync”](#symptom-blazor-wasm--token-not-available-in-components-exception-or-null-when-calling-getuseraccesstokenasync) **Cause:** In Blazor WASM, `HttpContext` is not available. Access tokens are managed server-side by the BFF host and must never be exposed to client-side components. **Solution:** Use `AddLocalApiHttpClient()` to register a typed HTTP client that routes through the BFF host. The BFF host attaches the token server-side before forwarding: ```csharp // Client-side Program.cs builder.Services .AddBffBlazorClient() .AddLocalApiHttpClient(); ``` The `WeatherHttpClient` then calls the BFF host’s local API endpoint (which does have access to `HttpContext` and can call `GetUserAccessTokenAsync()`), rather than calling the remote API directly. Caution Never attempt to retrieve an access token in a Blazor WASM component and pass it to JavaScript or store it in the component state. This defeats the BFF security model. *** ### Symptom: Silent Login Failures — `prompt=none` fails in Safari/Firefox, users unexpectedly logged out [Section titled “Symptom: Silent Login Failures — prompt=none fails in Safari/Firefox, users unexpectedly logged out”](#symptom-silent-login-failures--promptnone-fails-in-safarifirefox-users-unexpectedly-logged-out) **Cause:** Modern browsers block third-party cookies. The `prompt=none` / silent renew flow in traditional SPAs relies on an iframe that sends a cookie to the identity provider — this breaks when third-party cookies are blocked. **Solution:** The BFF pattern is specifically designed to avoid this problem. Token renewal is handled server-side using refresh tokens, which do not rely on third-party cookies. Ensure: 1. `offline_access` scope is requested so a refresh token is issued. 2. `SaveTokens = true` is set on the OIDC handler. 3. The BFF’s `Duende.AccessTokenManagement` integration is active (it is by default). ```csharp options.Scope.Add("offline_access"); // Required for refresh tokens options.SaveTokens = true; // Required to store tokens in the session ``` See [Third-Party Cookies](/bff/architecture/third-party-cookies/) for a deeper discussion of how browser cookie restrictions affect authentication flows. *** ### Symptom: 302 Redirect Instead of 401 on API Endpoints — SPA receives HTML instead of JSON [Section titled “Symptom: 302 Redirect Instead of 401 on API Endpoints — SPA receives HTML instead of JSON”](#symptom-302-redirect-instead-of-401-on-api-endpoints--spa-receives-html-instead-of-json) **Cause:** The API endpoint is not marked as a BFF API endpoint, so ASP.NET Core’s default challenge behavior (302 redirect) applies instead of BFF’s 401 response. **Solution:** Add `.AsBffApiEndpoint()` to the endpoint: ```csharp // Minimal API app.MapGet("/api/data", () => Results.Ok("data")) .RequireAuthorization() .AsBffApiEndpoint(); // Converts 302 challenge to 401 // MVC controllers app.MapControllers() .RequireAuthorization() .AsBffApiEndpoint(); ``` This instructs the BFF middleware to return `401` for unauthenticated requests rather than issuing a redirect challenge. Your SPA can then detect the `401` and navigate to `/bff/login`. *** ### Symptom: Cookie Size Exceeding Browser Limits — users with many roles cannot log in, cookie silently dropped [Section titled “Symptom: Cookie Size Exceeding Browser Limits — users with many roles cannot log in, cookie silently dropped”](#symptom-cookie-size-exceeding-browser-limits--users-with-many-roles-cannot-log-in-cookie-silently-dropped) **Cause:** All claims are stored in the authentication cookie by default. Large numbers of claims (e.g., from many roles or large identity tokens) can cause the cookie to exceed the 4KB browser limit. ASP.NET Core chunks cookies, but excessively large sessions still cause issues. **Solution:** Switch to [server-side sessions](/bff/fundamentals/session/server-side-sessions/). The browser cookie then only holds a session ID (a small opaque value), and all claims are stored in the server-side session store: ```csharp builder.Services.AddBff() .AddEntityFrameworkServerSideSessions(options => { options.UseSqlServer(connectionString); }); ``` Additionally, filter unnecessary claims from the session using an `IClaimsTransformation` or by configuring the OIDC handler to not request unnecessary scopes: ```csharp // Only request claims you actually need options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); // Don't add scopes whose claims you don't use ``` *** ## See Also [Section titled “See Also”](#see-also) * [Getting Started: Single Frontend](/bff/getting-started/single-frontend/) — Correct initial setup * [Getting Started: Blazor](/bff/getting-started/blazor/) — Blazor-specific configuration * [Local APIs](/bff/fundamentals/apis/local/) — CSRF protection for embedded API endpoints * [YARP Integration](/bff/fundamentals/apis/yarp/) — Advanced proxy configuration * [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/) — Production session persistence * [Token Management](/bff/fundamentals/tokens/) — Access token refresh and revocation * [Third-Party Cookies](/bff/architecture/third-party-cookies/) — Browser cookie restrictions and BFF * [Access Token Management](/accesstokenmanagement/) — The underlying token lifecycle library ----- # Upgrading BFF Security Framework > Guide for upgrading Duende.BFF versions, including NuGet package updates Upgrading to a new Duende.BFF version is done by updating the NuGet package and handling any breaking changes. [GitHub Repository ](https://github.com/DuendeSoftware/products/tree/main/bff)View the source code for this library on GitHub. [NuGet Package ](https://www.nuget.org/packages/Duende.BFF)View the package on NuGet.org. ----- # Duende BFF Security Framework v2.x to v3.0 > Guide for upgrading Duende BFF Security Framework from version 2.x to version 3.0, including migration steps for custom implementations and breaking changes. Duende BFF Security Framework v3.0 is a significant release that includes: * .NET 9 support * Blazor support * Several fixes and improvements ## Upgrading [Section titled “Upgrading”](#upgrading) If you rely on the default extension methods for wiring up the BFF, then V3 should be a drop-in replacement. ### Migrating From Custom Implementations Of IHttpMessageInvokerFactory [Section titled “Migrating From Custom Implementations Of IHttpMessageInvokerFactory”](#migrating-from-custom-implementations-of-ihttpmessageinvokerfactory) In Duende.BFF V2, there was an interface called `IHttpMessageInvokerFactory`. This class was responsible for creating and wiring up yarp’s `HttpMessageInvoker`. This interface has been removed in favor YARP’s `IForwarderHttpClientFactory`. One common scenario for creating a custom implementation of this class was for mocking the http client during unit testing. If you wish to inject a http handler for unit testing, you should now inject a custom `IForwarderHttpClientFactory`. For example: ```csharp // A Forwarder factory that forwards the messages to a message handler (which can be easily retrieved from a testhost) public class BackChannelHttpMessageInvokerFactory(HttpMessageHandler backChannel) : IForwarderHttpClientFactory { public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context) => new HttpMessageInvoker(backChannel); } // Wire up the forwarder in your application's test host: services.AddSingleton( new BackChannelHttpMessageInvokerFactory(_apiHost.Server.CreateHandler())); ``` ### Migrating From Custom Implementations Of IHttpTransformerFactory [Section titled “Migrating From Custom Implementations Of IHttpTransformerFactory”](#migrating-from-custom-implementations-of-ihttptransformerfactory) The `IHttpTransformerFactory` was a way to globally configure the YARP tranform pipeline. In V3, the way that the default `endpoints.MapRemoteBffApiEndpoint()` method builds up the YARP transform has been simplified significantly. Most of the logic has been pushed down to the `AccessTokenRequestTransform`. Here are common scenario’s for implementing your own `IHttpTransformerFactory` and how to upgrade: #### Replacing Defaults [Section titled “Replacing Defaults”](#replacing-defaults) If you used a custom implementation of `IHttpTransformerFactory` to change the default behavior of `MapRemoteBffApiEndpoint()`, for example to add additional transforms, then you can now inject a custom delegate into the ASP.NET Core service provider: ```csharp services.AddSingleton(CustomDefaultYarpTransforms); //... // This is an example of how to add a response header to ALL invocations of MapRemoteBffApiEndpoint() private void CustomDefaultBffTransformBuilder(string localpath, TransformBuilderContext context) { context.AddResponseHeader("added-by-custom-default-transform", "some-value"); DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken(localpath, context); } ``` Another way of doing this is to create a custom extensionmethod `MyCustomMapRemoteBffApiEndpoint()` that wraps the `MapRemoteBffApiEndpoint()` and use that everywhere in your application. This is a great way to add other defaults that should apply to all endpoints, such as requiring a specific type of access token. #### Configuring Transforms For A Single Route [Section titled “Configuring Transforms For A Single Route”](#configuring-transforms-for-a-single-route) Another common usecase for overriding the `IHttpTransformerFactory` was to have a custom transform for a single route, by applying a switch statement and testing for specific routes. Now, there is an overload on the `endpoints.MapRemoteBffApiEndpoint()` that allows you to configure the pipeline directly: ```csharp endpoints.MapRemoteBffApiEndpoint( "/local-path", _apiHost.Url(), context => { // do something custom: IE: copy request headers context.CopyRequestHeaders = true; // wire up the default transformer logic DefaultTransformers.DirectProxyWithAccessToken("/local-path", context); }) // Continue with normal BFF configuration, for example, allowing optional user access tokens .WithOptionalUserAccessToken(); ``` ### Removed method RemoteApiEndpoint.Map(localpath, apiAddress). [Section titled “Removed method RemoteApiEndpoint.Map(localpath, apiAddress).”](#removed-method-remoteapiendpointmaplocalpath-apiaddress) The Map method was no longer needed as most of the logic had been moved to either the `MapRemoteBffApiEndpoint` and the DefaultTransformers. The map method also wasn’t very explicit about what it did and a number of test scenario’s tried to verify if it wasn’t called wrongly. You are now expected to call the method `MapRemoteBffApiEndpoint`. This method now has a nullable parameter that allows you to inject your own transformers. ### AccessTokenRetrievalContext Properties Are Now Typed [Section titled “AccessTokenRetrievalContext Properties Are Now Typed”](#accesstokenretrievalcontext-properties-are-now-typed) The LocalPath and ApiAddress properties are now typed. They used to be strings. If you rely on these, for example for implementing a custom `IAccessTokenRetriever`, then you should adjust their usage accordingly. ```csharp /// /// The locally requested path. /// public required PathString PathMatch { get; set; } /// /// The remote address of the API. /// public required Uri ApiAddress { get; set; } ``` ### AddAddEntityFrameworkServerSideSessionsServices Renamed To AddEntityFrameworkServerSideSessionsServices [Section titled “AddAddEntityFrameworkServerSideSessionsServices Renamed To AddEntityFrameworkServerSideSessionsServices”](#addaddentityframeworkserversidesessionsservices-renamed-to-addentityframeworkserversidesessionsservices) If you used the method `AddAddEntityFrameworkServerSideSessionsServices()` in your code, please replace it with the corrected `AddEntityFrameworkServerSideSessionsServices()`. ### StateProviderPollingDelay and StateProviderPollingInterval Split Into Separate Options For WebAssembly and Server. [Section titled “StateProviderPollingDelay and StateProviderPollingInterval Split Into Separate Options For WebAssembly and Server.”](#stateproviderpollingdelay-and-stateproviderpollinginterval-split-into-separate-options-for-webassembly-and-server) If you used `BffBlazorOptions.StateProviderPollingInterval` or `BffBlazorOptions.StateProviderPollingDelay` to configure different polling settings, you should now consider if this same setting applies to either Server, WASM or both. Set the appropriate properties accordingly. ### Server Side Sessions Database Migrations [Section titled “Server Side Sessions Database Migrations”](#server-side-sessions-database-migrations) No [Entity Framework database migrations](/bff/fundamentals/session/server-side-sessions/#entity-framework-migrations) are required for the server side sessions feature when using the `Duende.BFF.EntityFramework` package. The database structure remains the same: serversidesessions.sql ```sqlite CREATE TABLE "UserSessions" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_UserSessions" PRIMARY KEY AUTOINCREMENT, "ApplicationName" TEXT NULL, "SubjectId" TEXT NOT NULL, "SessionId" TEXT NULL, "Created" TEXT NOT NULL, "Renewed" TEXT NOT NULL, "Expires" TEXT NULL, "Ticket" TEXT NOT NULL, "Key" TEXT NOT NULL ); CREATE UNIQUE INDEX "IX_UserSessions_ApplicationName_Key" ON "UserSessions" ("ApplicationName", "Key"); CREATE UNIQUE INDEX "IX_UserSessions_ApplicationName_SessionId" ON "UserSessions" ("ApplicationName", "SessionId"); CREATE UNIQUE INDEX "IX_UserSessions_ApplicationName_SubjectId_SessionId" ON "UserSessions" ("ApplicationName", "SubjectId", "SessionId"); CREATE INDEX "IX_UserSessions_Expires" ON "UserSessions" ("Expires"); ``` ----- # Duende BFF Security Framework v3.0 to v4.0 > Guide for upgrading Duende BFF Security Framework from version 3.x to version 4.0, including migration steps for custom implementations and breaking changes. ## Migration Checklist [Section titled “Migration Checklist”](#migration-checklist) Use this checklist to track your upgrade. Each item links to the detailed section below. * [ ] Update `Duende.BFF` NuGet package to v4.x * [ ] [Replace `TokenType` enum with `RequiredTokenType`](#remote-apis) — move `using` to `Duende.Bff.AccessTokenManagement` * [ ] [Replace `.RequireAccessToken()` with `.WithAccessToken()`](#remote-apis) on all remote API registrations * [ ] [Replace `.WithOptionalUserAccessToken()` with `.WithAccessToken(RequiredTokenType.UserOrNone)`](#remote-apis) * [ ] [Update YARP token type config](#configuring-token-types-in-yarp) to use `RequiredTokenType` enum values * [ ] [Rename custom service classes](#service-to-endpoint-updates) (`IUserService` → `IUserEndpoint`, etc.) and update to new extensibility pattern * [ ] [Update `IUserSessionStore` implementations](#custom-session-store) — replace `string key` with `UserSessionKey` struct * [ ] [Update `GetUserAccessTokenAsync` namespace](#access-token-retrieval) — use `Duende.AccessTokenManagement.OpenIdConnect` * [ ] [Optionally migrate to new simplified wireup](#simplified-wireup-without-explicit-authentication-setup) (`.ConfigureOpenIdConnect()` + `.ConfigureCookies()`) * [ ] [Run EF Core database migration](#server-side-sessions-database-migrations) if using server-side sessions (`ApplicationName` → `PartitionKey`) * [ ] Verify YARP-based API proxying still works end-to-end Database schema breaking change The `UserSessions.ApplicationName` column is renamed to `PartitionKey`. If multiple BFF v3 apps share the same session database, upgrade all of them simultaneously or provision a new database for the v4 instance. *** Duende BFF Security Framework v4.0 is a significant release that includes: * Multi-frontend support * OpenTelemetry support * Support for login prompts * Several fixes and improvements The extensibility approach has been drastically changed, and many `virtual` methods containing implementation logic are now internal instead. ## Upgrading [Section titled “Upgrading”](#upgrading) This release introduces many breaking changes. This upgrade guide covers cases where a breaking change was introduced. ### Remote APIs [Section titled “Remote APIs”](#remote-apis) The syntax for configuring remote APIs has changed slightly: Program.cs ```diff // Use a client credentials token -app.MapRemoteBffApiEndpoint("/api/client-token", "https://localhost:5010") -.RequireAccessToken(TokenType.Client); +app.MapRemoteBffApiEndpoint("/api/client-token", new Uri("https://localhost:5010")) +.WithAccessToken(RequiredTokenType.Client); // Use the client token only if the user is logged in -app.MapRemoteBffApiEndpoint("/api/optional-user-token", "https://localhost:5010") -.WithOptionalUserAccessToken(); +app.MapRemoteBffApiEndpoint("/api/optional-user-token", new Uri("https://localhost:5010")) +.WithAccessToken(RequiredTokenType.UserOrNone); ``` * The enum `TokenType` has been renamed to `RequiredTokenType`, and moved from the `Duende.Bff` to `Duende.Bff.AccessTokenManagement` namespace. * The methods to configure the token type have all been replaced with a new method `WithAccessToken()` * Requesting an optional access token should no longer be done by calling `WithOptionalUserAccessToken()`. Use `WithAccessToken(RequiredTokenType.UserOrNone)` instead. ### Configuring Token Types In YARP [Section titled “Configuring Token Types In YARP”](#configuring-token-types-in-yarp) The required token type configuration in YARP has also changed slightly. It uses the enum values from `RequiredTokenType`. ### Extending The BFF [Section titled “Extending The BFF”](#extending-the-bff) #### Service To Endpoint Updates [Section titled “Service To Endpoint Updates”](#service-to-endpoint-updates) Service interfaces and their default implementations have been renamed and have changed, resulting in an updated extensibility model: * Generally, the interfaces have been renamed, e.g. from `IUserService` to `IUserEndpoint`. * Default implementation is now internal, but can be used when overriding the endpoint: ```diff -public class MyUserService : DefaultUserService -{ -public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) - { // Custom logic here -return base.ProcessRequestAsync(context); - } -} +var bffOptions = app.Services.GetRequiredService>().Value; +app.MapGet(bffOptions.UserPath, async (HttpContext context, CancellationToken ct) => +{ // ... custom logic before calling the endpoint implementation ... +var endpointProcessor = context.RequestServices.GetRequiredService(); +await endpointProcessor.ProcessRequestAsync(context, ct); // ... custom logic after calling the endpoint implementation ... +}); ``` For more information, see the [endpoints documentation](/bff/extensibility/management/). #### Custom Session Store [Section titled “Custom Session Store”](#custom-session-store) If you have a custom implementation of `IUserSessionStore`, the interface has changed to support multiple frontends. In all methods, the `string key` has been replaced with a strongly typed `UserSessionKey` struct, which contains the `PartitionKey` and `SessionId`: * `PartitionKey` - Corresponds to the frontend name (or `ApplicationName` in V3). * `SessionId` - The user’s session identifier. ```diff public class MySessionStore : IUserSessionStore { -public Task GetUserSessionAsync(string key, CancellationToken cancellationToken) + public Task GetUserSessionAsync(UserSessionKey key, CancellationToken cancellationToken) { // ... } // ... } ``` Also see [related database changes and migrations](#server-side-sessions-database-migrations). #### Access Token Retrieval [Section titled “Access Token Retrieval”](#access-token-retrieval) The `HttpContext.GetUserAccessTokenAsync` extension method has been removed from the `Duende.Bff` namespace. You should now use the extension method from the `Duende.AccessTokenManagement.OpenIdConnect` namespace. ```csharp using Duende.AccessTokenManagement.OpenIdConnect; // ... var token = await HttpContext.GetUserAccessTokenAsync(); ``` #### Simplified Wireup Without Explicit Authentication Setup [Section titled “Simplified Wireup Without Explicit Authentication Setup”](#simplified-wireup-without-explicit-authentication-setup) The V3 style of wireup still works, but BFF V4 comes with a newer style of wireup: ```csharp services.AddBff() .ConfigureOpenIdConnect(options => { options.Authority = "your authority"; options.ClientId = "your client id"; options.ClientSecret = "secret"; // ... other OpenID Connect options. } .ConfigureCookies(options => { // The cookie options are automatically configured with recommended practices. // However, you can change the config here. }); ``` Adding this will automatically configure a Cookie and OpenID Connect flow. #### Adding Multiple Frontends [Section titled “Adding Multiple Frontends”](#adding-multiple-frontends) You can statically add a list of frontends by calling the `AddFrontends` method. ```csharp .AddFrontends( new BffFrontend(BffFrontendName.Parse("default-frontend")) .WithCdnIndexHtmlUrl(new Uri("https://localhost:5005/static/index.html")), new BffFrontend(BffFrontendName.Parse("with-path")) .WithOpenIdConnectOptions(opt => { opt.ClientId = "bff.multi-frontend.with-path"; opt.ClientSecret = "secret"; }) .WithCdnIndexHtmlUrl(new Uri("https://localhost:5005/static/index.html")) .MapToPath("/with-path"), new BffFrontend(BffFrontendName.Parse("with-domain")) .WithOpenIdConnectOptions(opt => { opt.ClientId = "bff.multi-frontend.with-domain"; opt.ClientSecret = "secret"; }) .WithCdnIndexHtmlUrl(new Uri("https://localhost:5005/static/index.html")) .MapToHost(HostHeaderValue.Parse("https://app1.localhost:5005")) .WithRemoteApis( new RemoteApi("/api/user-token", new Uri("https://localhost:5010")), new RemoteApi("/api/client-token", new Uri("https://localhost:5010")) ) ``` #### Loading Configuration From `IConfiguration` [Section titled “Loading Configuration From IConfiguration”](#loading-configuration-from-iconfiguration) Loading configuration, including OpenID Connect configuration from `IConfiguration` is now supported: ```csharp services.AddBff().LoadConfiguration(bffConfig); ``` This enables you to configure your OpenID Connect options, including secrets, and configure the list of frontends. This also adds a file watcher, to automatically add / remove frontends from the config file. See the type `BffConfiguration` to see what settings can be configured. ## Handling SPA Static Assets [Section titled “Handling SPA Static Assets”](#handling-spa-static-assets) The BFF can be configured to handle the static file assets that are typically used when developing SPA based apps. ### Proxying Only `index.html` [Section titled “Proxying Only index.html”](#proxying-only-indexhtml) When deploying a multi-frontend BFF, it makes most sense to have the frontends configured with an `index.html` file that is retrieved from a Content Delivery Network (CDN). This can be done in various ways. For example, if you use Vite, you can publish static assets with a base URL configured. This will make sure that any static asset, (such as images, scripts, etc.) are retrieved directly from the CDN for best performance. ```csharp var frontend = new BffFrontend(BffFrontendName.Parse("frontend1")) .WithCdnIndexHtml(new Uri("https://my_cdn/some_app/index.html")) ``` The BFF automatically wires up a catch-all route that serves`index.html` for that specific frontend. See [Serve the index page from the BFF host](/bff/architecture/ui-hosting/#serve-the-index-page-from-the-bff-host) for more information. ### Proxying All Static Assets [Section titled “Proxying All Static Assets”](#proxying-all-static-assets) When developing a Single-Page Application (SPA), it’s very common to use a development webserver such as Vite. While Vite can publish static assets with a base URL, this doesn’t work well during development. The best development experience can be achieved by configuring the BFF to proxy all static assets from the development server: ```csharp var frontend = new BffFrontend(BffFrontendName.Parse("frontend1")) .WithProxiedStaticAssets(new Uri("https://localhost:3000")); // https://localhost:3000 would be the URL of your development web server. ``` While this can also be done in production, it will proxy all static assets through the BFF. This will increase the bandwidth consumed by the BFF and reduce the overall performance of your application. ### Proxying Assets Based On Environment [Section titled “Proxying Assets Based On Environment”](#proxying-assets-based-on-environment) If you’re using a local development server during development and a CDN in production, you can configure this as follows: ```csharp // In this example, the environment name from the application builder is used to determine // if we're running in production or not. var runningInProduction = () => builder.Environment.EnvironmentName == Environments.Production; // Then, when configuring the frontend, you can switch when the static assets will be proxied. new BffFrontend(BffFrontendName.Parse("default-frontend")) .WithBffStaticAssets(new Uri("https://localhost:5010/static"), useCdnWhen: runningInProduction); ``` ### Server Side Sessions Database Migrations [Section titled “Server Side Sessions Database Migrations”](#server-side-sessions-database-migrations) When using the server side sessions feature backed by the `Duende.BFF.EntityFramework` package, you will need to script [Entity Framework database migrations](/bff/fundamentals/session/server-side-sessions/#entity-framework-migrations) and apply these changes to your database. ```shell dotnet ef migrations add BFFUserSessionsV4 -o Migrations -c SessionDbContext ``` In the `UserSessions` table, a number of changes were introduced: * The `ApplicationName` column was renamed to `PartitionKey`. This column will contain the BFF frontend name. * Related indexes were updated. serversidesessions.sql ```sqlite ALTER TABLE "UserSessions" RENAME COLUMN "ApplicationName" TO "PartitionKey"; DROP INDEX "IX_UserSessions_ApplicationName_SubjectId_SessionId"; CREATE UNIQUE INDEX "IX_UserSessions_PartitionKey_SubjectId_SessionId" ON "UserSessions" ("PartitionKey", "SubjectId", "SessionId"); DROP INDEX "IX_UserSessions_ApplicationName_SessionId"; CREATE UNIQUE INDEX "IX_UserSessions_PartitionKey_SessionId" ON "UserSessions" ("PartitionKey", "SessionId"); DROP INDEX "IX_UserSessions_ApplicationName_Key"; CREATE UNIQUE INDEX "IX_UserSessions_PartitionKey_Key" ON "UserSessions" ("PartitionKey", "Key"); ```