diff --git a/astro/src/content/docs/bff/architecture/index.md b/astro/src/content/docs/bff/architecture/index.md deleted file mode 100644 index a7976396f..000000000 --- a/astro/src/content/docs/bff/architecture/index.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: "Architecture" -description: Overview of BFF host architecture, including authentication, session management, and integration with ASP.NET Core components -date: 2020-09-10T08:22:12+02:00 -sidebar: - order: 1 - label: "Overview" -redirect_from: - - /bff/v2/architecture/ - - /bff/v3/architecture/ - - /identityserver/v5/bff/architecture/ - - /identityserver/v6/bff/architecture/ - - /identityserver/v7/bff/architecture/ ---- - -A BFF host is an ASP.NET Core application, tied to a single browser based application. It performs the following -functions: - -* Authenticate the user using OpenID Connect -* Manages the user's session using Secure Cookies and optional Server-side Session Management. -* Optionally, provides access to the UI assets. -* Server-side Token Management -* Blazor support with unified authentication state management across rendering modes. - -## Authentication Flow - -The following diagram shows how the BFF protects browser-based applications: - -![BFF Security Framework Architecture Overview](../images/bff_application_architecture.svg) - - -* **Authentication flows**: The server handles the authentication flows. There are specific endpoints for login / logout. While the browser is involved with these authentication flows, because the user is redirected to and from the identity provider, the browser-based application will never see the authentication tokens. These are exchanged for a code on the server only. -* **Cookies**: After successful authentication, a cookie is added. This cookie protects all subsequent calls to the APIs. When using this type of authentication, **CSRF protection** is very important. -* **Access to APIs**: The BFF can expose embedded APIs (which are hosted by the BFF itself) or proxy calls to remote APIs (which is more common in a microservice environment). While proxying, it will exchange the authentication cookie for an access token. -* **Session Management**: The BFF can manage the users session. This can either be cookie-based session management or storage-based session management. - - -## Internals -Duende.BFF builds on widely used tools and frameworks, including ASP.NET Core's OpenID Connect and cookie authentication -handlers, YARP, and [Duende.AccessTokenManagement](/accesstokenmanagement/index.mdx). Duende.BFF combines these tools and adds additional security and -application features that are useful with a BFF architecture so that you can focus on providing application logic -instead of security logic: - -![Duende BFF Security Framework - components](../images/bff_blocs.svg) - -### ASP.NET OpenID Connect Handler - -Duende.BFF uses ASP.NET's OpenID Connect handler for OIDC and OAuth protocol processing. As long-term users of and -contributors to this library, we think it is a well implemented and flexible implementation of the protocols. - -### ASP.NET Cookie Handler - -Duende.BFF uses ASP.NET's Cookie handler for session management. The cookie handler provides a claims-based identity to -the application persisted in a digitally signed and encrypted cookie that is protected with modern cookie security -features, including the Secure, HttpOnly and SameSite attributes. The handler also provides absolute and sliding session -support, and has a flexible extensibility model, which Duende.BFF uses to -implement [server-side session management](/bff/fundamentals/session/server-side-sessions.mdx) -and [back-channel logout support](/bff/fundamentals/session/management/back-channel-logout.md). - -### Duende.AccessTokenManagement - -Duende.BFF uses the Duende.AccessTokenManagement library for access token management and storage. This includes storage -and retrieval of tokens, refreshing tokens as needed, and revoking tokens on logout. The library provides integration -with the ASP.NET HTTP client to automatically attach tokens to outgoing HTTP requests, and its underlying management -actions can also be programmatically invoked through an imperative API. - -### API Endpoints - -In the BFF architecture, the frontend makes API calls to backend services via the BFF host exclusively. Typically, the -BFF acts as a reverse proxy to [remote APIs](/bff/fundamentals/apis/remote.mdx), providing session and token management. -Implementing local APIs within the BFF host is also [possible](/bff/fundamentals/apis/local.mdx). Regardless, requests to -APIs are authenticated with the session cookie and need to be secured with an anti-forgery protection header. - -### YARP - -Duende.BFF proxies requests to remote APIs using Microsoft's YARP (Yet Another Reverse Proxy). You can set up YARP using -a simplified developer-centric configuration API provided by Duende.BFF, or if you have more complex requirements, you -can use the full YARP configuration system directly. If you are using YARP directly, Duende.BFF -provides [YARP integration](/bff/fundamentals/apis/yarp.md) to add BFF security and identity features. - -### UI Assets - -The BFF host typically serves at least some of the UI assets of the frontend, which can be HTML/JS/CSS, WASM, and/or -server-rendered content. Serving the UI assets, or at least the index page of the UI from the same origin as the backend -simplifies requests from the frontend to the backend. Doing so makes the two components same-origin, so that browsers -will allow requests with no need to use CORS and automatically include cookies (including the crucial authentication -cookie). This also avoids issues where [third-party cookie blocking](/bff/architecture/third-party-cookies.md) or the -SameSite cookie attribute prevents the frontend from sending the authentication cookie to the backend. - -It is also possible to separate the BFF and UI and host them separately. See [here](/bff/architecture/ui-hosting.md) for -more discussion of UI hosting architecture. - -### Blazor Support - -Blazor based applications have unique challenges when it comes to authentication state. It's possible to mix various -rendering models in a single application. Auto mode even starts off server rendered, then transitions to WASM when the -code has loaded. - -BFF Security Framework has built support for Blazor, where it helps to unify access to authentication state and to -secure access to backend services. diff --git a/astro/src/content/docs/bff/architecture/index.mdx b/astro/src/content/docs/bff/architecture/index.mdx new file mode 100644 index 000000000..9c365f19e --- /dev/null +++ b/astro/src/content/docs/bff/architecture/index.mdx @@ -0,0 +1,193 @@ +--- +title: "Architecture" +description: Overview of BFF host architecture, including authentication, session management, and integration with ASP.NET Core components +date: 2020-09-10T08:22:12+02:00 +sidebar: + order: 1 + label: "Overview" +redirect_from: + - /bff/v2/architecture/ + - /bff/v3/architecture/ + - /identityserver/v5/bff/architecture/ + - /identityserver/v6/bff/architecture/ + - /identityserver/v7/bff/architecture/ +--- + +import { CardGrid, LinkCard } from "@astrojs/starlight/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. + +:::tip[New to BFF?] +If you haven't yet decided whether to use BFF, start with the [overview](/bff/) which covers the threat model and the BFF-vs-token-in-browser comparison. +::: + +## How the BFF Fits Into Your System + +The following diagram shows how the BFF protects browser-based applications: + +```mermaid +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 + +### 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.md). + +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. + + + +### 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 + + + +### 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. | + + + +### 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. + + + +### 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) + + + +## Trust Boundaries + +```mermaid +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 + +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/index.mdx) | +| YARP | Reverse proxy for remote APIs | [BFF YARP integration](/bff/fundamentals/apis/yarp.md) | + +## See Also + + + + + + + + diff --git a/astro/src/content/docs/bff/architecture/multi-frontend.md b/astro/src/content/docs/bff/architecture/multi-frontend.md index df578dcf7..9770b0497 100644 --- a/astro/src/content/docs/bff/architecture/multi-frontend.md +++ b/astro/src/content/docs/bff/architecture/multi-frontend.md @@ -72,7 +72,17 @@ BFF V4 still allows you to manually configure the ASP.NET Core authentication op To achieve this, the BFF automatically configures the ASP.NET Core pipeline: -![BFF Multi-Frontend Pipeline](../images/bff_multi_frontend_pipeline.svg) +```mermaid +--- +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. diff --git a/astro/src/content/docs/bff/architecture/ui-hosting.md b/astro/src/content/docs/bff/architecture/ui-hosting.md index d34f1df65..822ec6a0c 100644 --- a/astro/src/content/docs/bff/architecture/ui-hosting.md +++ b/astro/src/content/docs/bff/architecture/ui-hosting.md @@ -30,7 +30,27 @@ 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: -![Hosting BFF UI from the UI](../images/bff_ui_hosting_loc.svg) +```mermaid +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: @@ -58,7 +78,25 @@ outside of Visual Studio (e.g., using the node cli). You might also want to have 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: -![Hosting BFF UI on CDN](../images/bff_ui_hosting_cdn.svg) +```mermaid +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 @@ -90,7 +128,28 @@ another host (presumably a CDN). This technique makes the UI and BFF have exactl 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: -![BFF Proxies the Index html from CDN](../images/bff_ui_hosting_proxy_index.svg) +```mermaid +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 diff --git a/astro/src/content/docs/bff/diagnostics/index.md b/astro/src/content/docs/bff/diagnostics/index.md deleted file mode 100644 index 527038a33..000000000 --- a/astro/src/content/docs/bff/diagnostics/index.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: "Diagnostics" -description: Overview of Duende Backend for Frontend (BFF) diagnostic capabilities including logging and OpenTelemetry integration to assist with monitoring and troubleshooting -date: 2025-11-27T08:20:20+02:00 -sidebar: - label: Overview - order: 1 ---- - -## 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.md) guide. - -### 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: - -```json -// appsettings.json -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Duende.Bff": "Debug" - } - } -} -``` - -:::note[Multiple frontends] -When using [multiple frontends and the `FrontendSelectionMiddleware`](/bff/architecture/multi-frontend.md), -log messages are written in a log scope that contains a `frontend` property with the name of the frontend for which the -log message was emitted. -::: - -## OpenTelemetry :badge[v4.0] - -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): - -```csharp -// Program.cs -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 - -OpenTelemetry metrics are run-time measurements are typically used to show graphs on a dashboard, to inspect overall -application health, or to set up monitoring rules. - -The BFF host emits metrics from several sources, and collects these through the `Duende.Bff` meter: - -* `session.started` - a counter that communicates the number of sessions started -* `session.ended` - a counter that communicates the number of sessions ended diff --git a/astro/src/content/docs/bff/diagnostics/index.mdx b/astro/src/content/docs/bff/diagnostics/index.mdx new file mode 100644 index 000000000..cc34f80f3 --- /dev/null +++ b/astro/src/content/docs/bff/diagnostics/index.mdx @@ -0,0 +1,186 @@ +--- +title: "Diagnostics" +description: Overview of Duende Backend for Frontend (BFF) diagnostic capabilities including logging and OpenTelemetry integration to assist with monitoring and troubleshooting +date: 2025-11-27T08:20:20+02:00 +sidebar: + label: Overview + order: 1 +--- + +import { CardGrid, LinkCard } from "@astrojs/starlight/components"; + +## 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.md) guide. + +### 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: + +```json +// appsettings.json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Duende.Bff": "Debug" + } + } +} +``` + +:::note[Multiple frontends] +When using [multiple frontends and the `FrontendSelectionMiddleware`](/bff/architecture/multi-frontend.md), +log messages are written in a log scope that contains a `frontend` property with the name of the frontend for which the +log message was emitted. +::: + +## OpenTelemetry :badge[v4.0] + +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): + +```csharp +// Program.cs +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 + +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 + +| 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 + +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]) +``` + +:::note +Metric names in Prometheus are automatically converted from dot notation (e.g., `session.started`) to underscores (e.g., `session_started_total`). +::: + +## 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 + +```csharp +// Program.cs +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 + +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 + +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 + + + + + + diff --git a/astro/src/content/docs/bff/extensibility/index.md b/astro/src/content/docs/bff/extensibility/index.md deleted file mode 100644 index 208586c59..000000000 --- a/astro/src/content/docs/bff/extensibility/index.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: BFF Extensibility -description: Overview of the extensibility points available in Duende.BFF for customizing session management, HTTP forwarding, and data storage -sidebar: - label: Overview - order: 1 -redirect_from: - - /bff/v2/extensibility/ - - /bff/v3/extensibility/ - - /identityserver/v5/bff/extensibility/ - - /identityserver/v6/bff/extensibility/ - - /identityserver/v7/bff/extensibility/ ---- - -Duende.BFF can be extended in the following areas - -* custom logic at the session management endpoints -* custom logic and configuration for HTTP forwarding -* custom data storage for server-side sessions and access/refresh tokens diff --git a/astro/src/content/docs/bff/extensibility/index.mdx b/astro/src/content/docs/bff/extensibility/index.mdx new file mode 100644 index 000000000..1363b313d --- /dev/null +++ b/astro/src/content/docs/bff/extensibility/index.mdx @@ -0,0 +1,106 @@ +--- +title: BFF Extensibility +description: Overview of all extensibility points in Duende.BFF for customizing session management, management endpoints, HTTP forwarding, and token storage. +sidebar: + label: Overview + order: 1 +redirect_from: + - /bff/v2/extensibility/ + - /bff/v3/extensibility/ + - /identityserver/v5/bff/extensibility/ + - /identityserver/v6/bff/extensibility/ + - /identityserver/v7/bff/extensibility/ +--- + +import { CardGrid, LinkCard } from "@astrojs/starlight/components"; + +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 + +| 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 + +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) + +All management endpoint customizations in v4 follow the same pattern: + +```csharp +// Program.cs +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 + +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 + +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 + +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 + + + + + diff --git a/astro/src/content/docs/bff/extensibility/management/back-channel-logout.mdx b/astro/src/content/docs/bff/extensibility/management/back-channel-logout.mdx index 6a3bb5e32..a1623575e 100644 --- a/astro/src/content/docs/bff/extensibility/management/back-channel-logout.mdx +++ b/astro/src/content/docs/bff/extensibility/management/back-channel-logout.mdx @@ -20,6 +20,10 @@ The `IBackchannelLogoutEndpoint` is the top-level abstraction that processes req 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`. +:::note[How this endpoint works] +See [Back-Channel Logout Endpoint](/bff/fundamentals/session/management/back-channel-logout/) for an explanation of how server-to-server logout works and how to configure it. +::: + diff --git a/astro/src/content/docs/bff/extensibility/management/diagnostics.mdx b/astro/src/content/docs/bff/extensibility/management/diagnostics.mdx index b4b89e4f8..d9228f830 100644 --- a/astro/src/content/docs/bff/extensibility/management/diagnostics.mdx +++ b/astro/src/content/docs/bff/extensibility/management/diagnostics.mdx @@ -17,6 +17,10 @@ import { Tabs, TabItem } from "@astrojs/starlight/components"; The BFF diagnostics endpoint can be customized by implementing the `IDiagnosticsEndpoint`. +:::note[How this endpoint works] +See [Diagnostics Endpoint](/bff/fundamentals/session/management/diagnostics/) for an explanation of what this endpoint provides and when it is enabled. +::: + diff --git a/astro/src/content/docs/bff/extensibility/management/login.mdx b/astro/src/content/docs/bff/extensibility/management/login.mdx index 9edeae577..31776a92f 100644 --- a/astro/src/content/docs/bff/extensibility/management/login.mdx +++ b/astro/src/content/docs/bff/extensibility/management/login.mdx @@ -19,6 +19,10 @@ The BFF login endpoint has extensibility points in two interfaces. The `ILoginEn 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. +:::note[How this endpoint works] +See [Login Endpoint](/bff/fundamentals/session/management/login/) for an explanation of what this endpoint does and how to use it from your frontend. +::: + diff --git a/astro/src/content/docs/bff/extensibility/management/logout.mdx b/astro/src/content/docs/bff/extensibility/management/logout.mdx index e5930950b..9bc04593e 100644 --- a/astro/src/content/docs/bff/extensibility/management/logout.mdx +++ b/astro/src/content/docs/bff/extensibility/management/logout.mdx @@ -19,6 +19,10 @@ The BFF logout endpoint has extensibility points in two interfaces. The `ILogout 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. +:::note[How this endpoint works] +See [Logout Endpoint](/bff/fundamentals/session/management/logout/) for an explanation of what this endpoint does and how to use it from your frontend. +::: + diff --git a/astro/src/content/docs/bff/extensibility/management/silent-login.mdx b/astro/src/content/docs/bff/extensibility/management/silent-login.mdx index 1417b249e..f838079f9 100644 --- a/astro/src/content/docs/bff/extensibility/management/silent-login.mdx +++ b/astro/src/content/docs/bff/extensibility/management/silent-login.mdx @@ -17,6 +17,10 @@ import { Tabs, TabItem } from "@astrojs/starlight/components"; The BFF silent login endpoint can be customized by implementing the `ISilentLoginEndpoint`. +:::note[How this endpoint works] +See [Silent Login Endpoint](/bff/fundamentals/session/management/silent-login/) for an explanation of the silent login flow and usage. +::: + diff --git a/astro/src/content/docs/bff/extensibility/management/user.mdx b/astro/src/content/docs/bff/extensibility/management/user.mdx index 570a8eed3..e8d6d683f 100644 --- a/astro/src/content/docs/bff/extensibility/management/user.mdx +++ b/astro/src/content/docs/bff/extensibility/management/user.mdx @@ -17,6 +17,10 @@ import { Tabs, TabItem } from "@astrojs/starlight/components"; The BFF user endpoint can be customized by implementing the `IUserEndpoint`. +:::note[How this endpoint works] +See [User Endpoint](/bff/fundamentals/session/management/user/) for an explanation of what this endpoint returns and how to call it from your frontend. +::: + diff --git a/astro/src/content/docs/bff/extensibility/sessions.mdx b/astro/src/content/docs/bff/extensibility/sessions.mdx index a82910104..ce2dde30d 100644 --- a/astro/src/content/docs/bff/extensibility/sessions.mdx +++ b/astro/src/content/docs/bff/extensibility/sessions.mdx @@ -22,7 +22,7 @@ your application's needs. ## User Session Store -If using the server-side sessions feature, you will need to have a store for the session data. +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: diff --git a/astro/src/content/docs/bff/fundamentals/apis/index.md b/astro/src/content/docs/bff/fundamentals/apis/index.md index ed2c5f95e..64601f7d9 100644 --- a/astro/src/content/docs/bff/fundamentals/apis/index.md +++ b/astro/src/content/docs/bff/fundamentals/apis/index.md @@ -13,16 +13,56 @@ redirect_from: - /identityserver/v7/bff/apis/ --- -A frontend application using the BFF pattern can call two types of APIs: +A frontend application using the BFF pattern can call two types of APIs: embedded (local) APIs, and proxied remote APIs. -#### Embedded (Local) APIs +## Choosing an API Approach -These APIs embedded inside the BFF and typically exist to support the BFF's frontend; they are not shared with other frontends or services. +```mermaid +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.mdx) | +| API is shared by multiple clients or deployed separately | [Remote API — Direct Forwarding](remote.mdx) | +| Complex routing, load balancing, or transforms are needed | [YARP](yarp.md) | +| 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` | + +:::tip[Start with local APIs when in doubt] +If the API only serves this one frontend and doesn't need to be independently deployed or versioned, embed it directly in the BFF host as a local API. It's the simplest approach and benefits from full CSRF protection with minimal configuration. +::: + +## 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.mdx) for more information. -#### 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.mdx) for most scenarios. If you have more complex requirements, you can also directly interact with [YARP](yarp.md) +You can use [Direct Forwarding](remote.mdx) for most scenarios. If you have more complex requirements, you can also directly interact with [YARP](yarp.md). + +## 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 + diff --git a/astro/src/content/docs/bff/fundamentals/blazor/data-access.mdx b/astro/src/content/docs/bff/fundamentals/blazor/data-access.mdx new file mode 100644 index 000000000..fc3532683 --- /dev/null +++ b/astro/src/content/docs/bff/fundamentals/blazor/data-access.mdx @@ -0,0 +1,204 @@ +--- +title: Blazor Data Access Patterns +description: How to securely access local and remote APIs from Blazor components using the BFF security framework. +sidebar: + label: Data Access Patterns + order: 3 +--- + +import { CardGrid, LinkCard } from "@astrojs/starlight/components"; + +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 + +```mermaid +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 + +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 + +Use an interface to abstract between server and client implementations: + +```csharp +// Shared/IDataAccessor.cs +public interface IDataAccessor +{ + Task GetData(); +} + +public record Data(string Value); +``` + +### Server Implementation + +```csharp +// Server/ServerDataAccessor.cs +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: + +```csharp +// Server/Program.cs +builder.Services.AddSingleton(); + +// ... + +app.MapGet("/some_data", async (IDataAccessor dataAccessor) => await dataAccessor.GetData()) + .RequireAuthorization() + .AsBffApiEndpoint(); +``` + +### Client (WASM) Implementation + +On the client, use an `HttpClient` that routes through the BFF host: + +```csharp +// Client/Program.cs +builder.Services.AddBffBlazorClient() + .AddLocalApiHttpClient(); + +// Register the concrete implementation with the abstraction +builder.Services.AddSingleton(sp => + sp.GetRequiredService()); +``` + +```csharp +// Client/HttpClientDataAccessor.cs +internal class HttpClientDataAccessor(HttpClient client) : IDataAccessor +{ + public async Task GetData() => + await client.GetFromJsonAsync("/some_data") + ?? throw new JsonException("Failed to deserialize"); +} +``` + +:::note +When using `AddLocalApiHttpClient()`, the `HttpClient` is pre-configured to include the authentication cookie and `X-CSRF` header automatically. You do not need to set these manually. +::: + +## 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 + +```csharp +// Server/Program.cs +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 + +```csharp +// Client/Program.cs +builder.Services.AddBffBlazorClient(); +builder.Services.AddRemoteApiHttpClient("backend"); +builder.Services.AddTransient(sp => + sp.GetRequiredService().CreateClient("backend")); +``` + +The diagram below shows the full flow: + +```mermaid +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 + +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 + + + + + + + + diff --git a/astro/src/content/docs/bff/fundamentals/blazor/index.md b/astro/src/content/docs/bff/fundamentals/blazor/index.md index b63b4a305..b1b95071a 100644 --- a/astro/src/content/docs/bff/fundamentals/blazor/index.md +++ b/astro/src/content/docs/bff/fundamentals/blazor/index.md @@ -1,6 +1,6 @@ --- title: BFF Security Framework Blazor Support -description: Learn how to integrate and use the BFF Security Framework with Microsoft Blazor applications for secure authentication and authorization. +description: Overview of integrating the Duende BFF Security Framework with Blazor applications for secure authentication and authorization. sidebar: label: Overview order: 1 @@ -13,376 +13,39 @@ redirect_from: - /bff/blazor/rendering-modes/ --- -Microsoft's Blazor framework aids developers in creating rich, interactive web applications using C# and .NET. Taking -inspiration from the popular JavaScript library React, Blazor helps deliver experiences through a component-based model, -with multiple rendering modes, as you will see below. While Blazor is a suitable framework for building rich, -interactive web applications, it also has some challenges when it comes to 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. -With the Duende BFF Security Framework, we aim to address these challenges or at the very least give guidance on how to -deal with them given your Blazor's solution choices. You will notice that the BFF security pattern is not applicable to -all Blazor implementations but rather to specific rendering modes. The goal of the BFF is to keep tokens out of the client and only use them in the secure context of the server. +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 -Blazor has many architectural options, and it is essential to understand how they work to implement security in your -Blazor applications. Like most web applications, the model has three elements, the backend, the frontend, and the -client. The chosen model determines the execution context's location. The BFF's role is to manage the security context -between all elements within the chosen execution context when appropriate. - -From a high level, let's define what the hosting elements are: - -- **Backend**: The server-side application with logic for handling operations. i.e. APIs. -- **Frontend**: The client-side Blazor application. -- **Client**: The browser that is used to interact with the frontend. +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: ```mermaid flowchart LR - Client[Client] + Client[Client / Browser] subgraph BFF Host - Backend[Backend] - Frontend[Frontend] + Backend[Backend APIs] + Frontend[Blazor Frontend] end Client <--> Frontend Frontend <--> Backend ``` -For Blazor applications, we recommend the BFF be the host for the frontend and the backend of a solution. As you will -see in later sections, this allows for a more straightforward integration and provides a unified approach to managing -authentication and authorization. - -Here's a diagram of what a typical Blazor solution might look like when implemented with the BFF pattern: - -![blazor-architecture](../../images/bff_blazor.svg) - -Note that both the frontend and backend are within a single project within the BFF host, similar to the simplified diagram we previously showed. While it's possible to separate the frontend and backend into separate projects, this comes with additional complexity and is not recommended. - -Let's get into Blazor rending modes and whether the modes are suitable with the BFF pattern. - -## Blazor Rendering Modes - -Blazor -supports [several rendering](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-9.0#render-modes) -modes: - -* **Static Server** - Static server-side rendering (static SSR) -* **Interactive Server** - Interactive server-side rendering (interactive SSR) using Blazor Server and WebSockets. -* **Interactive WebAssembly** - Client-side rendering (CSR) using Blazor WebAssembly. -* **Interactive Auto** - Interactive SSR using Blazor Server initially and then CSR on subsequent visits after the - Blazor bundle is downloaded. - -For developers considering BFF security with these Blazor modes, here is a table with our recommendation of whether to -use the BFF pattern or not: - -| Name | Description | Render Location | Interactive | BFF? | -|-------------------------|------------------------------------------------------------------------------------------------------------------------|---------------------|-------------|------| -| Static Server | Static server-side rendering (static SSR) | Server | ❌ | ❌ | -| Interactive Server | Interactive server-side rendering (interactive SSR) using Blazor Server. | Server | ✅ | ❌ | -| Interactive WebAssembly | Client-side rendering (CSR) using Blazor WebAssembly | Client | ✅ | ✅ | -| Interactive Auto | Interactive SSR using Blazor Server initially and then CSR on subsequent visits after the Blazor bundle is downloaded. | Server, then client | ✅ | ✅ | - -See the following sections for a more detailed explanation of each mode and how it works with the BFF, if at all. - -### Static Server - -:::caution -We advise not using the BFF pattern with this rendering mode as interactivity is limited, though you may want to consider BFF if you have other interactive JavaScript elements. -::: - -The Static server mode allows developers to render pages built with Blazor components, but that doesn’t require any interactivity beyond basic HTML elements. These applications are typically used for static content, such as marketing pages, landing pages, and so on. - -If your application is static, then you don't need to use the BFF pattern, as you can utilize the same security patterns that you would use in a typical ASP.NET Core application. You may still need to use the `AuthenticationStateProvider` to manage authentication state, see the section below for more information. - -While you could certainly use the BFF pattern with a static server implementation for future extensibility plans, it would not add value to an application that is static with no client-side interactivity. - -### Interactive Server - -:::caution -We advise not using the BFF pattern with this rendering mode is managed on the server. Though you may want to consider BFF if you have other interactive JavaScript elements, but it is typically unlikely. -::: - -The Interactive Server mode allows developers to render pages built with Blazor components, and that also allows for interactivity. This mode is ideal for applications that require a rich user experience, such as a web application that allows users to create, edit, and delete data. The interactivity for this mode is handled by the Blazor Server framework powered by WebSockets and more specifically [SignalR](https://dotnet.microsoft.com/en-us/apps/aspnet/signalr). - -The BFF pattern is not typically applicable to this mode, as most interactivity is handled on the server by the Blazor Server framework with state changes being pushed to the client via WebSockets. - -You may still want to explore `AuthenticationStateProvider` for managing authentication state, see the section below for more information. You may also want to explore the [Session Management](/bff/fundamentals/session/index.md) section for more information on how to configure the BFF to use sessions. - -### Interactive WebAssembly - -:::note -**We recommend using the BFF pattern with this rendering mode, as your frontend will be operating with the context of the client, and not the server.** -::: - -The Interactive WebAssembly mode allows developers to render pages built with Blazor components, and that also allows for interactivity. This mode is ideal for applications that require a rich user experience, such as a web application that allows users to create, edit, and delete data. **The interactivity for this mode is handled by the Blazor WebAssembly framework and operates within the context of the client.** - -In a typical Blazor WebAssembly application, you will have three projects: `Client`, `Server`, and `Shared`. The `Client` project is the Blazor application that is rendered by the browser. The `Server` project is the ASP.NET Core web application that hosts the Blazor application. The `Shared` project is a project that contains C# classes that are shared between the `Client` and `Server` projects. - -Let's take a look at how to install and configure the BFF pattern given the above project structure. - -In the `Server` project, you will need to add the following NuGet packages, assuming you will want to use the OpenID Connect handler: - -```bash -dotnet add package Duende.Bff -dotnet add package Duende.Bff.Blazor -dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect -``` - -You will also need to modify the `Program.cs` file in the `Server` project to configure the BFF in the services collection: - -```csharp -// Server/Program.cs -builder.Services.AddBff() - // Add in-memory implementation - .AddServerSideSessions() - .AddBlazorServer(); -``` - -The `AddBlazorServer` method will configure the BFF to use services on the host that allow the client to interact with the server securely. - -You will also need to modify the ASP.NET Core pipeline to use the BFF: - -```csharp {25-26, 31-32} -// Server/Program.cs -var app = builder.Build(); - -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseWebAssemblyDebugging(); -} -else -{ - app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); -} - -app.UseHttpsRedirection(); - -app.UseBlazorFrameworkFiles(); -app.UseStaticFiles(); - -app.UseRouting(); - -app.UseAuthentication(); - -// 👋 Add BFF Middleware -app.UseBff(); - -app.UseAuthorization(); -app.UseAntiforgery(); - -app.MapRazorPages(); - -app.MapControllers() - .RequireAuthorization() - .AsBffApiEndpoint(); - -app.MapFallbackToFile("index.html"); -app.Run(); -``` - -Now, on the client, you will need to add the following NuGet packages: - -```bash -dotnet add package Duende.BFF.Blazor.Client -``` - -You will also need to modify the `Program.cs` file in the `Client` project to configure the BFF in the services collection: - -```csharp {11-14} -// Client/Program.cs -using BlazorWasm.Client; -using Duende.Bff.Blazor.Client; -using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; - -var builder = WebAssemblyHostBuilder.CreateDefault(args); -builder.RootComponents.Add("#app"); -builder.RootComponents.Add("head::after"); - -builder.Services - // 👋 Provides auth state provider that polls the /bff/user endpoint - .AddBffBlazorClient() - .AddCascadingAuthenticationState(); - -await builder.Build().RunAsync(); -``` - -See our [Session Management section](/bff/fundamentals/session/index.md) for more information on how to configure the BFF to use sessions. - -### Interactive Auto +For a detailed architecture diagram and explanation, see the [Architecture overview](/bff/architecture/). -:::note -**We recommend using the BFF pattern with this rendering mode, as your frontend may be executing code within the client context, or the server.** -::: - -Blazor Interactive Auto is a combination of Interactive Server and Interactive WebAssembly, where rendering is initially done on the server, but then the client is updated with the latest WebAssembly version of the application on subsequent visits. - -As you may have guessed, this creates a security state that is unpredictable and can add complexity to your application. Since your Blazor application may be running within the context of the client, you will need to use a BFF and the Duende library to manage authentication state between these two modalities. - -## Authentication State - -The `AuthenticationState` contains information about the currently logged-in user. This is partly populated from -information from the user, but is also enriched with several management claims, such as the Logout URL. - -Blazor uses AuthenticationStateProviders to make authentication state available to components. On the server, the -authentication state is already mostly managed by the authentication framework. However, the BFF will add the Logout url -to the claims using the `AddServerManagementClaimsTransform`. On the client, there are some other claims that might be -useful. The `BffClientAuthenticationStateProvider` will poll the server to update the client on the latest -authentication state, such as the user's claims. This also notifies the front-end if the session is terminated on the -server. - -## Server Side Token Store - -Blazor Server applications have the same token management requirements as a regular ASP.NET Core web application. -Because Blazor Server streams content to the application over a websocket, there often is no HTTP request or response to -interact with during the execution of a Blazor Server application. You therefore cannot use `HttpContext` in a Blazor -Server application as you would in a traditional ASP.NET Core web application. - -This means: - -* you cannot use `HttpContext` extension methods -* you can’t use the ASP.NET authentication session to store tokens -* the normal mechanism used to automatically attach tokens to Http Clients making API calls won't work - -The `ServerSideTokenStore`, together with the Blazor Server functionality in Duende.AccessTokenManagement is -automatically registered when you register Blazor Server. - -For more information on this, see [Blazor Server](/accesstokenmanagement/blazor-server.md) - -## Data Access Techniques - -Depending on the type of Blazor application you are building, you may need to use different techniques to access data from within your components and pages. The following sections will cover some of the common scenarios. - -If your BFF application can directly access data (for example, a database or an unsecured HTTP API), then you have to decide where this information is rendered. - -For server side rendering, you'll typically abstract your data access logic into a separate class (such as a repository or a query object) and inject this into your component for rendering. - -For web assembly rendering, you'll need to make the data available via a web service on the server. Then on the client, you'll need a configured HTTP client that accesses this information securely. - -When using auto-rendering mode, you'll need to make sure that the component gets a different 'data access' component for server rendering vs client rendering. Consider the following diagram: - -![Embedded APIs](../../images/bff_blazor_local_api.svg) - -In this diagram, you'll see the example `IDataAccessor` that has two implementations. One that accesses the data via an HTTP client (for use in WASM) and one that directly accesses the data. - -### Embedded APIs - -Embedded APIs are a way to access data from within a Blazor application without the need to authenticate outside the current security boundary of the client or the backend. - -Below is an example of registering an `IDataAccessor` abstraction. First let's create the `IDataAccessor` interface: - -```csharp -// Shared/IDataAccessor.cs -public interface IDataAccessor -{ - Task GetData(); -} -public record Data(string Value); -``` - -We can implement a Server implementation of the `IDataAccessor` interface. - -```csharp -// Server/ServerWeatherClient.cs -// Create a class that would actually get the data from the database -internal class ServerWeatherClient() : IDataAccessor -{ - public Task GetData() - { - // get the actual data from the database - } -} -``` -and register it in the `Program.cs` file: - -```csharp -// Server/Program.cs -// Register the server implementation for accessing some data -builder.Services.AddSingleton(); -``` - -Then we can use the `IDataAccessor` in our endpoints: - -``` -// Server/Program.cs -// Register an api that will access the data -app.MapGet("/some_data", async (IDataAccessor dataAccessor) => await dataAccessor.GetData()) - .RequireAuthorization() - .AsBffApiEndpoint(); -``` - -We can also register a `HttpClientDataAccessor` that will be used by the Blazor client to access the data. - -```csharp -// Client/Program.cs -// Setup on the client -// Register an HTTP client that can access the data via an Embedded API. -builder.Services.AddLocalApiHttpClient(); - -// Register an adapter that would abstract between the data accessor and the http client. -builder.Services.AddSingleton(sp => sp.GetRequiredService()); - -internal class HttpClientDataAccessor(HttpClient client) : IDataAccessor -{ - public async Task GetSomeData() => await client.GetFromJsonAsync("/some_data") - ?? throw new JsonException("Failed to deserialize"); -} -``` - -Note that data access is contained within the security boundary of the host, so we never need to pass a token to any client to access data. This is what we mean by 'embedded' APIs. - -### Secured Remote APIs - -If your BFF needs to secure access to remote APIs, then your components can both directly use a (typed) `HttpClient`. How this `HttpClient` is configured is quite different on the client vs the server though. - -* On the **Client**, the HTTP client needs to be secured with the authentication cookie and CORS protection headers. This - then calls the http endpoint on the server. - -* On the **Server**, you'd need to expose the proxied http endpoint. This then uses a http client that's configured to send access tokens. These may or may not contain a user token. - -This diagram shows this in more detail: - -![remote APIs](../../images/bff_blazor_remote_api.svg) - -```csharp -// Server/Program.cs -app.MapRemoteBffApiEndpoint("/remote-apis/user-token", new Uri("https://localhost:5010")) - -builder.Services.AddUserAccessTokenHttpClient("backend", - configureClient: client => client.BaseAddress = new Uri("https://localhost:5010/")); -``` - -Then in the client application, we can use the `HttpClient` to access the remote API. - -```csharp -// Copyright (c) Duende Software. All rights reserved. -// Licensed under the MIT License. See LICENSE in the project root for license information. - -using BlazorWasm.Client; -using Duende.Bff.Blazor.Client; -using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; - -var builder = WebAssemblyHostBuilder.CreateDefault(args); -builder.RootComponents.Add("#app"); -builder.RootComponents.Add("head::after"); - -builder.Services - .AddBffBlazorClient() // Provides auth state provider that polls the /bff/user endpoint - .AddCascadingAuthenticationState(); - -builder.Services.AddRemoteApiHttpClient("backend"); -builder.Services.AddTransient(sp => sp.GetRequiredService().CreateClient("backend")); - -await builder.Build().RunAsync(); -``` +## Where to Go Next -## Other Resources +| 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 | -Here are some other resources that may be useful as you implement security in your Blazor applications: +## See Also -* [Access Token Management](/accesstokenmanagement/index.mdx) -* [Blazor Server](/accesstokenmanagement/blazor-server.md) -* [IdentityServer Quickstarts](/identityserver/quickstarts/0-overview.md) -* [Big Picture](/identityserver/overview/big-picture.md) \ No newline at end of file +- [Access Token Management](/accesstokenmanagement/index.mdx) +- [Blazor Server token management](/accesstokenmanagement/blazor-server.md) +- [IdentityServer Quickstarts](/identityserver/quickstarts/0-overview.md) diff --git a/astro/src/content/docs/bff/fundamentals/blazor/rendering-modes.mdx b/astro/src/content/docs/bff/fundamentals/blazor/rendering-modes.mdx new file mode 100644 index 000000000..efea4adbd --- /dev/null +++ b/astro/src/content/docs/bff/fundamentals/blazor/rendering-modes.mdx @@ -0,0 +1,106 @@ +--- +title: Blazor Rendering Modes & BFF +description: Learn which Blazor rendering modes are compatible with the BFF security pattern and why. +sidebar: + label: Rendering Modes + order: 2 +--- + +import { CardGrid, LinkCard } from "@astrojs/starlight/components"; + +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 + +| 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 + +:::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 + +:::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/index.md) features of BFF if you want server-side session control, but the BFF security pattern itself is not required. + +## Interactive WebAssembly + +:::note +**BFF is recommended for Interactive WebAssembly.** Your Blazor components execute inside the browser and can be subject to XSS and token theft attacks. +::: + +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 + +:::note +**BFF is recommended for Interactive Auto.** This mode starts as Interactive Server but transitions to WebAssembly, creating an unpredictable execution context. +::: + +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 + +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 + +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.md). + +## See Also + + + + + + diff --git a/astro/src/content/docs/bff/fundamentals/deployment.mdx b/astro/src/content/docs/bff/fundamentals/deployment.mdx new file mode 100644 index 000000000..b51709f63 --- /dev/null +++ b/astro/src/content/docs/bff/fundamentals/deployment.mdx @@ -0,0 +1,177 @@ +--- +title: Production Deployment +description: Guide for deploying Duende BFF to production, covering load balancing, data protection, health checks, cookie domain configuration, and monitoring. +sidebar: + order: 90 + label: Deployment +--- + +import { CardGrid, LinkCard } from "@astrojs/starlight/components"; + +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 + +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 + +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); +``` + +:::caution[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) + +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 + + + +### 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 + +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 + +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; + }); +``` + +:::caution[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 + +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 + +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 + +| 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 + +``` +# 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 + + + + + + + diff --git a/astro/src/content/docs/bff/fundamentals/middleware-pipeline.mdx b/astro/src/content/docs/bff/fundamentals/middleware-pipeline.mdx new file mode 100644 index 000000000..ce0456156 --- /dev/null +++ b/astro/src/content/docs/bff/fundamentals/middleware-pipeline.mdx @@ -0,0 +1,154 @@ +--- +title: Middleware Pipeline +description: The correct ASP.NET Core middleware order for Duende BFF applications, with explanations of what each component does and common misconfiguration pitfalls. +sidebar: + label: Middleware Pipeline + order: 5 +--- + +import { CardGrid, LinkCard } from "@astrojs/starlight/components"; + +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 + +```csharp +// Program.cs +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 + +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 | + +:::caution[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 + +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 + +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 + +| 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 + + + + + + + diff --git a/astro/src/content/docs/bff/fundamentals/multi-frontend/configuration.mdx b/astro/src/content/docs/bff/fundamentals/multi-frontend/configuration.md similarity index 100% rename from astro/src/content/docs/bff/fundamentals/multi-frontend/configuration.mdx rename to astro/src/content/docs/bff/fundamentals/multi-frontend/configuration.md diff --git a/astro/src/content/docs/bff/fundamentals/multi-frontend/index.mdx b/astro/src/content/docs/bff/fundamentals/multi-frontend/index.mdx index 447092ace..5862eb7e5 100644 --- a/astro/src/content/docs/bff/fundamentals/multi-frontend/index.mdx +++ b/astro/src/content/docs/bff/fundamentals/multi-frontend/index.mdx @@ -90,7 +90,7 @@ The order in which configuration is applied is 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.mdx). +as via [Configuration](configuration.md). ## Frontend Selection diff --git a/astro/src/content/docs/bff/fundamentals/options.md b/astro/src/content/docs/bff/fundamentals/options.md index 6f808410b..87feb09f8 100644 --- a/astro/src/content/docs/bff/fundamentals/options.md +++ b/astro/src/content/docs/bff/fundamentals/options.md @@ -24,6 +24,93 @@ builder.Services.AddBff(options => }) ``` +## 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 + +```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) + +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 + +```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 * **`EnforceBffMiddleware`** diff --git a/astro/src/content/docs/bff/fundamentals/session/handlers.mdx b/astro/src/content/docs/bff/fundamentals/session/handlers.mdx index d37adfa74..9f43e5ee2 100644 --- a/astro/src/content/docs/bff/fundamentals/session/handlers.mdx +++ b/astro/src/content/docs/bff/fundamentals/session/handlers.mdx @@ -57,7 +57,7 @@ services.AddBff() ``` Each frontend can have custom OpenID Connect configuration and Cookie Configuration. This can both be configured programmatically -via [Configuration](/bff/fundamentals/multi-frontend/configuration.mdx). +via [Configuration](/bff/fundamentals/multi-frontend/configuration.md). ## Manually Configuring Authentication diff --git a/astro/src/content/docs/bff/fundamentals/session/index.md b/astro/src/content/docs/bff/fundamentals/session/index.md deleted file mode 100644 index 1ff2abf6a..000000000 --- a/astro/src/content/docs/bff/fundamentals/session/index.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Authentication & Session Management -description: Learn how to set up authentication and session management components in ASP.NET Core BFF applications, including OpenID Connect, cookie handling, and back-channel logout support. -date: 2020-09-10T08:20:20+02:00 -sidebar: - order: 1 - label: Overview -redirect_from: - - /bff/session/ - - /bff/v2/session/ - - /bff/v3/fundamentals/session/ - - /identityserver/v5/bff/session/ - - /identityserver/v6/bff/session/ - - /identityserver/v7/bff/session/ ---- - -This section deals with setting up the following components - -* the ASP.NET Core authentication system - * the OpenID Connect handler - * the cookie handler -* the BFF session management endpoints -* server-side sessions -* back-channel logout support \ No newline at end of file diff --git a/astro/src/content/docs/bff/fundamentals/session/index.mdx b/astro/src/content/docs/bff/fundamentals/session/index.mdx new file mode 100644 index 000000000..952fda1a9 --- /dev/null +++ b/astro/src/content/docs/bff/fundamentals/session/index.mdx @@ -0,0 +1,135 @@ +--- +title: Authentication & Session Management +description: Learn how BFF manages authentication sessions — from the initial OIDC login to server-side session storage, token lifecycle, and back-channel logout. +date: 2020-09-10T08:20:20+02:00 +sidebar: + order: 1 + label: Overview +redirect_from: + - /bff/session/ + - /bff/v2/session/ + - /bff/v3/fundamentals/session/ + - /identityserver/v5/bff/session/ + - /identityserver/v6/bff/session/ + - /identityserver/v7/bff/session/ +--- + +import { CardGrid, LinkCard } from "@astrojs/starlight/components"; + +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 + +```mermaid +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 + +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 + +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 | + +:::tip[Recommended for production] +Use server-side sessions for any production deployment. They enable back-channel logout support, avoid cookie size issues with large claim sets, and allow the server to forcibly end user sessions. +::: + +### 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 + +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 + +| 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 + + + + + + + + diff --git a/astro/src/content/docs/bff/fundamentals/session/management/back-channel-logout.md b/astro/src/content/docs/bff/fundamentals/session/management/back-channel-logout.md index 1decc6edc..943dc29b2 100644 --- a/astro/src/content/docs/bff/fundamentals/session/management/back-channel-logout.md +++ b/astro/src/content/docs/bff/fundamentals/session/management/back-channel-logout.md @@ -42,3 +42,7 @@ Back-channel logout tokens include a sub (subject ID) and sid (session ID) claim 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.md#session-management) to true. + +## 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/). diff --git a/astro/src/content/docs/bff/fundamentals/session/management/diagnostics.md b/astro/src/content/docs/bff/fundamentals/session/management/diagnostics.md index da8402686..0912940b0 100644 --- a/astro/src/content/docs/bff/fundamentals/session/management/diagnostics.md +++ b/astro/src/content/docs/bff/fundamentals/session/management/diagnostics.md @@ -21,4 +21,6 @@ The `/bff/diagnostics` endpoint returns the current user and 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 +To add custom logic to the diagnostics endpoint, see [Diagnostics Endpoint Extensibility](/bff/extensibility/management/diagnostics/). diff --git a/astro/src/content/docs/bff/fundamentals/session/management/login.md b/astro/src/content/docs/bff/fundamentals/session/management/login.md index b825aa044..577d54ada 100644 --- a/astro/src/content/docs/bff/fundamentals/session/management/login.md +++ b/astro/src/content/docs/bff/fundamentals/session/management/login.md @@ -32,3 +32,7 @@ After authentication is complete, the login endpoint will redirect back to your ```js window.location = "/login?returnUrl=/logged-in"; ``` + +## Customize This Endpoint + +To add custom logic before or after the login endpoint processes a request, see [Login Endpoint Extensibility](/bff/extensibility/management/login/). diff --git a/astro/src/content/docs/bff/fundamentals/session/management/logout.md b/astro/src/content/docs/bff/fundamentals/session/management/logout.md index d23c3aecb..43c85c43f 100644 --- a/astro/src/content/docs/bff/fundamentals/session/management/logout.md +++ b/astro/src/content/docs/bff/fundamentals/session/management/logout.md @@ -30,3 +30,7 @@ window.location = `${logoutUrl}&returnUrl=/logged-out`; ## 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 + +To add custom logic before or after the logout endpoint processes a request, see [Logout Endpoint Extensibility](/bff/extensibility/management/logout/). + diff --git a/astro/src/content/docs/bff/fundamentals/session/management/silent-login.md b/astro/src/content/docs/bff/fundamentals/session/management/silent-login.md index 152568557..9a1ebaf1d 100644 --- a/astro/src/content/docs/bff/fundamentals/session/management/silent-login.md +++ b/astro/src/content/docs/bff/fundamentals/session/management/silent-login.md @@ -60,3 +60,7 @@ window.addEventListener("message", e => { } }); ``` + +## Customize This Endpoint + +To add custom logic to the silent login endpoint, see [Silent Login Endpoint Extensibility](/bff/extensibility/management/silent-login/). diff --git a/astro/src/content/docs/bff/fundamentals/session/management/user.md b/astro/src/content/docs/bff/fundamentals/session/management/user.md index c0acd96d3..7dd28e71b 100644 --- a/astro/src/content/docs/bff/fundamentals/session/management/user.md +++ b/astro/src/content/docs/bff/fundamentals/session/management/user.md @@ -142,3 +142,7 @@ var req = new Request("/bff/user?slide=false", { }); ``` +## 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/). + diff --git a/astro/src/content/docs/bff/fundamentals/tokens.md b/astro/src/content/docs/bff/fundamentals/tokens.md index 13081e195..5d39a0d85 100644 --- a/astro/src/content/docs/bff/fundamentals/tokens.md +++ b/astro/src/content/docs/bff/fundamentals/tokens.md @@ -92,3 +92,13 @@ await HttpContext.RevokeUserRefreshTokenAsync(); ``` This will invalidate the refresh token at the token service. + +## 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 + diff --git a/astro/src/content/docs/bff/getting-started/blazor.mdx b/astro/src/content/docs/bff/getting-started/blazor.mdx index bf2232be4..553ee0e20 100644 --- a/astro/src/content/docs/bff/getting-started/blazor.mdx +++ b/astro/src/content/docs/bff/getting-started/blazor.mdx @@ -14,449 +14,513 @@ redirect_from: - /identityserver/v7/bff/samples/bff-blazor/ --- -import { Code } from "@astrojs/starlight/components"; +import { Code, Steps } from "@astrojs/starlight/components"; import { Tabs, TabItem } from "@astrojs/starlight/components"; -This quickstart walks you through how to create a BFF Blazor application. The sourcecode for this quickstart is available on [GitHub](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v3/Quickstarts/BlazorBffApp) +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). + +:::note[Version] +This guide targets **Duende BFF v4**. If you are still on v3, expand the v3 tabs in each step below. See the [v3 → v4 upgrade guide](/bff/upgrading/bff-v3-to-v4/) when you are ready to migrate. +::: + +## What You'll 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. + +:::note[Prerequisites] +- .NET 8.0 SDK or later +- Familiarity with Blazor's hosting models (Server vs. WASM) +- An OpenID Connect-compatible identity provider (e.g., [Duende IdentityServer](/identityserver/), Auth0, Azure AD) +::: ## Creating the project structure -The first step is to create a Blazor app. You can do so using the command line: - -```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. - -## Configuring 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 you need to configure the application to use the BFF Blazor application. Add this to your services: - -{/* prettier-ignore */} - - {/* prettier-ignore */} - - ```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 loggin 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(); - ``` - - - {/* prettier-ignore */} - - ```csharp - // BFF setup for blazor - 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 loggin 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. Replace the app.UseAntiforgery() with the code below: - -```csharp -app.UseRouting(); -app.UseAuthentication(); - -// Add the BFF middleware which performs anti forgery protection -app.UseBff(); -app.UseAuthorization(); -app.UseAntiforgery(); - -// Add the BFF management endpoints, such as login, logout, etc. -app.MapBffManagementEndpoints(); -``` + + +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: + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + ```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(); + ``` + + + {/* prettier-ignore */} + + ```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()`: + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + ```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. + ``` + + {/* prettier-ignore */} + + ```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 -To add the BFF to the client project, add the following: + + +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 + +Add the following components to your `BlazorBffApp.Client/Components` folder: + + -```shell -cd .. -cd BlazorBffApp.Client -dotnet add package Duende.BFF.Blazor.Client -``` - -Then add the following to your program.cs: +1. **LoginDisplay.razor** -```csharp -builder.Services - .AddBffBlazorClient(); // Provides auth state provider that polls the /bff/user endpoint + 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. -builder.Services - .AddCascadingAuthenticationState(); -``` + ```razor title="BlazorBffApp.Client/Components/LoginDisplay.razor" + @using Duende.Bff.Blazor.Client + @using Microsoft.AspNetCore.Components.Authorization + @using Microsoft.Extensions.Options -Your application is ready to use BFF now. + @rendermode InteractiveAuto -## Configuring your application to use bff's features + @inject IOptions Options -Add the following components to your BlazorBffApp.Client's Component folder: + + + Hello, @context.User.Identity?.Name + Log Out + + + Log in + + + Log in + + -### LoginDisplay.razor -The following code shows a login / logout button depending on your state. Note, you'll need to use the -logout link from the LogoutUrl claim, because this contains both the correct route and the session id. -Add it to the BffBlazorApp.Client/Components folder + @code { + string BffLogoutUrl(AuthenticationState context) + { + var logoutUrl = context.User.FindFirst(Constants.ClaimTypes.LogoutUrl); + return $"{Options.Value.StateProviderBaseAddress}{logoutUrl?.Value}"; + } + } + ``` -```csharp -@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 - - +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. -@code { - string BffLogoutUrl(AuthenticationState context) - { - var logoutUrl = context.User.FindFirst(Constants.ClaimTypes.LogoutUrl); - return $"{Options.Value.StateProviderBaseAddress}{logoutUrl?.Value}"; - } -} -``` + ```razor title="BlazorBffApp.Client/Components/RedirectToLogin.razor" + @inject NavigationManager Navigation -### RedirectToLogin.razor + @rendermode InteractiveAuto -The following code will redirect users to Identity Server for authentication. Once authentication is complete, -the users will be redirected back to where they came from. Add it to the BffBlazorApp.Client/Components folder + @code { + protected override void OnInitialized() + { + var returnUrl = Uri.EscapeDataString("/" + Navigation.ToBaseRelativePath(Navigation.Uri)); + Navigation.NavigateTo($"bff/login?returnUrl={returnUrl}", forceLoad: true); + } + } + ``` -```csharp -@inject NavigationManager Navigation +3. **Modifications to Routes.razor** -@rendermode InteractiveAuto + Replace the contents of `Routes.razor` so it matches below: -@code { - protected override void OnInitialized() - { - var returnUrl = Uri.EscapeDataString("/" + Navigation.ToBaseRelativePath(Navigation.Uri)); - Navigation.NavigateTo($"bff/login?returnUrl={returnUrl}", forceLoad: true); - } -} -``` + ```razor title="BlazorBffApp.Client/Routes.razor" + @using Microsoft.AspNetCore.Components.Authorization + @using BlazorBffApp.Client.Components -### Modifications to Routes.razor + + + + + @if (context.User.Identity?.IsAuthenticated != true) + { + + } + else + { +

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

+ } +
+
+ +
+
+ ``` -Replace the contents of routes.razor so it matches below: + This ensures that accessing a page that requires authorization automatically redirects the user to the identity provider for authentication. -```csharp -@using Microsoft.AspNetCore.Components.Authorization -@using BlazorBffApp.Client.Components +4. **Modifications to MainLayout.razor** - - - - - @if (context.User.Identity?.IsAuthenticated != true) - { - - } - else - { -

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

- } -
-
- -
-
-``` + Modify your `MainLayout.razor` to include the `LoginDisplay`: -This makes sure that, if you're accessing a page that requires authorization, that you are automatically redirected to Identity Server for authentication. - -### Modifications to MainLayout.razor - -Modify your MainLayout so it matches below: - -```csharp -@inherits LayoutComponentBase -@using BlazorBffApp.Client.Components - -
- - -
-
- -
- -
- @Body -
-
-
- -
- An unhandled error has occurred. - Reload - 🗙 -
-``` - -This adds the LoginDisplay to the header. - -Now your application supports logging in / out. + ```razor title="BlazorBffApp.Client/Layout/MainLayout.razor" + @inherits LayoutComponentBase + @using BlazorBffApp.Client.Components + +
+ + +
+
+ +
+ +
+ @Body +
+
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ ``` + + Now your application supports logging in and out. + +
## Exposing APIs -Now we're going to expose an embedded API for weather forecasts to Blazor WebAssembly (WASM) and call it via a HttpClient. +Now we're going to expose an embedded API for weather forecasts to Blazor WebAssembly (WASM) and call it via an `HttpClient`. -> By default, the system will perform both pre-rendering on the server AND WASM based rendering on the client. For this reason, you'll need to register both a server and client version of a component that retrieves data. -> See the [Microsoft documentation](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-9.0#client-side-services-fail-to-resolve-during-prerendering) for more information on this. +:::note +By default, the system will perform both pre-rendering on the server AND WASM-based rendering on the client. For this reason, you'll need to register both a server and client version of a component that retrieves data. +See the [Microsoft documentation](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-9.0#client-side-services-fail-to-resolve-during-prerendering) for more information. +::: -### Configuring the Client app + -Add a class called WeatherClient to the BffBlazorApp.Client project: +1. **Configuring the Client app** -```csharp -public class WeatherHttpClient(HttpClient client) : IWeatherClient -{ - public async Task GetWeatherForecasts() => await client.GetFromJsonAsync("WeatherForecast") - ?? throw new JsonException("Failed to deserialize"); -} + Add a class called `WeatherHttpClient` to the `BlazorBffApp.Client` project: -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); -} + ```csharp title="BlazorBffApp.Client/WeatherHttpClient.cs" + public class WeatherHttpClient(HttpClient client) : IWeatherClient + { + public async Task GetWeatherForecasts() => + await client.GetFromJsonAsync("WeatherForecast") + ?? throw new JsonException("Failed to deserialize"); + } -// The IWeatherClient interface will form an abstraction between 'server' logic and client logic. -public interface IWeatherClient -{ - Task GetWeatherForecasts(); -} -``` - -Then register this as a component in program.cs. - -```csharp -builder.Services - .AddBffBlazorClient()// Provides auth state provider that polls the /bff/user endpoint - - // Register a HTTP Client that's configured to fetch data from the server. - .AddLocalApiHttpClient() ; - -// Register the concrete implementation with the abstraction -builder.Services.AddSingleton(); -``` - -### Configuring the server - -Add a class called ServerWeatherClient to your BlazorBffApp server project: - -```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: - -```csharp -app.MapGet("/WeatherForecast", (IWeatherClient weatherClient) => weatherClient.GetWeatherForecasts()); -``` - -Also register the client abstraction: - -```csharp -builder.Services.AddSingleton(); -``` - -### Displaying Weather Information From The API - -By default, the blazor template ships with a weather page. - -Change the content of the **Weather.razor** like this: - -```csharp -@page "/weather" -@using BlazorBffApp.Client.Components -@using Microsoft.AspNetCore.Authorization - -@rendermode InteractiveWebAssembly -@attribute [Authorize] - -Weather - - -``` - -Now add a component called WeatherComponent - -```csharp -@inject IWeatherClient WeatherClient -

Weather

- -

This component demonstrates showing data.

- -@if (forecasts == null) -{ -

Loading...

-} -else -{ - - - - - - - - - - - @foreach (var forecast in forecasts) - { - - - - - - - } - -
DateTemp. (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(); - } -} -``` + 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`: + + ```csharp title="BlazorBffApp.Client/Program.cs" + 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: + + ```csharp title="BlazorBffApp/ServerWeatherClient.cs" + 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: + + ```csharp title="BlazorBffApp/Program.cs" + // 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: + + ```razor title="BlazorBffApp.Client/Pages/Weather.razor" + @page "/weather" + @using BlazorBffApp.Client.Components + @using Microsoft.AspNetCore.Authorization + + @rendermode InteractiveWebAssembly + @attribute [Authorize] + + Weather + + + ``` + + Now add a `WeatherComponent.razor`: + + ```razor title="BlazorBffApp.Client/Components/WeatherComponent.razor" + @inject IWeatherClient WeatherClient +

Weather

+ +

This component demonstrates showing data.

+ + @if (forecasts == null) + { +

Loading...

+ } + else + { + + + + + + + + + + + @foreach (var forecast in forecasts) + { + + + + + + + } + +
DateTemp. (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(); + } + } + ``` + +
+ +:::caution[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 + +- [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 diff --git a/astro/src/content/docs/bff/getting-started/index.mdx b/astro/src/content/docs/bff/getting-started/index.md similarity index 100% rename from astro/src/content/docs/bff/getting-started/index.mdx rename to astro/src/content/docs/bff/getting-started/index.md diff --git a/astro/src/content/docs/bff/getting-started/multi-frontend.mdx b/astro/src/content/docs/bff/getting-started/multi-frontend.mdx index ed4170265..d3f8c3545 100644 --- a/astro/src/content/docs/bff/getting-started/multi-frontend.mdx +++ b/astro/src/content/docs/bff/getting-started/multi-frontend.mdx @@ -8,184 +8,202 @@ sidebar: text: v4 variant: tip --- -import { Code } from "@astrojs/starlight/components"; +import { Code, Steps } from "@astrojs/starlight/components"; import { Tabs, TabItem } from "@astrojs/starlight/components"; 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. -:::note +## What You'll 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. + +:::note[Prerequisites] +- .NET 8.0 SDK or later +- Familiarity with the [Single Frontend setup](/bff/getting-started/single-frontend/) +- Duende.BFF v4 or later (multi-frontend is a v4+ feature) +::: + +:::note[Multi-frontend is v4+] Multi-frontend support is available in Duende.BFF v4 and later. The v3-style of wiring up BFF is not supported for this scenario. ::: -## Prerequisites - -- .NET 8.0 or later -- Multiple frontend applications (e.g., React, Angular, Vue, or plain JavaScript) - ## Setting Up A BFF Project For Multiple Frontends -### 1. Create A New ASP.NET Core Project - -```bash title="Terminal" -dotnet new web -n MyMultiBffApp -cd MyMultiBffApp -``` - -### 2. Add The Duende.BFF NuGet Package - -```bash title="Terminal" -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. - -```csharp -// Program.cs -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` - -{/* prettier-ignore */} - - {/* prettier-ignore */} - - - Register multiple frontends directly in code using `AddFrontends`: - - - - - {/* prettier-ignore */} - - You can also load frontend configuration from an `IConfiguration` source, such as a JSON file: - - Example `bffconfig.json`: - - - - Load and use the configuration in `Program.cs`: - - - - - - - -### 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.mdx#5-adding-remote-apis). - -- **Direct proxying per frontend:** - You can configure remote APIs for each frontend individually: - - ```csharp - // Program.cs - 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.mdx#6-adding-server-side-sessions) for details. + + +1. **Create A New ASP.NET Core Project** + + ```bash title="Terminal" + dotnet new web -n MyMultiBffApp + cd MyMultiBffApp + ``` + +2. **Add The Duende.BFF NuGet Package** + + ```bash title="Terminal" + 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. + + ```csharp + // Program.cs + 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`** + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + + Register multiple frontends directly in code using `AddFrontends`: + + + + + {/* prettier-ignore */} + + You can also load frontend configuration from an `IConfiguration` source, such as a JSON file: + + Example `bffconfig.json`: + + + + Load and use the configuration in `Program.cs`: + + + + + + +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: + + ```csharp + // Program.cs + 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 + +- [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 diff --git a/astro/src/content/docs/bff/getting-started/single-frontend.mdx b/astro/src/content/docs/bff/getting-started/single-frontend.mdx index 25761bfb6..a32bdc5d1 100644 --- a/astro/src/content/docs/bff/getting-started/single-frontend.mdx +++ b/astro/src/content/docs/bff/getting-started/single-frontend.mdx @@ -5,341 +5,466 @@ sidebar: order: 10 label: "Single Frontend" --- -import { Code } from "@astrojs/starlight/components"; +import { Code, Steps } from "@astrojs/starlight/components"; import { Tabs, TabItem } from "@astrojs/starlight/components"; 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. -:::note +## What You'll 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 + +:::note[Prerequisites] +- .NET 8.0 SDK or later +- A frontend application (e.g., React, Angular, Vue, or plain JavaScript) +- An OpenID Connect-compatible identity provider (e.g., [Duende IdentityServer](/identityserver/), Auth0, Azure AD) +::: + +:::note[BFF v4 default frontend] Duende.BFF V4 introduced a new way of configuring the BFF, which automatically configures the BFF using recommended practices. If you're upgrading from V3, please refer to the [upgrade guide](/bff/upgrading/bff-v3-to-v4.md). -When in single frontend mode, an implicit default frontend is automatically registered. This ensures all the management routes and OpenID Connect-handling routes are available for your frontend. +When in single frontend mode, an implicit default frontend is automatically registered. This ensures all the management routes and OpenID Connect-handling routes are available for your frontend. When you call `.AddFrontend()` to add a new frontend, the system switches to multi-frontend mode. If you wish to have a default frontend in multi-frontend mode, you'll need to explicitly add it. See [multi-frontend support](/bff/fundamentals/multi-frontend/) for more information on this topic. ::: -## Prerequisites +## Setting Up A BFF project -- .NET 8.0 or later -- A frontend application (e.g., React, Angular, Vue, or plain JavaScript) + + +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`: + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + ```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"; -## Setting Up A BFF project + options.GetClaimsFromUserInfoEndpoint = true; + options.SaveTokens = true; + options.MapInboundClaims = false; -### 1. Create A New ASP.NET Core Project + options.Scope.Clear(); + options.Scope.Add("openid"); + options.Scope.Add("profile"); -Create a new ASP.NET Core Web Application: + // 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; + }); -```sh -dotnet new web -n MyBffApp -cd MyBffApp -``` + builder.Services.AddAuthorization(); -### 2. Add The Duende.BFF NuGet Package + var app = builder.Build(); -Install the Duende.BFF package: + app.UseAuthentication(); + app.UseRouting(); -```sh -dotnet add package Duende.BFF -``` + // adds antiforgery protection for local APIs + app.UseBff(); -### 3. Configure BFF In `Program.cs` - -Add the following to your `Program.cs`: - -{/* prettier-ignore */} - - {/* prettier-ignore */} - - ```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; - }); + // adds authorization for local and remote API endpoints + app.UseAuthorization(); - builder.Services.AddAuthorization(); + app.Run(); - 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(); + + {/* prettier-ignore */} + + ```csharp + builder.Services.AddBff(); - app.Run(); + // 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"; - ``` - - - {/* prettier-ignore */} - - ```csharp - builder.Services.AddBff(); + 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(); - // 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. + // adds authorization for local and remote API endpoints + app.UseAuthorization(); -It's important to mark up the APIs with .AsBffApiEndpoint(), because this adds CSRF protection. + // login, logout, user, backchannel logout... + app.MapBffManagementEndpoints(); -{/* prettier-ignore */} - - {/* prettier-ignore */} - + app.Run(); - + -// Place your custom routes after the 'UseAuthorization()' -app.MapGet("/hello-world", () => "hello-world") - .AsBffApiEndpoint(); // Adds CSRF protection to the controller endpoints`}/> + 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. - - - + :::tip + Always call `.AsBffApiEndpoint()` on your local API routes. Without it, the `X-CSRF` header is not enforced and your endpoints are vulnerable to CSRF attacks. See [Local APIs](/bff/fundamentals/apis/local/) for details. + ::: - - + {/* prettier-ignore */} + + {/* prettier-ignore */} + + + "hello-world") + .AsBffApiEndpoint(); // Adds CSRF protection to the controller endpoints`}/> + + + + + -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. + -```bash title="Terminal" -dotnet add package Duende.BFF.Yarp -``` + + -{/* prettier-ignore */} - - {/* prettier-ignore */} - - - - - - {/* prettier-ignore */} - - - (StringComparer.OrdinalIgnoreCase) - { - { "destination_1", new DestinationConfig { Address = "https://remote-api-address" } } - } - }); +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. -// ... + :::tip + For a comparison of local vs. remote vs. YARP-based APIs, see the [API Overview](/bff/fundamentals/apis/). + ::: -app.UseAuthorization(); + ```bash title="Terminal" + dotnet add package Duende.BFF.Yarp + ``` -// Add the Yarp middleware that will proxy the requests. -app.MapReverseProxy(proxyApp => { - proxyApp.UseAntiforgeryCheck(); -});`}/> + {/* prettier-ignore */} + + {/* prettier-ignore */} + - You can also use an `IConfiguration` instead of programmatically configuring the proxy. + - + // ... -### 6. Adding Server-Side Sessions + // 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);`}/> -{/* prettier-ignore */} - - {/* prettier-ignore */} - + + {/* prettier-ignore */} + - 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. + (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** + + {/* prettier-ignore */} + + {/* prettier-ignore */} + + + 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. + + + + + {/* prettier-ignore */} + - + For production scenarios, you can use Entity Framework to persist sessions in a database. First, add the NuGet package: + + + + Then configure the session store in your `Program.cs`: + + + { + 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. + + + + + + +:::caution[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. +::: - - {/* prettier-ignore */} - +## Frontend Integration - For production scenarios, you can use Entity Framework to persist sessions in a database. First, add the NuGet package: +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 - Then configure the session store in your `Program.cs`: +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. - - { - options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")); +```javascript +// Fetch the current user from the BFF session +async function getUser() { + const response = await fetch('/bff/user', { + headers: { 'X-CSRF': '1' } }); - -// ...existing code for authentication, authorization, etc.`}/> - You will also need to run the Entity Framework migrations to create the necessary tables. + if (response.ok) { + return await response.json(); // Array of { type, value } claim objects + } + + return null; // Not authenticated +} +``` + +### 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 + +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(); +``` + +:::tip +Use `bffFetch` (or an equivalent interceptor in your framework) consistently throughout your frontend. This ensures every authenticated request includes the CSRF header and gracefully handles session expiry. +::: + +### 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 +- [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 diff --git a/astro/src/content/docs/bff/getting-started/templates.mdx b/astro/src/content/docs/bff/getting-started/templates.md similarity index 100% rename from astro/src/content/docs/bff/getting-started/templates.mdx rename to astro/src/content/docs/bff/getting-started/templates.md diff --git a/astro/src/content/docs/bff/index.mdx b/astro/src/content/docs/bff/index.mdx index 05373ab02..b5b7fa176 100644 --- a/astro/src/content/docs/bff/index.mdx +++ b/astro/src/content/docs/bff/index.mdx @@ -22,7 +22,26 @@ redirect_from: import { CardGrid, LinkCard } from "@astrojs/starlight/components"; -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. +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. @@ -50,13 +69,47 @@ If you're starting a new BFF project, consider the following startup guides: * [Multi-frontend BFF](/bff/getting-started/multi-frontend.mdx) * [Blazor](/bff/getting-started/blazor.mdx) +## 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 + +| 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 | + +:::caution[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. +::: + +:::tip[When BFF is the right choice] +Use BFF if any of the following are true: +- Your frontend calls APIs that require user authentication +- You need reliable session management across browsers +- You want protection against XSS token theft +- You're building a new application and want to follow current IETF best practices + +See the [architecture](/bff/architecture/) pages for a deeper discussion of the threat model. +::: + ## 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 +### 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. @@ -68,72 +121,81 @@ With this approach, the browser based application will not have direct access to 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 - -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. +### 3rd party cookies -## CSRF protection +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. -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. +### CSRF protection -# The Duende BFF framework +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. -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. +### The BFF Framework in an application architecture -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) +The following diagram illustrates how the Duende BFF Security Framework fits into a typical application architecture. -## The BFF Framework in an application architecture +```mermaid +flowchart TD + subgraph Browser + SPA["Browser-Based Application"] + CookieJar["🍪 Cookie Jar"] + end -The following diagram illustrates how the Duende BFF Security Framework fits into a typical application architecture. + 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 -![Backend For Frontend application architecture diagram](images/bff_application_architecture.svg) + 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. +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. +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 +### 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. ```mermaid -%%{ init: { 'theme': 'neutral' } }%% sequenceDiagram actor Alice - Alice->>App: /login - App->>Alice: /account 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. ```mermaid -%%{ init: { 'theme': 'neutral' } }%% sequenceDiagram actor Alice - Alice->>App: /login - App->>Alice: /account - App->>Service 1: request - App-->>Service N...: N... request box App session participant App end @@ -143,6 +205,10 @@ sequenceDiagram 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.mdx). @@ -164,51 +230,51 @@ Server-side sessions at IdentityServer allow for more powerful features: 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 +### 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. +Let's look at some of the common ways browser-based apps are typically attacked and what their consequences would be. -### 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. +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 +##### 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. +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 +##### 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. +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 +##### 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. +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. +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 +##### 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. +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: +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 +#### 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 +##### 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. @@ -217,7 +283,6 @@ Browsers have built-in control when cookies should be sent. For example, by sett 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: ```mermaid -%%{ init: { 'theme': 'neutral' } }%% flowchart TD; A[JavaScript makes a cross-domain XHR call] --> B{Is it a GET or HEAD?}; subgraph cors-safe @@ -244,19 +309,19 @@ flowchart TD; 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 +##### 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. +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 +##### 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. +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: @@ -264,12 +329,17 @@ So, effective CSRF attack protection relies on these pillars: 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 +##### 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. +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 -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. +- [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 diff --git a/astro/src/content/docs/bff/samples/index.mdx b/astro/src/content/docs/bff/samples/index.mdx index e5ce3f029..3adcd06ba 100644 --- a/astro/src/content/docs/bff/samples/index.mdx +++ b/astro/src/content/docs/bff/samples/index.mdx @@ -18,10 +18,10 @@ This section contains a collection of clients using our BFF security framework. ## JavaScript Frontend -This sample shows how to use the BFF framework with a JavaScript-based frontend (e.g. SPA). +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. ## Feedback diff --git a/astro/src/content/docs/bff/troubleshooting.md b/astro/src/content/docs/bff/troubleshooting.md new file mode 100644 index 000000000..cca1eaf9b --- /dev/null +++ b/astro/src/content/docs/bff/troubleshooting.md @@ -0,0 +1,253 @@ +--- +title: "Troubleshooting" +description: "Diagnose and fix common problems with Duende BFF: anti-forgery failures, CORS errors, session expiration, YARP misconfigurations, Blazor token issues, and more." +sidebar: + order: 90 +--- + +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 + +**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 + +**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"); +``` + +:::tip +Whenever possible, serve the SPA's `index.html` from the BFF host itself. This makes the frontend and backend same-origin and eliminates CORS complexity entirely. See [UI Hosting](/bff/architecture/ui-hosting/) for options. +::: + +--- + +### 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 + +**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` + +**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 + +**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 + +**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 + +**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 +``` + +:::tip +Server-side sessions are recommended for all production BFF deployments, regardless of claim volume. They also enable server-initiated logout and better session visibility. See [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/) for setup instructions. +::: + +--- + +## 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 diff --git a/astro/src/content/docs/bff/upgrading/bff-v3-to-v4.md b/astro/src/content/docs/bff/upgrading/bff-v3-to-v4.md index a4f4e99df..67e8e8357 100644 --- a/astro/src/content/docs/bff/upgrading/bff-v3-to-v4.md +++ b/astro/src/content/docs/bff/upgrading/bff-v3-to-v4.md @@ -6,6 +6,28 @@ sidebar: order: 20 --- +## 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 + +:::caution[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 diff --git a/astro/src/styles/custom.css b/astro/src/styles/custom.css index ac5996813..be2858387 100644 --- a/astro/src/styles/custom.css +++ b/astro/src/styles/custom.css @@ -45,10 +45,204 @@ display: block; } -:root[data-theme='light'] { - .mermaid { - background: var(--sl-color-gray-7); - } +/* Mermaid diagrams — shared base styles */ +.mermaid { + border-radius: 0.5rem; + padding: 1.5rem 1rem; + margin: 1rem 0; + overflow-x: auto; + text-align: center; +} + +.mermaid svg { + max-width: 100%; + height: auto; +} + +/* Dark mode — vibrant purple palette */ +:root:not([data-theme='light']) .mermaid { + background: #1a1525; + border: 1px solid #6e45af; +} + +:root:not([data-theme='light']) .mermaid .node rect, +:root:not([data-theme='light']) .mermaid .node circle, +:root:not([data-theme='light']) .mermaid .node ellipse, +:root:not([data-theme='light']) .mermaid .node polygon, +:root:not([data-theme='light']) .mermaid .node path { + fill: #2d1f5e !important; + stroke: #916bdb !important; + stroke-width: 1.5px; +} + +:root:not([data-theme='light']) .mermaid .node .label, +:root:not([data-theme='light']) .mermaid .nodeLabel, +:root:not([data-theme='light']) .mermaid .label text, +:root:not([data-theme='light']) .mermaid text { + fill: #ffffff !important; + color: #ffffff !important; +} + +:root:not([data-theme='light']) .mermaid .edgePath .path, +:root:not([data-theme='light']) .mermaid .flowchart-link { + stroke: #916bdb !important; + stroke-width: 1.5px; +} + +:root:not([data-theme='light']) .mermaid .arrowheadPath, +:root:not([data-theme='light']) .mermaid marker path { + fill: #916bdb !important; + stroke: #916bdb !important; +} + +:root:not([data-theme='light']) .mermaid .cluster rect { + fill: #241b3a !important; + stroke: #6e45af !important; + stroke-width: 1.5px; +} + +:root:not([data-theme='light']) .mermaid .cluster text, +:root:not([data-theme='light']) .mermaid .cluster span { + fill: #ffffff !important; + color: #ffffff !important; +} + +/* Styled subgraphs — preserve semantic colors in dark mode */ +:root:not([data-theme='light']) .mermaid .cluster rect[style*="d9ead3"], +:root:not([data-theme='light']) .mermaid .cluster rect[style*="6aa84f"] { + fill: #1a2e1a !important; + stroke: #4a8c4a !important; +} + +:root:not([data-theme='light']) .mermaid .cluster rect[style*="f4cccc"], +:root:not([data-theme='light']) .mermaid .cluster rect[style*="cc0000"] { + fill: #2e1a1a !important; + stroke: #cc4444 !important; +} + +/* Decision diamonds — accent pop (dark only) */ +:root:not([data-theme='light']) .mermaid .node.decision rect, +:root:not([data-theme='light']) .mermaid .node.default.decision polygon, +:root:not([data-theme='light']) .mermaid .node polygon[style] { + fill: #43257c !important; + stroke: #b794f6 !important; +} + +/* Styled nodes — preserve semantic green in dark mode (must follow generic polygon[style]) */ +:root:not([data-theme='light']) .mermaid .node rect[style*="d5e8d4"], +:root:not([data-theme='light']) .mermaid .node polygon[style*="d5e8d4"] { + fill: #1a2e1a !important; + stroke: #4a8c4a !important; +} + +/* Light mode — clean light appearance, let Mermaid defaults show through */ +:root[data-theme='light'] .mermaid { + background: #f5f6f8; + border: 1px solid #dcdfe4; +} + +/* Sequence diagrams — dark mode */ + +/* Kill SVG drop-shadow filter references */ +:root:not([data-theme='light']) .mermaid [filter], +:root:not([data-theme='light']) .mermaid [style*="filter"] { + filter: none !important; +} + +:root:not([data-theme='light']) .mermaid .actor { + fill: #2d1f5e !important; + stroke: #916bdb !important; + stroke-width: 1.5px; +} + +:root:not([data-theme='light']) .mermaid g.actor { + filter: none !important; +} + +:root:not([data-theme='light']) .mermaid .actor-man circle, +:root:not([data-theme='light']) .mermaid .actor-man line { + fill: #916bdb !important; + stroke: #916bdb !important; +} + +:root:not([data-theme='light']) .mermaid text.actor, +:root:not([data-theme='light']) .mermaid .actor tspan, +:root:not([data-theme='light']) .mermaid .actor-box .actor, +:root:not([data-theme='light']) .mermaid g.actor text, +:root:not([data-theme='light']) .mermaid g.actor tspan { + fill: #ffffff !important; + color: #ffffff !important; +} + +:root:not([data-theme='light']) .mermaid .messageLine0, +:root:not([data-theme='light']) .mermaid .messageLine1 { + stroke: #916bdb !important; + stroke-width: 1.5px; +} + +:root:not([data-theme='light']) .mermaid .messageText { + fill: #ffffff !important; +} + +:root:not([data-theme='light']) .mermaid .sequenceNumber { + fill: #ffffff !important; +} + +:root:not([data-theme='light']) .mermaid #arrowhead path, +:root:not([data-theme='light']) .mermaid #crosshead path, +:root:not([data-theme='light']) .mermaid .arrowMarkerPath { + fill: #916bdb !important; + stroke: #916bdb !important; +} + +:root:not([data-theme='light']) .mermaid .activation0, +:root:not([data-theme='light']) .mermaid .activation1, +:root:not([data-theme='light']) .mermaid .activation2 { + fill: #43257c !important; + stroke: #916bdb !important; +} + +:root:not([data-theme='light']) .mermaid .loopLine { + stroke: #6e45af !important; + stroke-width: 1.5px; +} + +:root:not([data-theme='light']) .mermaid .loopText, +:root:not([data-theme='light']) .mermaid .loopText tspan { + fill: #c4aef0 !important; +} + +:root:not([data-theme='light']) .mermaid .labelBox { + fill: #241b3a !important; + stroke: #6e45af !important; +} + +:root:not([data-theme='light']) .mermaid .labelText, +:root:not([data-theme='light']) .mermaid .labelText tspan { + fill: #ffffff !important; +} + +:root:not([data-theme='light']) .mermaid .note { + fill: #43257c !important; + stroke: #916bdb !important; +} + +:root:not([data-theme='light']) .mermaid .noteText, +:root:not([data-theme='light']) .mermaid .noteText tspan { + fill: #ffffff !important; +} + +.mermaid rect.rect { + filter: none !important; +} + +:root:not([data-theme='light']) .mermaid rect.rect { + fill: #241b3a !important; + stroke: #6e45af !important; +} + +:root:not([data-theme='light']) .mermaid .sequenceDiagram line[class="200"] { + stroke: #6e45af !important; } iframe { @@ -56,10 +250,6 @@ iframe { width: 100%; } -.mermaid { - background: var(--sl-color-gray-1); -} - .hero > img { --size: min(10rem, calc(5rem + 6vw)); --blur: calc(var(--size) / 5); @@ -145,7 +335,7 @@ summary { } .hs-error-msg { - text-decoration:var(--sl-color-accent-high) ; + text-decoration-color: var(--sl-color-accent-high); color: var(--sl-color-accent-high); font-size: 0.875rem; } diff --git a/build.cs b/build.cs index 9fcb890a8..09b8e8fed 100644 --- a/build.cs +++ b/build.cs @@ -52,7 +52,7 @@ await RunAsync("docker", $"-v \"{astroPath}:/app\" " + $"-v \"{outputPath}:/output\" " + "-w /app " + - "-e NODE_OPTIONS=\"--max-old-space-size=4096\" " + + "-e NODE_OPTIONS=\"--max-old-space-size=8192\" " + "node:24-slim " + "sh -c \"npm ci && npm run build && cp -r dist/. /output/\"", configureEnvironment: env => env.Add("MSYS_NO_PATHCONV", "1"));