diff --git a/BFF/v2/Angular/README.md b/BFF/v2/Angular/README.md new file mode 100644 index 000000000..4adf5702a --- /dev/null +++ b/BFF/v2/Angular/README.md @@ -0,0 +1,30 @@ +# BFF Angular Sample + +This sample shows an [Angular](https://angular.dev/) single-page application hosted by a BFF. The BFF is the only thing the browser talks to: it serves the Angular app, runs the OpenID Connect flow, and holds the tokens, so nothing sensitive ever lands in JavaScript. + +There is also a separate JWT-protected ToDo API in the solution. It is not used in the default configuration — the BFF hosts the ToDo endpoints itself — but the sample ships it so you can switch to the reverse-proxy style with a one-line change. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`Angular.Bff`** — the BFF, running on `https://localhost:6001`. It serves the Angular app from the build output, maps the `/bff/*` management endpoints, and hosts the ToDo API in the same process. +- **`angular.client`** — the Angular SPA, an `.esproj` project so it can take part in the .NET solution and the VS launch profiles. Its dev server runs on `https://localhost:4200`. +- **`Angular.Api`** — a separate JWT-protected ToDo API on `https://localhost:7001`. Not called by default. + +## How to Run + +1. Run the `Angular.Bff` project. This starts the Angular dev server through the SPA proxy and the BFF together. +1. Browse to `https://localhost:6001` and use the login button. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create and delete ToDo items, open **Show User Session** to see the claims the BFF returns, and then log out. + +## What to Look For + +- **The SPA proxy** — `Angular.Bff.csproj` sets `SpaRoot`, `SpaProxyLaunchCommand` (`npm start`) and `SpaProxyServerUrl` (`https://localhost:4200`). During development the browser actually talks to the Angular dev server, and the dev server forwards `/bff`, `/signin-oidc`, `/signout-callback-oidc` and `/todos` on to the BFF (see `proxy.conf.js`). That is what keeps everything same-origin from the browser's point of view. +- **The authentication service** — `src/app/authentication.service.ts` calls `bff/user` once and caches the result with `shareReplay(1)`. `getIsAuthenticated`, `getUsername` and `getLogoutUrl` are just projections over that one request, and a failed call is turned into an anonymous session rather than an error. +- **The CSRF header** — `src/app/csrf-header.interceptor.ts` is a class-based `HttpInterceptor` registered through `HTTP_INTERCEPTORS` (with `provideHttpClient(withInterceptorsFromDi())`). It adds `X-CSRF: 1` to every outgoing request, which is what lets the BFF reject cross-site request forgery on its API endpoints. +- **Login and logout are redirects** — the nav menu links to `/bff/login`, and the logout link is the `bff:logout_url` claim from `/bff/user`. The client never constructs a token request itself. +- **The local API** — `Program.cs` maps `/todos` with `MapGroup("/todos").ToDoGroup().RequireAuthorization().AsBffApiEndpoint()`. The `AsBffApiEndpoint()` part is what requires both a valid session and a valid CSRF header. +- **Switching to the remote API** — `Program.cs` also contains a commented-out `MapRemoteBffApiEndpoint("/todos", "https://localhost:7001/todos").RequireAccessToken(TokenType.User)`. Comment the local group out and this one in, and the same Angular code now talks to `Angular.Api` through the YARP proxy with the user's access token attached. +- **Client-side routing** — `app.MapFallbackToFile("/index.html")` makes the deep links (`/user-session`) work when the BFF serves the built app rather than the dev server. diff --git a/BFF/v2/BlazorWasm/README.md b/BFF/v2/BlazorWasm/README.md new file mode 100644 index 000000000..b39e7bbcb --- /dev/null +++ b/BFF/v2/BlazorWasm/README.md @@ -0,0 +1,35 @@ +# BFF Blazor WebAssembly Sample + +This sample shows a standalone Blazor WebAssembly application protected by a [Backend-for-Frontend (BFF)](https://docs.duendesoftware.com/identityserver/bff), so that no tokens ever reach the browser. + +An ordinary ASP.NET Core application authenticates the user with OIDC and then serves the Blazor WebAssembly app with `UseBlazorFrameworkFiles()` and `MapFallbackToFile("index.html")`. The WASM app talks to the BFF's own endpoints and never sees an access token. + +This version deliberately does the client-side plumbing **by hand**, which makes it useful for understanding what the official Blazor packages do for you in later versions. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`BlazorWasm.Server`** — the BFF host, running on `https://localhost:7189`. It handles the OIDC login, maps the `/bff/*` management endpoints, and serves the WebAssembly app itself. +- **`BlazorWasm.Client`** — the Blazor WebAssembly application. +- **`BlazorWasm.Shared`** — a small class library holding the `WeatherForecast` model that both projects use. + +Because the host serves the WebAssembly app, **the only URL you need is `https://localhost:7189`**. + +## How to Run + +1. Run the `BlazorWasm.Server` project. +1. Browse to `https://localhost:7189` and use the login link in the navigation. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Visit the Fetch Data page, which calls a protected BFF endpoint, then sign out. + +This sample does not run its own identity server, so you need internet access to reach the demo server. + +## What to Look For + +- **The custom authentication state provider** — `BlazorWasm.Client/BFF/BffAuthenticationStateProvider.cs` is an `AuthenticationStateProvider` that reads the session by calling `bff/user?slide=false`, deserialises the claim list, and builds a `ClaimsIdentity`. It caches the result for 60 seconds and runs a background timer to notice when the session disappears. This is roughly what `AddBffBlazorClient()` gives you in later versions. +- **The custom CSRF handler** — `BlazorWasm.Client/BFF/AntiforgeryHandler.cs` is an eleven-line `DelegatingHandler` that adds the `X-CSRF: 1` header to every outgoing request. Registering it with `AddHttpClient("backend", ...).AddHttpMessageHandler()` is how the client satisfies the CSRF requirement of a BFF API endpoint. +- **Server-side sessions** — the `__Host-blazor` cookie is `SameSite=Strict`, so the BFF does not need a server-side session store; the token is held in the encrypted cookie. +- **Protecting an endpoint** — `BlazorWasm.Server/Program.cs` maps its controllers with `MapControllers().RequireAuthorization().AsBffApiEndpoint()`. The `AsBffApiEndpoint()` call is what adds the CSRF header check on top of ordinary authorization. +- **Middleware order** — `app.UseBff()` sits between `UseAuthentication()` and `UseAuthorization()`. +- **The CSRF header itself** — this version relies on the header check performed by `UseBff()`; it does not additionally call ASP.NET Core's `UseAntiforgery()` middleware. diff --git a/BFF/v2/DPoP/README.md b/BFF/v2/DPoP/README.md new file mode 100644 index 000000000..3ee84a550 --- /dev/null +++ b/BFF/v2/DPoP/README.md @@ -0,0 +1,30 @@ +# BFF DPoP Sample + +This sample shows a BFF that requests *DPoP-bound* access tokens (Demonstration of Proof of Possession, [RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)) and calls a downstream API with them. + +Instead of a bearer token that anyone can replay, DPoP binds the access token to a key that only the BFF holds. For every API call the BFF generates a short-lived *DPoP proof* JWT that covers the HTTP method and target URI, and the API checks that proof against the request it actually received. A stolen access token on its own is therefore useless without the corresponding private key. + +The sample shows the four access-token flavours side by side — anonymous, user, client credentials, and user-or-client — both through the BFF's own endpoint helpers and through raw YARP routes. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`DPoP.Api`** — the downstream API, running on `https://localhost:6001`. It validates the access token *and* the DPoP proof on every request, and echoes back the path, message and headers. It ships a self-contained DPoP validation implementation under `DPoP.Api/DPoP/`, so you can see exactly what a resource server has to do. +- **`DPoP.Bff`** — the BFF, running on `https://localhost:5002`. It hosts a small static UI and proxies the API calls on the browser's behalf. + +## How to Run + +1. Run the `DPoP.Bff` and `DPoP.Api` projects. +1. Browse to `https://localhost:5002` and log in. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Try the different endpoints and watch the requests and responses in the browser's network tab. + +## What to Look For + +- **Enabling DPoP in the BFF** — `Program.cs` in `DPoP.Bff` creates a 2048-bit RSA key, converts it to a JWK, sets `Alg` to `PS256`, and assigns the serialized JWK to `options.DPoPJsonWebKey` inside `AddBff(options => { ... })`. Setting this key is all it takes to switch the BFF over to DPoP. +- **Token types** — the BFF exposes `/api/anonymous`, `/api/user-token`, `/api/client-token` and `/api/user-or-client-token`, each guarded by the matching token requirement, plus `/api/optional-user-token` for the "send a token if we have one" case. +- **YARP routes** — `/yarp/anonymous`, `/yarp/user-token`, `/yarp/client-token` and `/yarp/user-or-client-token` show the same calls driven purely by the YARP configuration in `YarpConfigurator.cs`. +- **Proof validation on the API** — `DPoP.Api/DPoP/DPoPProofValidator.cs` validates the proof's `htm`, `htu` and `ath` claims against the incoming request and access token, and enforces a one-second proof validity window. +- **DPoP nonces** — the API supports server-issued nonces. `DPoPOptions.ValidateNonce` defaults to `false`; when enabled the API responds with a `DPoP-Nonce` header and rejects the request until the client retries with that nonce. +- **No forwarded headers on the API** — the API deliberately does *not* use forwarded-headers middleware, because it would change the `htu` that the DPoP proof is validated against. diff --git a/BFF/v2/JsBffSample/README.md b/BFF/v2/JsBffSample/README.md new file mode 100644 index 000000000..ebdff7ffe --- /dev/null +++ b/BFF/v2/JsBffSample/README.md @@ -0,0 +1,27 @@ +# BFF JavaScript Sample + +This sample shows the most basic shape of a BFF: an ASP.NET Core application that serves a plain JavaScript UI and handles the authentication and token handling for the browser, so no tokens ever reach JavaScript. + +The UI is served from `wwwroot` and talks to same-origin endpoints only. Log in and out happen through the BFF's management endpoints, and the session is read back from `/bff/user`. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`FrontendHost`** — the BFF, running on `https://localhost:5010`. It serves the static UI (`index.html`, `todo.js`, `session.js`), maps the `/bff/*` management endpoints, and hosts the ToDo API in the same process. +- **`BackendApiHost`** — a separate JWT-protected ToDo API, running on `https://localhost:5020`. It is not used in the default configuration, but it is there so you can compare the two styles. + +## How to Run + +1. Run the `FrontendHost` and `BackendApiHost` projects. +1. Browse to `https://localhost:5010` and use the login button. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create, update and delete ToDo items, then reload the page to see the session restored from the cookie. + +## What to Look For + +- **Login and logout** — the UI simply links to `/bff/login?returnUrl=/` and `/bff/logout?returnUrl=/`. All of the OAuth/OIDC flow happens in the BFF. +- **Reading the session** — `wwwroot/session.js` fetches `/bff/user` and renders the user's name and claims. JavaScript never sees an access token; it only learns *who* is logged in. +- **The CSRF header** — every call from the UI sends an `x-csrf: 1` header. This is what lets the BFF reject cross-site request forgery on its API endpoints. +- **The local API** — `FrontendHost/ToDoController.cs` is an ordinary MVC controller marked with `.RequireAuthorization().AsBffApiEndpoint()`. That marks it as a BFF endpoint, which requires both a valid session and a valid CSRF header. +- **Switching to a remote API** — `Program.cs` contains a commented-out `MapRemoteBffApiEndpoint("/todos", "https://localhost:5020/todos")` showing the alternative: keep the same UI and let the BFF proxy the calls to a real downstream API instead of handling them locally. diff --git a/BFF/v2/JsBffYarpSample/README.md b/BFF/v2/JsBffYarpSample/README.md new file mode 100644 index 000000000..fb79200aa --- /dev/null +++ b/BFF/v2/JsBffYarpSample/README.md @@ -0,0 +1,28 @@ +# BFF JavaScript Sample with YARP + +This sample is the reverse-proxy flavour of the [JavaScript BFF sample](../v4/JsBffSample). The UI is identical, but instead of handling the ToDo calls itself, the BFF runs a [YARP](https://github.com/dotnet/yarp) reverse proxy that forwards `/todos/**` to a real downstream API, attaching the user's access token on the way out. + +This is the pattern to reach for when the API already exists and you do not want to hand-write a controller per endpoint — the BFF becomes a secure gateway instead of a second implementation of your API. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`FrontendHost`** — the BFF, running on `https://localhost:5010`. It serves the static UI (`index.html`, `todo.js`, `session.js`), maps the `/bff/*` management endpoints, and hosts the reverse proxy. +- **`BackendApiHost`** — the downstream JWT-protected ToDo API, running on `https://localhost:5020`. Unlike in the non-YARP sample, this one really is called. + +## How to Run + +1. Run the `FrontendHost` and `BackendApiHost` projects. +1. Browse to `https://localhost:5010` and use the login button. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create and delete ToDo items, and watch the proxied calls in the browser's network tab. + +## What to Look For + +- **The proxy pipeline** — `Program.cs` calls `AddBff()`, then `AddReverseProxy().AddTransforms()`, then the sample's own `YarpConfigurator.Configure()`. The provider is the piece that attaches the user's access token to each outgoing request. +- **Routes in code** — `YarpConfigurator.cs` builds the route and cluster configuration in C# with `LoadFromMemory`. The route matches `/todos/{**catch-all}`, points at the `cluster1` cluster, and declares the token requirement with `.WithAccessToken(TokenType.User)`. The cluster's single destination is `https://localhost:5020`. +- **Loading config from memory** — `InMemoryConfigProvider.cs` implements `IProxyConfigProvider` and `IProxyConfig` so the routes can be handed to YARP as an in-memory object rather than a `appsettings.json` section. That is what makes the route able to carry BFF-specific configuration such as the access token requirement. +- **Mapping the proxy** — `app.MapBffReverseProxy()` is the shorthand for `MapReverseProxy().AsBffApiEndpoint()`. The `AsBffApiEndpoint()` part is what enforces the session check and the CSRF header. +- **Going back to a local API** — the sample keeps `ToDoController.cs` and the matching `MapControllers().AsBffApiEndpoint()` call, commented out, so you can switch between the two styles and compare. +- **The CSRF header** — the UI sends `x-csrf: 1` on its requests, which the BFF requires before it will forward anything. diff --git a/BFF/v2/React/README.md b/BFF/v2/React/README.md new file mode 100644 index 000000000..fc42d06dd --- /dev/null +++ b/BFF/v2/React/README.md @@ -0,0 +1,43 @@ +# BFF React Sample + +This sample shows a [React](https://react.dev/) single-page application hosted by a BFF project. The SPA is developed with [Vite](https://vite.dev/), but in production it is served as static files from the BFF itself, so everything ends up same-origin and no tokens ever reach JavaScript. + +The BFF runs the authorization code flow against the [Duende demo identity server](https://demo.duendesoftware.com), keeps the tokens server-side, and exposes the ToDo API to the browser through its own BFF endpoints. A second, standalone JWT-protected API is included so you can compare hosting the API in the BFF process against proxying to a real downstream service. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`React.Bff`** — the BFF, running on `https://localhost:6001`. It serves the SPA, maps the `/bff/*` management endpoints, and hosts the ToDo API in the same process. +- **`React.Api`** — a separate JWT-protected ToDo API, running on `https://localhost:7001`. It is not used in the default configuration, but it is there so you can compare the two styles. +- **`react.client`** — the Vite dev server, running on `https://localhost:5173`. It is a build-time dependency of `React.Bff` and is only used during development. + +## How to Run + +1. Run the `React.Bff` and `React.Api` projects. +1. Browse to `https://localhost:6001` and use the login button. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create, update and delete ToDo items, then reload the page to see the session restored from the cookie. + +Running `React.Bff` starts the Vite dev server through the ASP.NET Core SPA proxy, so you get hot module replacement while developing. The Vite server proxies `/bff/*`, `/todos`, `/signin-oidc` and `/signout-callback-oidc` back to the BFF, which is why the app can use same-origin relative URLs in development and in production alike. + +## What to Look For + +- **Login and logout** — the UI links to `/bff/login` and `/bff/logout`. All of the OAuth/OIDC flow happens in the BFF. +- **Reading the session** — `react.client/src/components/UserSession.jsx` fetches `bff/user` and renders the user's claims. JavaScript never sees an access token; it only learns *who* is logged in. +- **The CSRF header** — every call from the UI sends an `X-CSRF: 1` header. This is what lets the BFF reject cross-site request forgery on its API endpoints. +- **The local API** — `React.Bff/ToDoEndpointGroup.cs` holds a small in-memory ToDo store, and `Program.cs` maps it with `.RequireAuthorization().AsBffApiEndpoint()`. That marks it as a BFF endpoint, which requires both a valid session and a valid CSRF header. +- **Switching to a remote API** — `Program.cs` contains a commented-out `MapRemoteBffApiEndpoint("/todos", "https://localhost:7001/todos").RequireAccessToken(Duende.Bff.TokenType.User)` showing the alternative: keep the same UI and let the BFF proxy the calls to a real downstream API instead of handling them locally. The UI does not change at all. +- **Validating tokens in the API** — `React.Api` is a plain JWT bearer resource server. It checks the `api` audience and guards the ToDo endpoints with two policies: `ApiCaller` (the token carries the `api` scope) and `InteractiveUser` (the token carries a `sub`, so a real user is behind it). +- **Client-side routing** — `app.MapFallbackToFile("/index.html")` makes the BFF return the SPA for unknown paths, so a deep link such as `/users` still boots the React app. +- **Explicit authentication schemes** — in BFF 2.x the cookie and OIDC handlers are registered by hand and the default schemes are named `cookie` / `oidc`: + ```csharp + builder.Services.AddAuthentication(options => + { + options.DefaultScheme = "cookie"; + options.DefaultChallengeScheme = "oidc"; + options.DefaultSignOutScheme = "oidc"; + }).AddCookie("cookie", options => { /* ... */ }) + .AddOpenIdConnect("oidc", options => { /* ... */ }); + ``` + BFF 4.x folds this into the BFF builder via `.ConfigureOpenIdConnect(...)` and `.ConfigureCookies(...)`. See the [BFF v4 sample](../v4/React) for that style. diff --git a/BFF/v2/SplitHosts/README.md b/BFF/v2/SplitHosts/README.md new file mode 100644 index 000000000..c91ee3161 --- /dev/null +++ b/BFF/v2/SplitHosts/README.md @@ -0,0 +1,29 @@ +# BFF Split Hosts Sample + +This sample shows what changes when the SPA and the BFF are **not** on the same origin. Instead of one application serving both the UI and the BFF endpoints, the UI is served by a bare static-file host on `https://localhost:5011` and the BFF runs separately on `https://localhost:5010`. + +The browser therefore makes *cross-origin* requests to the BFF, with `credentials: "include"`. That is the interesting part of this sample: CORS with credentials, cookie behaviour across origins, and the return-URL validation you need so that login and logout can only ever return to the SPA. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`FrontendHost`** — a static-file server only, running on `https://localhost:5011`. Its `Program.cs` is eleven lines and it references no BFF packages at all. It serves the same `index.html`, `todo.js` and `session.js` used in the other JavaScript samples. +- **`BackendHost`** — this is the BFF, running on `https://localhost:5010`. Despite the name, it is not a downstream service — it hosts the `/bff/*` management endpoints, the ToDo API, and the CORS policy. The browser is what calls it. +- **`BackendApiHost`** — a separate JWT-protected ToDo API, running on `https://localhost:5020`. It is not used in the default configuration. + +## How to Run + +1. Run the `FrontendHost`, `BackendHost` and `BackendApiHost` projects. +1. Browse to `https://localhost:5011` — this is the UI, and it is the URL you start from. +1. Use the login button, which redirects to the BFF on `:5010` and back again. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create and delete ToDo items, and watch the cross-origin calls in the network tab. + +## What to Look For + +- **The SPA points at the BFF explicitly** — `wwwroot/todo.js` builds its requests against `https://localhost:5010/todos` and sets `credentials: "include"`, and `wwwroot/session.js` reads `https://localhost:5010/bff/user`. The `returnUrl` in the login and logout links is `https://localhost:5011`, not `/`. +- **CORS is locked down to the SPA** — `Program.cs` in `BackendHost` registers a default policy allowing only the origin `https://localhost:5011`, only the `x-csrf` and `content-type` headers, only the `DELETE` method, and crucially `AllowCredentials()`. The app then calls `app.UseCors()`. +- **The return-URL validator** — `FrontendHostReturnUrlValidator.cs` implements `IReturnUrlValidator` and only accepts a return URL on host `localhost`, port `5011`. Without this, the BFF's `returnUrl` parameter would be an open redirect. +- **Why the cookie still works** — the session cookie is `__Host-bff` with `SameSite = Strict`. The `__Host-` prefix requires HTTPS and no explicit `Domain`, and `Strict` cookies are still sent cross-origin here because `:5010` and `:5011` are the *same site* (same host, different ports). Change the ports to different hostnames and the sample would need `SameSite = None` plus `Secure`. +- **No BFF code in the frontend** — `FrontendHost` is deliberately package-free. The split costs you nothing in terms of what the BFF can do; it only adds the CORS and return-URL configuration on the BFF side. diff --git a/BFF/v2/TokenExchange/README.md b/BFF/v2/TokenExchange/README.md new file mode 100644 index 000000000..ba8708be6 --- /dev/null +++ b/BFF/v2/TokenExchange/README.md @@ -0,0 +1,28 @@ +# BFF Token Exchange Sample + +This sample shows [OAuth 2.0 Token Exchange](https://www.rfc-editor.org/rfc/rfc8693) in a BFF. Before calling the downstream API's `/api/impersonation` endpoint, the BFF takes the signed-in user's access token and exchanges it at the identity provider's token endpoint for a *different* one. + +The BFF authenticates as its own confidential client (`spa` / `secret`) and passes the user's token as the `subject_token`, with `subject_token_type=urn:ietf:params:oauth:token-type:access_token` and `grant_type=urn:ietf:params:oauth:grant-type:token-exchange`. The custom grant validator in the identity provider swaps `alice` for `bob` (and vice versa), so the API receives an access token for the *other* user while the browser still holds a session for the original one. This is the classic delegation / impersonation flow. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`TokenExchange.IdentityServer`** — a full Duende IdentityServer host, running on `https://localhost:5001`. This sample ships its own identity provider (rather than using the public demo server) so the token exchange grant can be demonstrated. `TokenExchangeGrantValidator.cs` implements the `IExtensionGrantValidator` that performs the user swap. +- **`TokenExchange.Bff`** — the BFF, running on `https://localhost:6001`. Hosts a static UI and hosts both the plain and the impersonating API endpoints. +- **`TokenExchange.Api`** — the downstream API, running on `https://localhost:7001`. A JWT-protected resource API whose echo endpoint shows which user the presented token actually represents. + +## How to Run + +1. Run the `TokenExchange.IdentityServer`, `TokenExchange.Bff` and `TokenExchange.Api` projects. +1. Browse to `https://localhost:6001` and log in. +1. Log in with username `alice` and password `alice` (or `bob` / `bob`). +1. Call the user-token endpoint, then the impersonation endpoint, and compare the `sub` claim that comes back each time. + +## What to Look For + +- **The exchange on the BFF side** — `ImpersonationAccessTokenRetriever.cs` derives from `DefaultAccessTokenRetriever`, gets the current user's access token via the base implementation, and then calls `HttpClient.RequestTokenExchangeTokenAsync(...)` with a `TokenExchangeTokenRequest` pointed at the identity provider's `/connect/token` endpoint. The result is returned as a `BearerTokenResult` so the proxy can use it like any other token. +- **Wiring the retriever to an endpoint** — `/api/user-token` is a plain remote endpoint requiring a user access token. `/api/impersonation` adds `.WithAccessTokenRetriever()`, which is the only difference between the two. +- **The exchange on the identity provider side** — `TokenExchangeGrantValidator.cs` implements `IExtensionGrantValidator` with `GrantType => OidcConstants.GrantTypes.TokenExchange`. It validates the incoming `subject_token` with `ITokenValidator.ValidateAccessTokenAsync`, requires `subject_token_type` to be an access token, and returns a grant result whose subject is the *other* user, with `authenticationMethod: "swap-alice-and-bob"`. +- **The token exchange grant is not built in** — registering a custom extension grant validator via `isBuilder.AddExtensionGrantValidator()` is all that is needed; Duende IdentityServer does not ship a token exchange grant out of the box. +- **Test users** — `TokenExchange.IdentityServer/Pages/TestUsers.cs` holds the in-memory users `alice` / `alice` and `bob` / `bob`. The `spa` / `secret` client is allowed the `authorization_code`, `client_credentials` and `token-exchange` grants. diff --git a/BFF/v3/Angular/README.md b/BFF/v3/Angular/README.md new file mode 100644 index 000000000..399a30375 --- /dev/null +++ b/BFF/v3/Angular/README.md @@ -0,0 +1,32 @@ +# BFF Angular Sample + +This sample shows an [Angular](https://angular.dev/) single-page application hosted by a BFF. The BFF is the only thing the browser talks to: it serves the Angular app, runs the OpenID Connect flow, and holds the tokens, so nothing sensitive ever lands in JavaScript. + +Compared to [BFF v2](../v2/Angular), the Angular app itself has moved to server-side rendering, and the client code is written with standalone components and signals instead of an `NgModule` and RxJS streams. None of that changes the BFF side, which makes this a good sample for seeing what SSR does and does not mean for the BFF pattern. + +There is also a separate JWT-protected ToDo API in the solution. It is not used in the default configuration — the BFF hosts the ToDo endpoints itself — but the sample ships it so you can switch to the reverse-proxy style with a one-line change. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`Angular.Bff`** — the BFF, running on `https://localhost:6001`. It serves the Angular app from the build output, maps the `/bff/*` management endpoints, and hosts the ToDo API in the same process. +- **`angular.ssr`** — the Angular SSR app, an `.esproj` project so it can take part in the .NET solution and the VS launch profiles. Its dev server runs on `https://localhost:4200`. +- **`Angular.Api`** — a separate JWT-protected ToDo API on `https://localhost:7001`. Not called by default. + +## How to Run + +1. Run the `Angular.Bff` project. This starts the Angular dev server through the SPA proxy and the BFF together. +1. Browse to `https://localhost:6001` and use the login button. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create and delete ToDo items, open **Show User Session** to see the claims the BFF returns, and then log out. + +## What to Look For + +- **SSR, and what it does to the BFF pattern** — the app is built with `"outputMode": "server"` and an Express entry point in `src/server.ts`. The ToDo page is still `RenderMode.Client` and the user-session page is `RenderMode.Server` (see `app.routes.server.ts`), because the ToDo list is fetched from the BFF and has to happen in the browser after the session cookie is sent. +- **The authentication service is signal-based** — `src/app/Services/auth.service.ts` wraps the single `bff/user` request in `toSignal` and derives `isAuthenticated`, `username` and `logoutUrl` with `computed()`. The request itself is still cached with `shareReplay(1)`, so it happens once. The v2 version of this file exposes the same information as RxJS observables. +- **The CSRF header is a functional interceptor** — `src/app/Interceptors/csrf-header.interceptor.ts` is an `HttpInterceptorFn` registered with `provideHttpClient(withFetch(), withInterceptors([csrfHeaderInterceptor]))`. It sets `X-CSRF: 1` and `withCredentials: true` on every request. +- **The SPA proxy** — `Angular.Bff.csproj` sets `SpaRoot`, `SpaProxyLaunchCommand` (`npm start`) and `SpaProxyServerUrl` (`https://localhost:4200`). During development the browser talks to the Angular dev server, and the dev server forwards `/bff`, `/signin-oidc`, `/signout-callback-oidc` and `/todos` on to the BFF (see `proxy.conf.js`), which keeps everything same-origin from the browser's point of view. +- **The local API** — `Program.cs` maps `/todos` with `MapGroup("/todos").ToDoGroup().RequireAuthorization().AsBffApiEndpoint()`. The `AsBffApiEndpoint()` part is what requires both a valid session and a valid CSRF header. +- **Switching to the remote API** — `Program.cs` also contains a commented-out `MapRemoteBffApiEndpoint("/todos", "https://localhost:7001/todos").RequireAccessToken(TokenType.User)`. Comment the local group out and this one in, and the same Angular code now talks to `Angular.Api` through the YARP proxy with the user's access token attached. +- **Client-side routing** — `app.MapFallbackToFile("/index.html")` makes the deep links work when the BFF serves the built app rather than the dev server. diff --git a/BFF/v3/BlazorAutoRendering/README.md b/BFF/v3/BlazorAutoRendering/README.md new file mode 100644 index 000000000..c6ad3b09d --- /dev/null +++ b/BFF/v3/BlazorAutoRendering/README.md @@ -0,0 +1,33 @@ +# BFF Blazor Auto-Rendering Sample + +This sample shows the Blazor "auto rendering" model behind a [Backend-for-Frontend (BFF)](https://docs.duendesoftware.com/identityserver/bff), together with a YARP-proxied remote API. + +A single ASP.NET Core host serves the Blazor components with **both** the `InteractiveServer` and `InteractiveWebAssembly` render modes registered, and lets the framework pick per route. The interesting part is `IWeatherClient`: the same abstraction is resolved to a server-side implementation when the component renders on the server, and to an HTTP client that goes through the BFF when it renders in the browser. You get the security of the BFF without giving up interactivity, and without writing the component twice. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`BlazorAutoRendering`** — the BFF host, running on `https://localhost:7035`. It serves the Blazor app, the `/bff/*` management endpoints, a local API endpoint and a proxied remote API endpoint. +- **`BlazorAutoRendering.Client`** — the WebAssembly assembly. It has no launch profile of its own; the host serves it. +- **`BlazorAutoRendering.Api`** — a separate JWT-protected demo API, running on `https://localhost:7001`. This is the one the BFF proxies to. + +## How to Run + +1. Run the `BlazorAutoRendering` and `BlazorAutoRendering.Api` projects. +1. Browse to `https://localhost:7035` and use the login link. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Visit the Weather and Greet pages, then toggle interactive rendering to compare the two paths. + +The sample does not run its own identity server, so you need internet access to reach the demo server. + +## What to Look For + +- **Render modes** — `Program.cs` registers `AddInteractiveServerRenderMode()` and `AddInteractiveWebAssemblyRenderMode()` and adds the client assembly with `AddAdditionalAssemblies(...)`. `Pages/Home.razor` is `InteractiveServer`, `Pages/Counter.razor` is `InteractiveWebAssembly`, and `Pages/Weather.razor` and `Pages/Greet.razor` use `@rendermode InteractiveAuto` and let the framework choose. +- **The swappable client** — `AddSingleton()` on the host resolves the interface to an in-process implementation. The client project resolves the *same* interface to a `WeatherClient` that calls the BFF. Because the component only knows `IWeatherClient`, the same code works in both places. +- **The remote API path** — `MapRemoteBffApiEndpoint("/remote-apis/greetings", "https://localhost:7001").RequireAccessToken(TokenType.User)` proxies the greetings API and attaches the user's access token. The client is told where that prefix is with `AddBffBlazorClient(opt => opt.RemoteApiPath = "remote-apis/greetings/")`, so `Pages/Greet.razor` never hard-codes it. +- **Seeing which path ran** — `Pages/Greet.razor` renders `response.RequestMessage?.RequestUri`, so the displayed URL tells you whether the call was served locally or went through the proxy. +- **The local API endpoint** — `WeatherEndpointExtensions.MapWeatherEndpoints()` maps `GET /WeatherForecast` with `.RequireAuthorization().AsBffApiEndpoint()`, which adds the CSRF header check on top of authorization. +- **Two `HttpClient` flavours on the client** — `AddLocalApiHttpClient()` talks to the BFF's own endpoints, while `AddRemoteApiHttpClient("greet")` targets the proxied remote API path. +- **`SameSite=Lax` is deliberate here** — `Program.cs` carries a comment explaining that because the identity provider is on `duendesoftware.com` while the BFF is on `localhost`, a `Strict` cookie would not be sent after the redirect back from login. Use `Strict` when your identity provider is on the same site as the BFF. +- **Antiforgery** — `UseBff()` performs the BFF CSRF header check, and `UseAntiforgery()` adds the ASP.NET Core token store that Blazor forms use. diff --git a/BFF/v3/BlazorWasm/README.md b/BFF/v3/BlazorWasm/README.md new file mode 100644 index 000000000..18cde03a6 --- /dev/null +++ b/BFF/v3/BlazorWasm/README.md @@ -0,0 +1,35 @@ +# BFF Blazor WebAssembly Sample + +This sample shows a standalone Blazor WebAssembly application protected by a [Backend-for-Frontend (BFF)](https://docs.duendesoftware.com/identityserver/bff), so that no tokens ever reach the browser. + +An ordinary ASP.NET Core application authenticates the user with OIDC and then serves the Blazor WebAssembly app with `UseBlazorFrameworkFiles()` and `MapFallbackToFile("index.html")`. The WASM app talks to the BFF's own endpoints and never sees an access token. + +Compared with the [BFF v2 version](../v2/BlazorWasm) of this sample, the hand-written authentication state provider and CSRF handler are replaced by the official `Duende.BFF.Blazor.Client` package, and the BFF gains server-side sessions. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`BlazorWasm.Server`** — the BFF host, running on `https://localhost:7189`. It handles the OIDC login, maps the `/bff/*` management endpoints, and serves the WebAssembly app itself. +- **`BlazorWasm.Client`** — the Blazor WebAssembly application. +- **`BlazorWasm.Shared`** — a small class library holding the `WeatherForecast` model that both projects use. + +Because the host serves the WebAssembly app, **the only URL you need is `https://localhost:7189`**. + +## How to Run + +1. Run the `BlazorWasm.Server` project. +1. Browse to `https://localhost:7189` and use the login link in the navigation. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Visit the Fetch Data page, which calls a protected BFF endpoint, then sign out. + +This sample does not run its own identity server, so you need internet access to reach the demo server. + +## What to Look For + +- **The Blazor server-side helpers** — `Program.cs` in the host calls `AddBff().AddServerSideSessions().AddBlazorServer()`. `AddServerSideSessions()` keeps session state on the server rather than in the cookie, and `AddBlazorServer()` wires up the pieces Blazor needs. +- **The whole client setup, in four lines** — `Program.cs` in `BlazorWasm.Client` is now just `AddBffBlazorClient().AddCascadingAuthenticationState()` plus `AddLocalApiHttpClient("backend")`. The `AddBffBlazorClient()` call provides the authentication state provider that polls the `/bff/user` endpoint, and `AddLocalApiHttpClient` provides an `HttpClient` that already attaches the required CSRF header and talks to the BFF's local endpoints. +- **`CascadingAuthenticationState`** — this is what makes `AuthorizeView` and `AuthorizeRouteView` work in Razor components. +- **Antiforgery** — the host now calls `app.UseAntiforgery()` after `UseAuthorization()`, in addition to the CSRF header check that `UseBff()` performs. +- **The session cookie** — still `__Host-blazor` with `SameSite=Strict`. The `__Host-` prefix requires HTTPS and forbids setting an explicit `Domain`. +- **Protecting an endpoint** — `MapControllers().RequireAuthorization().AsBffApiEndpoint()` combines a valid session with a valid CSRF header. diff --git a/BFF/v3/DPoP/README.md b/BFF/v3/DPoP/README.md new file mode 100644 index 000000000..3ee84a550 --- /dev/null +++ b/BFF/v3/DPoP/README.md @@ -0,0 +1,30 @@ +# BFF DPoP Sample + +This sample shows a BFF that requests *DPoP-bound* access tokens (Demonstration of Proof of Possession, [RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)) and calls a downstream API with them. + +Instead of a bearer token that anyone can replay, DPoP binds the access token to a key that only the BFF holds. For every API call the BFF generates a short-lived *DPoP proof* JWT that covers the HTTP method and target URI, and the API checks that proof against the request it actually received. A stolen access token on its own is therefore useless without the corresponding private key. + +The sample shows the four access-token flavours side by side — anonymous, user, client credentials, and user-or-client — both through the BFF's own endpoint helpers and through raw YARP routes. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`DPoP.Api`** — the downstream API, running on `https://localhost:6001`. It validates the access token *and* the DPoP proof on every request, and echoes back the path, message and headers. It ships a self-contained DPoP validation implementation under `DPoP.Api/DPoP/`, so you can see exactly what a resource server has to do. +- **`DPoP.Bff`** — the BFF, running on `https://localhost:5002`. It hosts a small static UI and proxies the API calls on the browser's behalf. + +## How to Run + +1. Run the `DPoP.Bff` and `DPoP.Api` projects. +1. Browse to `https://localhost:5002` and log in. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Try the different endpoints and watch the requests and responses in the browser's network tab. + +## What to Look For + +- **Enabling DPoP in the BFF** — `Program.cs` in `DPoP.Bff` creates a 2048-bit RSA key, converts it to a JWK, sets `Alg` to `PS256`, and assigns the serialized JWK to `options.DPoPJsonWebKey` inside `AddBff(options => { ... })`. Setting this key is all it takes to switch the BFF over to DPoP. +- **Token types** — the BFF exposes `/api/anonymous`, `/api/user-token`, `/api/client-token` and `/api/user-or-client-token`, each guarded by the matching token requirement, plus `/api/optional-user-token` for the "send a token if we have one" case. +- **YARP routes** — `/yarp/anonymous`, `/yarp/user-token`, `/yarp/client-token` and `/yarp/user-or-client-token` show the same calls driven purely by the YARP configuration in `YarpConfigurator.cs`. +- **Proof validation on the API** — `DPoP.Api/DPoP/DPoPProofValidator.cs` validates the proof's `htm`, `htu` and `ath` claims against the incoming request and access token, and enforces a one-second proof validity window. +- **DPoP nonces** — the API supports server-issued nonces. `DPoPOptions.ValidateNonce` defaults to `false`; when enabled the API responds with a `DPoP-Nonce` header and rejects the request until the client retries with that nonce. +- **No forwarded headers on the API** — the API deliberately does *not* use forwarded-headers middleware, because it would change the `htu` that the DPoP proof is validated against. diff --git a/BFF/v3/JsBffSample/README.md b/BFF/v3/JsBffSample/README.md new file mode 100644 index 000000000..ebdff7ffe --- /dev/null +++ b/BFF/v3/JsBffSample/README.md @@ -0,0 +1,27 @@ +# BFF JavaScript Sample + +This sample shows the most basic shape of a BFF: an ASP.NET Core application that serves a plain JavaScript UI and handles the authentication and token handling for the browser, so no tokens ever reach JavaScript. + +The UI is served from `wwwroot` and talks to same-origin endpoints only. Log in and out happen through the BFF's management endpoints, and the session is read back from `/bff/user`. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`FrontendHost`** — the BFF, running on `https://localhost:5010`. It serves the static UI (`index.html`, `todo.js`, `session.js`), maps the `/bff/*` management endpoints, and hosts the ToDo API in the same process. +- **`BackendApiHost`** — a separate JWT-protected ToDo API, running on `https://localhost:5020`. It is not used in the default configuration, but it is there so you can compare the two styles. + +## How to Run + +1. Run the `FrontendHost` and `BackendApiHost` projects. +1. Browse to `https://localhost:5010` and use the login button. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create, update and delete ToDo items, then reload the page to see the session restored from the cookie. + +## What to Look For + +- **Login and logout** — the UI simply links to `/bff/login?returnUrl=/` and `/bff/logout?returnUrl=/`. All of the OAuth/OIDC flow happens in the BFF. +- **Reading the session** — `wwwroot/session.js` fetches `/bff/user` and renders the user's name and claims. JavaScript never sees an access token; it only learns *who* is logged in. +- **The CSRF header** — every call from the UI sends an `x-csrf: 1` header. This is what lets the BFF reject cross-site request forgery on its API endpoints. +- **The local API** — `FrontendHost/ToDoController.cs` is an ordinary MVC controller marked with `.RequireAuthorization().AsBffApiEndpoint()`. That marks it as a BFF endpoint, which requires both a valid session and a valid CSRF header. +- **Switching to a remote API** — `Program.cs` contains a commented-out `MapRemoteBffApiEndpoint("/todos", "https://localhost:5020/todos")` showing the alternative: keep the same UI and let the BFF proxy the calls to a real downstream API instead of handling them locally. diff --git a/BFF/v3/JsBffYarpSample/README.md b/BFF/v3/JsBffYarpSample/README.md new file mode 100644 index 000000000..fb79200aa --- /dev/null +++ b/BFF/v3/JsBffYarpSample/README.md @@ -0,0 +1,28 @@ +# BFF JavaScript Sample with YARP + +This sample is the reverse-proxy flavour of the [JavaScript BFF sample](../v4/JsBffSample). The UI is identical, but instead of handling the ToDo calls itself, the BFF runs a [YARP](https://github.com/dotnet/yarp) reverse proxy that forwards `/todos/**` to a real downstream API, attaching the user's access token on the way out. + +This is the pattern to reach for when the API already exists and you do not want to hand-write a controller per endpoint — the BFF becomes a secure gateway instead of a second implementation of your API. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`FrontendHost`** — the BFF, running on `https://localhost:5010`. It serves the static UI (`index.html`, `todo.js`, `session.js`), maps the `/bff/*` management endpoints, and hosts the reverse proxy. +- **`BackendApiHost`** — the downstream JWT-protected ToDo API, running on `https://localhost:5020`. Unlike in the non-YARP sample, this one really is called. + +## How to Run + +1. Run the `FrontendHost` and `BackendApiHost` projects. +1. Browse to `https://localhost:5010` and use the login button. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create and delete ToDo items, and watch the proxied calls in the browser's network tab. + +## What to Look For + +- **The proxy pipeline** — `Program.cs` calls `AddBff()`, then `AddReverseProxy().AddTransforms()`, then the sample's own `YarpConfigurator.Configure()`. The provider is the piece that attaches the user's access token to each outgoing request. +- **Routes in code** — `YarpConfigurator.cs` builds the route and cluster configuration in C# with `LoadFromMemory`. The route matches `/todos/{**catch-all}`, points at the `cluster1` cluster, and declares the token requirement with `.WithAccessToken(TokenType.User)`. The cluster's single destination is `https://localhost:5020`. +- **Loading config from memory** — `InMemoryConfigProvider.cs` implements `IProxyConfigProvider` and `IProxyConfig` so the routes can be handed to YARP as an in-memory object rather than a `appsettings.json` section. That is what makes the route able to carry BFF-specific configuration such as the access token requirement. +- **Mapping the proxy** — `app.MapBffReverseProxy()` is the shorthand for `MapReverseProxy().AsBffApiEndpoint()`. The `AsBffApiEndpoint()` part is what enforces the session check and the CSRF header. +- **Going back to a local API** — the sample keeps `ToDoController.cs` and the matching `MapControllers().AsBffApiEndpoint()` call, commented out, so you can switch between the two styles and compare. +- **The CSRF header** — the UI sends `x-csrf: 1` on its requests, which the BFF requires before it will forward anything. diff --git a/BFF/v3/OpenApi/README.md b/BFF/v3/OpenApi/README.md new file mode 100644 index 000000000..ee07b1b05 --- /dev/null +++ b/BFF/v3/OpenApi/README.md @@ -0,0 +1,34 @@ +# BFF OpenAPI Sample + +This sample shows a BFF that proxies two downstream APIs *and* rewrites their OpenAPI documents on the fly, so the front end sees a single, browser-facing API surface. + +Normally a downstream API's OpenAPI document describes server-to-server calls: it names the API's own `servers` entry and its JWT `securitySchemes`. Neither is useful to a browser talking to a BFF with a cookie. This sample intercepts each `/openapi/*.json` response as it is proxied through YARP and rewrites it — repointing `servers` at the BFF, dropping the security schemes, and prefixing every path with the BFF's local path. It then merges both APIs into one combined document, so Swagger UI and any client generator see the whole BFF surface at once. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`OpenApi.Api1`** — a minimal API exposing `GET /weatherforecastFahrenheit`, running on `https://localhost:7289`. It commits its generated OpenAPI document (`OpenApi.Api1.json`) and is protected by JWT bearer authentication. +- **`OpenApi.Api2`** — the same, exposing `GET /WeatherforecastCelcius`, running on `https://localhost:7297`. +- **`OpenApi.Bff`** — the BFF, running on `https://localhost:7082`. Hosts a static UI and Swagger UI, proxies both APIs, and rewrites and combines their OpenAPI documents. Its OIDC, cookie and API settings are bound from a hand-written `Configuration.cs`. +- **`OpenApi.BffOpenApiDocumentParser`** — a small console app that applies the same document transformation offline, so a pre-transformed document can be produced at build time for tooling or code generation. `OpenApi.Bff` references it as a project so both share one implementation. +- **`OpenApi.DevServer.AppHost`** — the .NET Aspire host that starts everything and wires up service discovery. Its dashboard runs on `https://localhost:17275`. +- **`OpenApi.DevServer.ServiceDefaults`** — the shared Aspire service defaults (OpenTelemetry, health checks, JWT bearer validation) referenced by the other projects. + +## How to Run + +1. Run the `OpenApi.DevServer.AppHost` project (or use the Aspire CLI with `aspire run`), which starts the two APIs and the BFF for you. +1. Open the Aspire dashboard at `https://localhost:17275` and click through to the BFF. +1. Log in with username `bob` and password `bob` (or `alice` / `alice`) at the [Duende demo identity server](https://demo.duendesoftware.com). +1. Browse to `https://localhost:7082` for the UI, or `https://localhost:7082/swagger` for Swagger UI. + +You can also run the projects individually without Aspire, in which case the fixed ports in each `launchSettings.json` apply. + +## What to Look For + +- **Rewriting the documents** — `OpenApiResponseTransform.cs` is registered as a YARP response transform. When a proxied response is an OpenAPI document it suppresses the original body and pipes it through `OpenApiTransformer.TransformOpenApiDocumentForBff`, which clears `Servers` and `Components.SecuritySchemes` and prefixes every path with the BFF's local path. +- **Combining the documents** — `OpenApiDocumentCombiner.cs` fetches each source document and merges the `Paths` and `Components` into one. Security schemes are intentionally *not* copied, because the browser never presents a bearer token to the BFF. Served from `/swagger/combined/v1.json`. +- **The BFF endpoint mapping** — `app.MapRemoteBffApiEndpoint("/api1", ...).WithOptionalUserAccessToken()` and the same for `/api2`. Both APIs are optional here so the document endpoints stay reachable. +- **Swagger UI as the browser** — `UseSwaggerUI(...)` registers all three documents, injects an `X-CSRF` request interceptor, and adds `bff-auth-button.js` so you can drive the "try it" buttons from the BFF session. +- **Service discovery** — `AddHttpForwarderWithServiceDiscovery()` lets YARP resolve the logical service names, so the BFF does not need to know the APIs' real addresses. +- **BFF configuration** — in this version the `BFF` settings are bound to a `Configuration` POCO in `OpenApi.Bff/Configuration.cs`, and the cookie and OIDC handlers are registered by hand around `AddBff().AddRemoteApis()`. diff --git a/BFF/v3/Quickstarts/BlazorBffApp/README.md b/BFF/v3/Quickstarts/BlazorBffApp/README.md new file mode 100644 index 000000000..a7fd9a3a3 --- /dev/null +++ b/BFF/v3/Quickstarts/BlazorBffApp/README.md @@ -0,0 +1,32 @@ +# Blazor BFF Quickstart + +This is the smallest complete BFF + Blazor app in the repository — a good starting point to copy from. + +There is one protected endpoint and nothing else: no YARP, no remote APIs, no separate API host. If you want to understand the moving parts before adding complexity, start here and then look at the [Blazor auto-rendering sample](../BlazorAutoRendering) or the [Blazor WebAssembly sample](../BlazorWasm). + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`BlazorBffApp`** — the BFF host, running on `https://localhost:7007`. It serves the Blazor app, the `/bff/*` management endpoints and the local API endpoint. +- **`BlazorBffApp.Client`** — the WebAssembly assembly. It has no launch profile of its own; the host serves it. + +## How to Run + +1. Run the `BlazorBffApp` project. +1. Browse to `https://localhost:7007` and use the login link. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Visit the Weather page, which calls the protected endpoint, then sign out. + +The sample does not run its own identity server, so you need internet access to reach the demo server. + +## What to Look For + +- **The whole BFF setup** — `Program.cs` is a single file you can read top to bottom: `AddBff().AddServerSideSessions().AddBlazorServer()`, an authentication block setting up the cookie and OIDC handlers, then `UseBff()` between `UseAuthentication()` and `UseAuthorization()`. +- **`AddServerSideSessions()`** — keeps session state on the server rather than inside the encrypted cookie, which is what lets the BFF hold access tokens across requests. +- **`AddBlazorServer()`** — the BFF-specific registration that lets Blazor components participate in the BFF session. +- **The cookie configuration** — `__Host-blazor` with `SameSite=Lax`. The sample includes a comment explaining that `Lax` is needed here only because the identity provider is on `duendesoftware.com` while the BFF is on `localhost`; use `Strict` when both are on the same site. The `__Host-` prefix requires HTTPS and forbids an explicit cookie `Domain`. +- **The local API endpoint** — `app.MapGet("/WeatherForecast", ...)` returns the forecasts, and the client calls it with `AddLocalApiHttpClient()`, which attaches the CSRF header the BFF requires. +- **The swapped client** — the host registers `IWeatherClient` to an inline `ServerWeatherClient` that returns random data without any HTTP call, while the WebAssembly project registers it to a `WeatherHttpClient` that goes through the BFF. Same interface, two implementations, and the Blazor component does not care which one it got. +- **The client setup** — `BlazorBffApp.Client/Program.cs` is five lines: `AddBffBlazorClient()` for the authentication state provider that polls `/bff/user`, `AddLocalApiHttpClient()`, the `IWeatherClient` registration and `AddCascadingAuthenticationState()` so `AuthorizeView` works in components. +- **Antiforgery** — `UseBff()` performs the BFF CSRF header check, and `UseAntiforgery()` adds the ASP.NET Core token store that Blazor forms use. diff --git a/BFF/v3/React/README.md b/BFF/v3/React/README.md new file mode 100644 index 000000000..9698a290c --- /dev/null +++ b/BFF/v3/React/README.md @@ -0,0 +1,43 @@ +# BFF React Sample + +This sample shows a [React](https://react.dev/) single-page application hosted by a BFF project. The SPA is developed with [Vite](https://vite.dev/), but in production it is served as static files from the BFF itself, so everything ends up same-origin and no tokens ever reach JavaScript. + +The BFF runs the authorization code flow against the [Duende demo identity server](https://demo.duendesoftware.com), keeps the tokens server-side, and exposes the ToDo API to the browser through its own BFF endpoints. A second, standalone JWT-protected API is included so you can compare hosting the API in the BFF process against proxying to a real downstream service. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`React.Bff`** — the BFF, running on `https://localhost:6001`. It serves the SPA, maps the `/bff/*` management endpoints, and hosts the ToDo API in the same process. +- **`React.Api`** — a separate JWT-protected ToDo API, running on `https://localhost:7001`. It is not used in the default configuration, but it is there so you can compare the two styles. +- **`react.client`** — the Vite dev server, running on `https://localhost:5173`. It is a build-time dependency of `React.Bff` and is only used during development. + +## How to Run + +1. Run the `React.Bff` and `React.Api` projects. +1. Browse to `https://localhost:6001` and use the login button. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create, update and delete ToDo items, then reload the page to see the session restored from the cookie. + +Running `React.Bff` starts the Vite dev server through the ASP.NET Core SPA proxy, so you get hot module replacement while developing. The Vite server proxies `/bff/*`, `/todos`, `/signin-oidc` and `/signout-callback-oidc` back to the BFF, which is why the app can use same-origin relative URLs in development and in production alike. + +## What to Look For + +- **Login and logout** — the UI links to `/bff/login` and `/bff/logout`. All of the OAuth/OIDC flow happens in the BFF. +- **Reading the session** — `react.client/src/components/UserSession.jsx` fetches `bff/user` and renders the user's claims. JavaScript never sees an access token; it only learns *who* is logged in. +- **The CSRF header** — every call from the UI sends an `X-CSRF: 1` header. This is what lets the BFF reject cross-site request forgery on its API endpoints. +- **The local API** — `React.Bff/ToDoEndpointGroup.cs` holds a small in-memory ToDo store, and `Program.cs` maps it with `.RequireAuthorization().AsBffApiEndpoint()`. That marks it as a BFF endpoint, which requires both a valid session and a valid CSRF header. +- **Switching to a remote API** — `Program.cs` contains a commented-out `MapRemoteBffApiEndpoint("/todos", "https://localhost:7001/todos").RequireAccessToken(Duende.Bff.TokenType.User)` showing the alternative: keep the same UI and let the BFF proxy the calls to a real downstream API instead of handling them locally. The UI does not change at all. +- **Validating tokens in the API** — `React.Api` is a plain JWT bearer resource server. It checks the `api` audience and guards the ToDo endpoints with two policies: `ApiCaller` (the token carries the `api` scope) and `InteractiveUser` (the token carries a `sub`, so a real user is behind it). +- **Client-side routing** — `app.MapFallbackToFile("/index.html")` makes the BFF return the SPA for unknown paths, so a deep link such as `/users` still boots the React app. +- **Explicit authentication schemes** — in BFF 3.x the cookie and OIDC handlers are registered by hand and the default schemes are named `cookie` / `oidc`: + ```csharp + builder.Services.AddAuthentication(options => + { + options.DefaultScheme = "cookie"; + options.DefaultChallengeScheme = "oidc"; + options.DefaultSignOutScheme = "oidc"; + }).AddCookie("cookie", options => { /* ... */ }) + .AddOpenIdConnect("oidc", options => { /* ... */ }); + ``` + BFF 4.x folds this into the BFF builder via `.ConfigureOpenIdConnect(...)` and `.ConfigureCookies(...)`, and adds the data-protection setup. See the [BFF v4 sample](../v4/React) for that style. diff --git a/BFF/v3/SplitHosts/README.md b/BFF/v3/SplitHosts/README.md new file mode 100644 index 000000000..175b16dba --- /dev/null +++ b/BFF/v3/SplitHosts/README.md @@ -0,0 +1,37 @@ +# BFF Split Hosts Sample + +This sample shows what changes when the SPA and the BFF are **not** on the same origin. Instead of one application serving both the UI and the BFF endpoints, the UI is served by a bare static-file host on `https://localhost:5011` and the BFF runs separately on `https://localhost:5010`. + +The browser therefore makes *cross-origin* requests to the BFF, with `credentials: "include"`. That is the interesting part of this sample: CORS with credentials, cookie behaviour across origins, and the return-URL validation you need so that login and logout can only ever return to the SPA. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`FrontendHost`** — a static-file server only, running on `https://localhost:5011`. Its `Program.cs` is eleven lines and it references no BFF packages at all. It serves the same `index.html`, `todo.js` and `session.js` used in the other JavaScript samples. +- **`BackendHost`** — this is the BFF, running on `https://localhost:5010`. Despite the name, it is not a downstream service — it hosts the `/bff/*` management endpoints, the ToDo API, and the CORS policy. The browser is what calls it. +- **`BackendApiHost`** — a separate JWT-protected ToDo API, running on `https://localhost:5020`. It is not used in the default configuration. + +## How to Run + +1. Run the `FrontendHost`, `BackendHost` and `BackendApiHost` projects. +1. Browse to `https://localhost:5011` — this is the UI, and it is the URL you start from. +1. Use the login button, which redirects to the BFF on `:5010` and back again. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create and delete ToDo items, and watch the cross-origin calls in the network tab. + +## What to Look For + +- **The SPA points at the BFF explicitly** — `wwwroot/todo.js` builds its requests against `https://localhost:5010/todos` and sets `credentials: "include"`, and `wwwroot/session.js` reads `https://localhost:5010/bff/user`. The `returnUrl` in the login and logout links is `https://localhost:5011`, not `/`. +- **CORS is locked down to the SPA** — `Program.cs` in `BackendHost` registers a default policy allowing only the origin `https://localhost:5011`, only the `x-csrf` and `content-type` headers, only the `DELETE` method, and crucially `AllowCredentials()`. The app then calls `app.UseCors()`. +- **The return-URL validator** — `FrontendHostReturnUrlValidator.cs` implements `IReturnUrlValidator` and only accepts a return URL on host `localhost`, port `5011`. Without this, the BFF's `returnUrl` parameter would be an open redirect. +- **Why the cookie still works** — the session cookie is `__Host-bff` with `SameSite = Strict`. The `__Host-` prefix requires HTTPS and no explicit `Domain`, and `Strict` cookies are still sent cross-origin here because `:5010` and `:5011` are the *same site* (same host, different ports). Change the ports to different hostnames and the sample would need `SameSite = None` plus `Secure`. +- **No BFF code in the frontend** — `FrontendHost` is deliberately package-free. The split costs you nothing in terms of what the BFF can do; it only adds the CORS and return-URL configuration on the BFF side. + +## Versions + +| BFF | .NET | `Duende.BFF` | +| --- | --- | --- | +| [BFF v2](../v2/SplitHosts) | net8.0 | 2.3.0 | +| [BFF v3](../v3/SplitHosts) | net8.0 | 3.1.0 | +| [BFF v4](../v4/SplitHosts) | net10.0 | 4.2.0 | diff --git a/BFF/v3/TokenExchange/README.md b/BFF/v3/TokenExchange/README.md new file mode 100644 index 000000000..4f33d2856 --- /dev/null +++ b/BFF/v3/TokenExchange/README.md @@ -0,0 +1,36 @@ +# BFF Token Exchange Sample + +This sample shows [OAuth 2.0 Token Exchange](https://www.rfc-editor.org/rfc/rfc8693) in a BFF. Before calling the downstream API's `/api/impersonation` endpoint, the BFF takes the signed-in user's access token and exchanges it at the identity provider's token endpoint for a *different* one. + +The BFF authenticates as its own confidential client (`spa` / `secret`) and passes the user's token as the `subject_token`, with `subject_token_type=urn:ietf:params:oauth:token-type:access_token` and `grant_type=urn:ietf:params:oauth:grant-type:token-exchange`. The custom grant validator in the identity provider swaps `alice` for `bob` (and vice versa), so the API receives an access token for the *other* user while the browser still holds a session for the original one. This is the classic delegation / impersonation flow. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`TokenExchange.IdentityServer`** — a full Duende IdentityServer host, running on `https://localhost:5001`. This sample ships its own identity provider (rather than using the public demo server) so the token exchange grant can be demonstrated. `TokenExchangeGrantValidator.cs` implements the `IExtensionGrantValidator` that performs the user swap. +- **`TokenExchange.Bff`** — the BFF, running on `https://localhost:6001`. Hosts a static UI and hosts both the plain and the impersonating API endpoints. +- **`TokenExchange.Api`** — the downstream API, running on `https://localhost:7001`. A JWT-protected resource API whose echo endpoint shows which user the presented token actually represents. + +## How to Run + +1. Run the `TokenExchange.IdentityServer`, `TokenExchange.Bff` and `TokenExchange.Api` projects. +1. Browse to `https://localhost:6001` and log in. +1. Log in with username `alice` and password `alice` (or `bob` / `bob`). +1. Call the user-token endpoint, then the impersonation endpoint, and compare the `sub` claim that comes back each time. + +## What to Look For + +- **The exchange on the BFF side** — `ImpersonationAccessTokenRetriever.cs` derives from `DefaultAccessTokenRetriever`, gets the current user's access token via the base implementation, and then calls `HttpClient.RequestTokenExchangeTokenAsync(...)` with a `TokenExchangeTokenRequest` pointed at the identity provider's `/connect/token` endpoint. The result is returned as a `BearerTokenResult` so the proxy can use it like any other token. +- **Wiring the retriever to an endpoint** — `/api/user-token` is a plain remote endpoint requiring a user access token. `/api/impersonation` adds `.WithAccessTokenRetriever()`, which is the only difference between the two. +- **The exchange on the identity provider side** — `TokenExchangeGrantValidator.cs` implements `IExtensionGrantValidator` with `GrantType => OidcConstants.GrantTypes.TokenExchange`. It validates the incoming `subject_token` with `ITokenValidator.ValidateAccessTokenAsync`, requires `subject_token_type` to be an access token, and returns a grant result whose subject is the *other* user, with `authenticationMethod: "swap-alice-and-bob"`. +- **The token exchange grant is not built in** — registering a custom extension grant validator via `isBuilder.AddExtensionGrantValidator()` is all that is needed; Duende IdentityServer does not ship a token exchange grant out of the box. +- **Test users** — `TokenExchange.IdentityServer/Pages/TestUsers.cs` holds the in-memory users `alice` / `alice` and `bob` / `bob`. The `spa` / `secret` client is allowed the `authorization_code`, `client_credentials` and `token-exchange` grants. + +## Versions + +| BFF | .NET | `Duende.Bff.Yarp` | `Duende.IdentityServer` | +| --- | --- | --- | --- | +| [BFF v2](../v2/TokenExchange) | net8.0 | 2.3.0 | 7.4.13 | +| [BFF v3](../v3/TokenExchange) | net8.0 | 3.1.0 | 7.4.13 | +| [BFF v4](../v4/TokenExchange) | net10.0 | 4.2.0 | 8.0.9 | diff --git a/BFF/v3/Vue/README.md b/BFF/v3/Vue/README.md new file mode 100644 index 000000000..bc7422ed2 --- /dev/null +++ b/BFF/v3/Vue/README.md @@ -0,0 +1,43 @@ +# BFF Vue Sample + +This sample shows a [Vue](https://vuejs.org/) single-page application hosted by a BFF project. The SPA is developed with [Vite](https://vite.dev/), but in production it is served as static files from the BFF itself, so everything ends up same-origin and no tokens ever reach JavaScript. + +The BFF runs the authorization code flow against the [Duende demo identity server](https://demo.duendesoftware.com), keeps the tokens server-side, and exposes the ToDo API to the browser through its own BFF endpoints. A second, standalone JWT-protected API is included so you can compare hosting the API in the BFF process against proxying to a real downstream service. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`Vue.Bff`** — the BFF, running on `https://localhost:6001`. It serves the SPA, maps the `/bff/*` management endpoints, and hosts the ToDo API in the same process. +- **`Vue.Api`** — a separate JWT-protected ToDo API, running on `https://localhost:7001`. It is not used in the default configuration, but it is there so you can compare the two styles. +- **`vue.client`** — the Vite dev server, running on `https://localhost:4200`. It is a build-time dependency of `Vue.Bff` and is only used during development. + +## How to Run + +1. Run the `Vue.Bff` and `Vue.Api` projects. +1. Browse to `https://localhost:6001` and use the login button. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create, update and delete ToDo items, then reload the page to see the session restored from the cookie. + +Running `Vue.Bff` starts the Vite dev server through the ASP.NET Core SPA proxy, so you get hot module replacement while developing. The Vite server proxies `/bff/*`, `/todos`, `/signin-oidc` and `/signout-callback-oidc` back to the BFF, which is why the app can use same-origin relative URLs in development and in production alike. + +## What to Look For + +- **Login, logout and the session** — `vue.client/src/auth.js` keeps the whole story in three reactive refs and three small functions. `checkAuthStatus()` calls `/bff/user` and stores the returned claims, `login()` redirects to `/bff/login`, and `logout()` navigates to the `bff:logout_url` claim value that the BFF hands back in `/bff/user`. JavaScript never sees an access token; it only learns *who* is logged in. +- **One helper for authenticated calls** — `vue.client/src/utils/api.js` exports a single `fetchApi(url, options)` wrapper that adds `credentials: 'include'` and the `X-CSRF: 1` header, and normalizes errors and `204 No Content` responses. Every view goes through it, so the CSRF requirement is never forgotten. +- **The CSRF header** — the `X-CSRF: 1` header is what lets the BFF reject cross-site request forgery on its API endpoints. Omitting it makes the BFF answer `400`. +- **The local API** — `Vue.Bff/ToDoEndpointGroup.cs` holds a small in-memory ToDo store, and `Program.cs` maps it with `.RequireAuthorization().AsBffApiEndpoint()`. That marks it as a BFF endpoint, which requires both a valid session and a valid CSRF header. +- **Switching to a remote API** — `Program.cs` contains a commented-out `MapRemoteBffApiEndpoint("/todos", "https://localhost:7001/todos").RequireAccessToken(Duende.Bff.TokenType.User)` showing the alternative: keep the same UI and let the BFF proxy the calls to a real downstream API instead of handling them locally. The UI does not change at all. +- **Validating tokens in the API** — `Vue.Api` is a plain JWT bearer resource server. It checks the `api` audience and guards the ToDo endpoints with two policies: `ApiCaller` (the token carries the `api` scope) and `InteractiveUser` (the token carries a `sub`, so a real user is behind it). It also calls `WithOpenApi()`, so you can browse the endpoint shape at `/openapi/v1.json`. +- **Client-side routing** — `vue.client/src/router/index.js` uses `createWebHistory` and lazy-loads the ToDo and user-session views. Its `beforeEnter` guards redirect to `/` when nobody is logged in, but that is only a UI convenience — the actual protection is the `RequireAuthorization()` on the BFF endpoints. +- **Explicit authentication schemes** — in BFF 3.x the cookie and OIDC handlers are registered by hand and the default schemes are named `cookie` / `oidc`: + ```csharp + builder.Services.AddAuthentication(options => + { + options.DefaultScheme = "cookie"; + options.DefaultChallengeScheme = "oidc"; + options.DefaultSignOutScheme = "oidc"; + }).AddCookie("cookie", options => { /* ... */ }) + .AddOpenIdConnect("oidc", options => { /* ... */ }); + ``` + BFF 4.x folds this into the BFF builder via `.ConfigureOpenIdConnect(...)` and `.ConfigureCookies(...)`, and adds the data-protection setup. See the [BFF v4 sample](../v4/Vue) for that style. diff --git a/BFF/v4/Angular/README.md b/BFF/v4/Angular/README.md new file mode 100644 index 000000000..f5e68d19c --- /dev/null +++ b/BFF/v4/Angular/README.md @@ -0,0 +1,10 @@ +# BFF Angular Sample + +This sample shows an Angular client being hosted by a BFF project. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## How to Run + +1. Run the `Angular.Api`, and `Angular.Bff` projects +1. Login with username/password `bob` diff --git a/BFF/v4/BlazorAutoRendering/README.md b/BFF/v4/BlazorAutoRendering/README.md new file mode 100644 index 000000000..283d0837b --- /dev/null +++ b/BFF/v4/BlazorAutoRendering/README.md @@ -0,0 +1,34 @@ +# BFF Blazor Auto-Rendering Sample + +This sample shows the Blazor "auto rendering" model behind a [Backend-for-Frontend (BFF)](https://docs.duendesoftware.com/identityserver/bff), together with a YARP-proxied remote API. + +A single ASP.NET Core host serves the Blazor components with **both** the `InteractiveServer` and `InteractiveWebAssembly` render modes registered, and lets the framework pick per route. The interesting part is `IWeatherClient`: the same abstraction is resolved to a server-side implementation when the component renders on the server, and to an HTTP client that goes through the BFF when it renders in the browser. You get the security of the BFF without giving up interactivity, and without writing the component twice. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`BlazorAutoRendering`** — the BFF host, running on `https://localhost:7035`. It serves the Blazor app, the `/bff/*` management endpoints, a local API endpoint and a proxied remote API endpoint. +- **`BlazorAutoRendering.Client`** — the WebAssembly assembly. It has no launch profile of its own; the host serves it. +- **`BlazorAutoRendering.Api`** — a separate JWT-protected demo API, running on `https://localhost:7001`. This is the one the BFF proxies to. + +## How to Run + +1. Run the `BlazorAutoRendering` and `BlazorAutoRendering.Api` projects. +1. Browse to `https://localhost:7035` and use the login link. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Visit the Weather and Greet pages, then toggle interactive rendering to compare the two paths. + +The sample does not run its own identity server, so you need internet access to reach the demo server. + +## What to Look For + +- **Render modes** — `Program.cs` registers `AddInteractiveServerRenderMode()` and `AddInteractiveWebAssemblyRenderMode()` and adds the client assembly with `AddAdditionalAssemblies(...)`. `Pages/Home.razor` is `InteractiveServer`, `Pages/Counter.razor` is `InteractiveWebAssembly`, and `Pages/Weather.razor` and `Pages/Greet.razor` use `@rendermode InteractiveAuto` and let the framework choose. +- **The swappable client** — `AddSingleton()` on the host resolves the interface to an in-process implementation. The client project resolves the *same* interface to a `WeatherClient` that calls the BFF. Because the component only knows `IWeatherClient`, the same code works in both places. +- **The remote API path** — `MapRemoteBffApiEndpoint("/remote-apis/greetings", new Uri("https://localhost:7001")).WithAccessToken(RequiredTokenType.User)` proxies the greetings API and attaches the user's access token. The client is told where that prefix is with `AddBffBlazorClient(opt => opt.RemoteApiPath = "remote-apis/greetings/")`, so `Pages/Greet.razor` never hard-codes it. +- **The v4 token API** — this is the headline rename in BFF 4: `RequireAccessToken(TokenType.User)` became `WithAccessToken(RequiredTokenType.User)`, and the remote API address is now a `Uri` rather than a string. The enum moved to the `Duende.Bff.AccessTokenManagement` namespace. +- **Seeing which path ran** — `Pages/Greet.razor` renders `response.RequestMessage?.RequestUri`, so the displayed URL tells you whether the call was served locally or went through the proxy. +- **The local API endpoint** — `WeatherEndpointExtensions.MapWeatherEndpoints()` maps `GET /WeatherForecast` with `.RequireAuthorization().AsBffApiEndpoint()`, which adds the CSRF header check on top of authorization. +- **BFF 4.x configuration style** — the OIDC and cookie setup is part of the BFF builder: `.ConfigureOpenIdConnect(o => { ... })` and `.ConfigureCookies(o => { ... })` replace the separate `AddCookie` / `AddOpenIdConnect` calls. The host no longer needs an explicit `OpenIdConnect` package reference, since it arrives transitively. +- **Data protection** — `AddDataProtection().SetApplicationName("BFF")` gives the BFF a stable application name so its data-protected session state survives restarts and works across replicas. See the [data protection docs](https://docs.duendesoftware.com/general/data-protection). +- **`SameSite=Lax` is deliberate here** — the sample carries a comment explaining that because the identity provider is on `duendesoftware.com` while the BFF is on `localhost`, a `Strict` cookie would not be sent after the redirect back from login. Use `Strict` when your identity provider is on the same site as the BFF. diff --git a/BFF/v4/BlazorWasm/README.md b/BFF/v4/BlazorWasm/README.md new file mode 100644 index 000000000..ebb5bfc6a --- /dev/null +++ b/BFF/v4/BlazorWasm/README.md @@ -0,0 +1,34 @@ +# BFF Blazor WebAssembly Sample + +This sample shows a standalone Blazor WebAssembly application protected by a [Backend-for-Frontend (BFF)](https://docs.duendesoftware.com/identityserver/bff), so that no tokens ever reach the browser. + +An ordinary ASP.NET Core application authenticates the user with OIDC and then serves the Blazor WebAssembly app with `MapStaticAssets()` and `MapFallbackToFile("index.html")`. The WASM app talks to the BFF's own endpoints and never sees an access token. This version also shows how the BFF can proxy a remote API with YARP, adding the user's access token on the way out. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`BFF`** — the BFF host, running on `https://localhost:7256`. It handles the OIDC login, maps the `/bff/*` management endpoints, serves a local API endpoint and a remote API endpoint, and serves the WebAssembly app itself. +- **`BlazorWasm`** — the Blazor WebAssembly application. + +Because the host serves the WebAssembly app, **the only URL you need is `https://localhost:7256`**. The `BlazorWasm` project also has its own launch profile on `https://localhost:7206`, which is only used when running the WASM app standalone against the dev server. + +Note that this sample no longer has a `Shared` project, and the host project is named `BFF` rather than `Server`. The solution is the newer `BlazorSample.slnx` format. + +## How to Run + +1. Run the `BFF` project. +1. Browse to `https://localhost:7256` and use the login link in the navigation. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Visit the Weather page, which calls both the local and the remote API endpoint, then sign out. + +This sample does not run its own identity server, so you need internet access to reach the demo server. The remote API is served by the demo server as well. + +## What to Look For + +- **Two kinds of protected endpoint** — `/api/data` is a local endpoint that reads a static `BFF/weather.json` and returns it, guarded by `.RequireAuthorization().AsBffApiEndpoint()`. `/remoteapi` is a YARP endpoint that proxies to `https://demo.duendesoftware.com/api` and attaches the user's access token with `.WithAccessToken()`. The Weather and Claims pages call one of each, so you can compare them. +- **BFF 4.x configuration style** — the OIDC and cookie setup is part of the BFF builder: `.ConfigureOpenIdConnect(o => { ... })` and `.ConfigureCookies(o => { ... })` replace the separate `AddCookie` / `AddOpenIdConnect` calls. `options.ClaimActions.MapAll()` is used so the home page can show every claim the demo server returns. +- **Data protection** — `AddDataProtection().SetApplicationName("BFF")` gives the BFF a stable application name so its data-protected state survives restarts and works across replicas. See the [data protection docs](https://docs.duendesoftware.com/general/data-protection). +- **The client `HttpClient` is registered by hand here** — unlike the other Blazor samples, `BlazorWasm/Program.cs` creates a plain `HttpClient` with `X-CSRF` added to its default request headers, rather than calling `AddLocalApiHttpClient`. `AddBffBlazorClient()` still provides the authentication state provider. +- **Authorization is enforced server-side only** — no page in this sample carries `[Authorize]`, and `App.razor` uses a plain `RouteView` rather than an `AuthorizeRouteView`. The protection lives on the BFF's endpoints, so a page may render while its data calls return a challenge. Compare with the [BlazorAutoRendering sample](../v4/BlazorAutoRendering) for the client-side pattern. +- **The session cookie** — `__Host-blazor` with `SameSite=Strict`. diff --git a/BFF/v4/DPoP/README.md b/BFF/v4/DPoP/README.md new file mode 100644 index 000000000..bf657d0a7 --- /dev/null +++ b/BFF/v4/DPoP/README.md @@ -0,0 +1,32 @@ +# BFF DPoP Sample + +This sample shows a BFF that requests *DPoP-bound* access tokens (Demonstration of Proof of Possession, [RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)) and calls a downstream API with them. + +Instead of a bearer token that anyone can replay, DPoP binds the access token to a key that only the BFF holds. For every API call the BFF generates a short-lived *DPoP proof* JWT that covers the HTTP method and target URI, and the API checks that proof against the request it actually received. A stolen access token on its own is therefore useless without the corresponding private key. + +The sample shows the four access-token flavours side by side — anonymous, user, client credentials, and user-or-client — both through the BFF's own endpoint helpers and through raw YARP routes. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`DPoP.Api`** — the downstream API, running on `https://localhost:6001`. It validates the access token *and* the DPoP proof on every request, and echoes back the path, message and headers. It ships a self-contained DPoP validation implementation under `DPoP.Api/DPoP/`, so you can see exactly what a resource server has to do. +- **`DPoP.Bff`** — the BFF, running on `https://localhost:5002`. It hosts a small static UI and proxies the API calls on the browser's behalf. + +## How to Run + +1. Run the `DPoP.Bff` and `DPoP.Api` projects. +1. Browse to `https://localhost:5002` and log in. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Try the different endpoints and watch the requests and responses in the browser's network tab. + +## What to Look For + +- **Enabling DPoP in the BFF** — `Program.cs` in `DPoP.Bff` creates a 2048-bit RSA key, converts it to a JWK, sets `Alg` to `PS256`, and wraps it in a `DPoPProofKey`, which is then assigned to `options.DPoPJsonWebKey` inside `AddBff(options => { ... })`. Setting this key is all it takes to switch the BFF over to DPoP. +- **BFF 4.x configuration style** — the OIDC and cookie setup is now part of the BFF builder: `.ConfigureOpenIdConnect(o => { ... })` and `.ConfigureCookies(o => { ... })` replace the separate `AddCookie` / `AddOpenIdConnect` calls, and the default schemes come from the `BffAuthenticationSchemes` constants (`BffCookie`, `BffOpenIdConnect`). +- **Data protection** — `AddDataProtection().SetApplicationName("BFF")` gives the BFF a stable application name so its data-protected state (including the DPoP key material) survives restarts and works across replicas. See the [data protection docs](https://docs.duendesoftware.com/general/data-protection). +- **Token types** — the BFF exposes `/api/anonymous`, `/api/user-token`, `/api/client-token` and `/api/user-or-client-token`, each guarded by the matching `WithAccessToken(RequiredTokenType.X)` requirement, plus `/api/optional-user-token` (`RequiredTokenType.UserOrNone`) for the "send a token if we have one" case. +- **YARP routes** — `/yarp/anonymous`, `/yarp/user-token`, `/yarp/client-token` and `/yarp/user-or-client-token` show the same calls driven purely by the YARP configuration in `YarpConfigurator.cs`. +- **Proof validation on the API** — `DPoP.Api/DPoP/DPoPProofValidator.cs` validates the proof's `htm`, `htu` and `ath` claims against the incoming request and access token, and enforces a one-second proof validity window. +- **DPoP nonces** — the API supports server-issued nonces. `DPoPOptions.ValidateNonce` defaults to `false`; when enabled the API responds with a `DPoP-Nonce` header and rejects the request until the client retries with that nonce. +- **No forwarded headers on the API** — the API deliberately does *not* use forwarded-headers middleware, because it would change the `htu` that the DPoP proof is validated against. diff --git a/BFF/v4/JsBffSample/README.md b/BFF/v4/JsBffSample/README.md new file mode 100644 index 000000000..305764fdc --- /dev/null +++ b/BFF/v4/JsBffSample/README.md @@ -0,0 +1,29 @@ +# BFF JavaScript Sample + +This sample shows the most basic shape of a BFF: an ASP.NET Core application that serves a plain JavaScript UI and handles the authentication and token handling for the browser, so no tokens ever reach JavaScript. + +The UI is served from `wwwroot` and talks to same-origin endpoints only. Log in and out happen through the BFF's management endpoints, and the session is read back from `/bff/user`. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`FrontendHost`** — the BFF, running on `https://localhost:5010`. It serves the static UI (`index.html`, `todo.js`, `session.js`), maps the `/bff/*` management endpoints, and hosts the ToDo API in the same process. +- **`BackendApiHost`** — a separate JWT-protected ToDo API, running on `https://localhost:5020`. It is not used in the default configuration, but it is there so you can compare the two styles. + +## How to Run + +1. Run the `FrontendHost` and `BackendApiHost` projects. +1. Browse to `https://localhost:5010` and use the login button. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create, update and delete ToDo items, then reload the page to see the session restored from the cookie. + +## What to Look For + +- **Login and logout** — the UI simply links to `/bff/login?returnUrl=/` and `/bff/logout?returnUrl=/`. All of the OAuth/OIDC flow happens in the BFF. +- **Reading the session** — `wwwroot/session.js` fetches `/bff/user` and renders the user's name and claims. JavaScript never sees an access token; it only learns *who* is logged in. +- **The CSRF header** — every call from the UI sends an `x-csrf: 1` header. This is what lets the BFF reject cross-site request forgery on its API endpoints. +- **The local API** — `FrontendHost/ToDoController.cs` is an ordinary MVC controller marked with `.RequireAuthorization().AsBffApiEndpoint()`. That marks it as a BFF endpoint, which requires both a valid session and a valid CSRF header. +- **Switching to a remote API** — `Program.cs` contains a commented-out `MapRemoteBffApiEndpoint("/todos", "https://localhost:5020/todos")` showing the alternative: keep the same UI and let the BFF proxy the calls to a real downstream API instead of handling them locally. +- **BFF 4.x configuration style** — the OIDC and cookie setup is part of the BFF builder: `.ConfigureOpenIdConnect(o => { ... })` and `.ConfigureCookies(o => { ... })` replace the separate `AddCookie` / `AddOpenIdConnect` calls, and the default schemes come from the `BffAuthenticationSchemes` constants. +- **Data protection** — `AddDataProtection().SetApplicationName("BFF")` gives the BFF a stable application name so its data-protected session state survives restarts and works across replicas. See the [data protection docs](https://docs.duendesoftware.com/general/data-protection). diff --git a/BFF/v4/JsBffYarpSample/README.md b/BFF/v4/JsBffYarpSample/README.md new file mode 100644 index 000000000..0bc7d917c --- /dev/null +++ b/BFF/v4/JsBffYarpSample/README.md @@ -0,0 +1,29 @@ +# BFF JavaScript Sample with YARP + +This sample is the reverse-proxy flavour of the [JavaScript BFF sample](../v4/JsBffSample). The UI is identical, but instead of handling the ToDo calls itself, the BFF runs a [YARP](https://github.com/dotnet/yarp) reverse proxy that forwards `/todos/**` to a real downstream API, attaching the user's access token on the way out. + +This is the pattern to reach for when the API already exists and you do not want to hand-write a controller per endpoint — the BFF becomes a secure gateway instead of a second implementation of your API. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`FrontendHost`** — the BFF, running on `https://localhost:5010`. It serves the static UI (`index.html`, `todo.js`, `session.js`), maps the `/bff/*` management endpoints, and hosts the reverse proxy. +- **`BackendApiHost`** — the downstream JWT-protected ToDo API, running on `https://localhost:5020`. Unlike in the non-YARP sample, this one really is called. + +## How to Run + +1. Run the `FrontendHost` and `BackendApiHost` projects. +1. Browse to `https://localhost:5010` and use the login button. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create and delete ToDo items, and watch the proxied calls in the browser's network tab. + +## What to Look For + +- **The proxy pipeline** — `Program.cs` calls `AddBff()`, then `AddReverseProxy().AddBffExtensions()`, then the sample's own `YarpConfigurator.Configure()`. In BFF 4, `AddBffExtensions()` is the convenience call that registers the access token transform for you, replacing the older `AddTransforms()`. +- **Routes in code** — `YarpConfigurator.cs` builds the route and cluster configuration in C# with `LoadFromMemory`. The route matches `/todos/{**catch-all}`, points at the `cluster1` cluster, and declares the token requirement with `.WithAccessToken(RequiredTokenType.User)`. The cluster's single destination is `https://localhost:5020`. +- **Loading config from memory** — `InMemoryConfigProvider.cs` implements `IProxyConfigProvider` and `IProxyConfig` so the routes can be handed to YARP as an in-memory object rather than a `appsettings.json` section. That is what makes the route able to carry BFF-specific configuration such as the access token requirement. +- **Mapping the proxy** — `app.MapBffReverseProxy()` is the shorthand for `MapReverseProxy().AsBffApiEndpoint()`. The `AsBffApiEndpoint()` part is what enforces the session check and the CSRF header. +- **Going back to a local API** — the sample keeps `ToDoController.cs` and the matching `MapControllers().AsBffApiEndpoint()` call, commented out, so you can switch between the two styles and compare. +- **BFF 4.x configuration style** — the OIDC and cookie setup is part of the BFF builder: `.ConfigureOpenIdConnect(o => { ... })` and `.ConfigureCookies(o => { ... })` replace the separate `AddCookie` / `AddOpenIdConnect` calls, and the default schemes come from the `BffAuthenticationSchemes` constants. Note that the token-type enum moved to `Duende.Bff.AccessTokenManagement` and became `RequiredTokenType`. +- **The CSRF header** — the UI sends `x-csrf: 1` on its requests, which the BFF requires before it will forward anything. diff --git a/BFF/v4/OpenApi/README.md b/BFF/v4/OpenApi/README.md new file mode 100644 index 000000000..093d5b152 --- /dev/null +++ b/BFF/v4/OpenApi/README.md @@ -0,0 +1,35 @@ +# BFF OpenAPI Sample + +This sample shows a BFF that proxies two downstream APIs *and* rewrites their OpenAPI documents on the fly, so the front end sees a single, browser-facing API surface. + +Normally a downstream API's OpenAPI document describes server-to-server calls: it names the API's own `servers` entry and its JWT `securitySchemes`. Neither is useful to a browser talking to a BFF with a cookie. This sample intercepts each `/openapi/*.json` response as it is proxied through YARP and rewrites it — repointing `servers` at the BFF, dropping the security schemes, and prefixing every path with the BFF's local path. It then merges both APIs into one combined document, so Swagger UI and any client generator see the whole BFF surface at once. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`OpenApi.Api1`** — a minimal API exposing `GET /weatherforecastFahrenheit`, running on `https://localhost:7289`. It commits its generated OpenAPI document (`OpenApi.Api1.json`) and is protected by JWT bearer authentication. +- **`OpenApi.Api2`** — the same, exposing `GET /WeatherforecastCelcius`, running on `https://localhost:7297`. +- **`OpenApi.Bff`** — the BFF, running on `https://localhost:7082`. Hosts a static UI and Swagger UI, proxies both APIs, and rewrites and combines their OpenAPI documents. Its OIDC, cookie and frontend settings are read from the `BFF` section of `appsettings.json`. +- **`OpenApi.BffOpenApiDocumentParser`** — a small console app that applies the same document transformation offline, so a pre-transformed document can be produced at build time for tooling or code generation. `OpenApi.Bff` references it as a project so both share one implementation. +- **`OpenApi.DevServer.AppHost`** — the .NET Aspire host that starts everything and wires up service discovery. Its dashboard runs on `https://localhost:17275`. +- **`OpenApi.DevServer.ServiceDefaults`** — the shared Aspire service defaults (OpenTelemetry, health checks, JWT bearer validation) referenced by the other projects. + +## How to Run + +1. Run the `OpenApi.DevServer.AppHost` project (or use the Aspire CLI with `aspire run`), which starts the two APIs and the BFF for you. +1. Open the Aspire dashboard at `https://localhost:17275` and click through to the BFF. +1. Log in with username `bob` and password `bob` (or `alice` / `alice`) at the [Duende demo identity server](https://demo.duendesoftware.com). +1. Browse to `https://localhost:7082` for the UI, or `https://localhost:7082/swagger` for Swagger UI. + +You can also run the projects individually without Aspire, in which case the fixed ports in each `launchSettings.json` apply. + +## What to Look For + +- **Rewriting the documents** — `OpenApiResponseTransform.cs` is registered as a YARP response transform. When a proxied response is an OpenAPI document it suppresses the original body and pipes it through `OpenApiTransformer.TransformOpenApiDocumentForBff`, which clears `Servers` and `Components.SecuritySchemes` and prefixes every path with the BFF's local path. +- **Combining the documents** — `OpenApiDocumentCombiner.cs` fetches each source document and merges the `Paths` and `Components` into one. Security schemes are intentionally *not* copied, because the browser never presents a bearer token to the BFF. Served from `/swagger/combined/v1.json`. +- **The BFF endpoint mapping** — `app.MapRemoteBffApiEndpoint("/api1", uri).WithAccessToken(RequiredTokenType.UserOrNone)` and the same for `/api2`. Both APIs are optional here so the document endpoints stay reachable. +- **Swagger UI as the browser** — `UseSwaggerUI(...)` registers all three documents, injects an `X-CSRF` request interceptor, and adds `bff-auth-button.js` so you can drive the "try it" buttons from the BFF session. +- **Service discovery** — `AddHttpForwarderWithServiceDiscovery()` lets YARP resolve the logical service names, so the BFF does not need to know the APIs' real addresses. +- **BFF 4.x configuration style** — the BFF reads its OIDC, cookie and frontend settings straight from the `BFF` section of `appsettings.json` via `AddBff().LoadConfiguration(...)`. There is no hand-written configuration POCO in this version. +- **OpenAPI library version** — this version uses `Microsoft.OpenApi` 2.x, so the document is read with `OpenApiDocument.LoadAsync(...)` and written with `SerializeAsJsonAsync(...)` rather than the older `OpenApiStreamReader` API. diff --git a/BFF/v4/Quickstarts/BlazorBffApp/README.md b/BFF/v4/Quickstarts/BlazorBffApp/README.md new file mode 100644 index 000000000..455cc60a1 --- /dev/null +++ b/BFF/v4/Quickstarts/BlazorBffApp/README.md @@ -0,0 +1,34 @@ +# Blazor BFF Quickstart + +This is the smallest complete BFF + Blazor app in the repository — a good starting point to copy from. + +There is one protected endpoint and nothing else: no YARP, no remote APIs, no separate API host. If you want to understand the moving parts before adding complexity, start here and then look at the [Blazor auto-rendering sample](../BlazorAutoRendering) or the [Blazor WebAssembly sample](../BlazorWasm). + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`BlazorBffApp`** — the BFF host, running on `https://localhost:7007`. It serves the Blazor app, the `/bff/*` management endpoints and the local API endpoint. +- **`BlazorBffApp.Client`** — the WebAssembly assembly. It has no launch profile of its own; the host serves it. + +## How to Run + +1. Run the `BlazorBffApp` project. +1. Browse to `https://localhost:7007` and use the login link. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Visit the Weather page, which calls the protected endpoint, then sign out. + +The sample does not run its own identity server, so you need internet access to reach the demo server. + +## What to Look For + +- **The whole BFF setup** — `Program.cs` is a single file you can read top to bottom: `AddBff().AddServerSideSessions().AddBlazorServer()`, the OIDC and cookie configuration, then `UseBff()` between `UseAuthentication()` and `UseAuthorization()`. +- **`AddServerSideSessions()`** — keeps session state on the server rather than inside the encrypted cookie, which is what lets the BFF hold access tokens across requests. +- **`AddBlazorServer()`** — the BFF-specific registration that lets Blazor components participate in the BFF session. +- **The BFF 4.x configuration style** — the cookie and OIDC setup is part of the BFF builder: `.ConfigureOpenIdConnect(o => { ... })` and `.ConfigureCookies(o => { ... })` replace the separate `AddAuthentication(...).AddCookie("cookie", ...).AddOpenIdConnect("oidc", ...)` chain used in BFF 3 and earlier. This is the single most visible difference between the two versions of this quickstart. +- **Data protection** — `AddDataProtection().SetApplicationName("BFF")` is new in this version. It gives the BFF a stable application name so the key ring protecting its session state is not regenerated on every restart; for more than one instance you would also need to persist and share those keys. See the [data protection docs](https://docs.duendesoftware.com/general/data-protection). +- **The cookie configuration** — `__Host-blazor` with `SameSite=Lax`. The sample includes a comment explaining that `Lax` is needed here only because the identity provider is on `duendesoftware.com` while the BFF is on `localhost`; use `Strict` when both are on the same site. The `__Host-` prefix requires HTTPS and forbids an explicit cookie `Domain`. +- **The local API endpoint** — `app.MapGet("/WeatherForecast", ...)` returns the forecasts, and the client calls it with `AddLocalApiHttpClient()`, which attaches the CSRF header the BFF requires. +- **The swapped client** — the host registers `IWeatherClient` to an inline `ServerWeatherClient` that returns random data without any HTTP call, while the WebAssembly project registers it to a `WeatherHttpClient` that goes through the BFF. Same interface, two implementations, and the Blazor component does not care which one it got. +- **The client setup** — `BlazorBffApp.Client/Program.cs` is five lines: `AddBffBlazorClient()` for the authentication state provider that polls `/bff/user`, `AddLocalApiHttpClient()`, the `IWeatherClient` registration and `AddCascadingAuthenticationState()` so `AuthorizeView` works in components. This is unchanged from the v3 quickstart. +- **Antiforgery** — `UseBff()` performs the BFF CSRF header check, and `UseAntiforgery()` adds the ASP.NET Core token store that Blazor forms use. diff --git a/BFF/v4/React/README.md b/BFF/v4/React/README.md new file mode 100644 index 000000000..0f14ccbf8 --- /dev/null +++ b/BFF/v4/React/README.md @@ -0,0 +1,34 @@ +# BFF React Sample + +This sample shows a [React](https://react.dev/) single-page application hosted by a BFF project. The SPA is developed with [Vite](https://vite.dev/), but in production it is served as static files from the BFF itself, so everything ends up same-origin and no tokens ever reach JavaScript. + +The BFF runs the authorization code flow against the [Duende demo identity server](https://demo.duendesoftware.com), keeps the tokens server-side, and exposes the ToDo API to the browser through its own BFF endpoints. A second, standalone JWT-protected API is included so you can compare hosting the API in the BFF process against proxying to a real downstream service. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`React.Bff`** — the BFF, running on `https://localhost:6001`. It serves the SPA, maps the `/bff/*` management endpoints, and hosts the ToDo API in the same process. +- **`React.Api`** — a separate JWT-protected ToDo API, running on `https://localhost:7001`. It is not used in the default configuration, but it is there so you can compare the two styles. +- **`react.client`** — the Vite dev server, running on `https://localhost:5173`. It is a build-time dependency of `React.Bff` and is only used during development. + +## How to Run + +1. Run the `React.Bff` and `React.Api` projects. +1. Browse to `https://localhost:6001` and use the login button. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create, update and delete ToDo items, then reload the page to see the session restored from the cookie. + +Running `React.Bff` starts the Vite dev server through the ASP.NET Core SPA proxy, so you get hot module replacement while developing. The Vite server proxies `/bff/*`, `/todos`, `/signin-oidc` and `/signout-callback-oidc` back to the BFF, which is why the app can use same-origin relative URLs in development and in production alike. + +## What to Look For + +- **Login and logout** — the UI links to `/bff/login` and `/bff/logout`. All of the OAuth/OIDC flow happens in the BFF. +- **Reading the session** — `react.client/src/components/UserSession.jsx` fetches `bff/user` and renders the user's claims. JavaScript never sees an access token; it only learns *who* is logged in. +- **The CSRF header** — every call from the UI sends an `X-CSRF: 1` header. This is what lets the BFF reject cross-site request forgery on its API endpoints. +- **The local API** — `React.Bff/ToDoEndpointGroup.cs` holds a small in-memory ToDo store, and `Program.cs` maps it with `.RequireAuthorization().AsBffApiEndpoint()`. That marks it as a BFF endpoint, which requires both a valid session and a valid CSRF header. +- **Switching to a remote API** — `Program.cs` contains a commented-out `MapRemoteBffApiEndpoint("/todos", "https://localhost:7001/todos").RequireAccessToken(Duende.Bff.TokenType.User)` showing the alternative: keep the same UI and let the BFF proxy the calls to a real downstream API instead of handling them locally. The UI does not change at all. +- **Validating tokens in the API** — `React.Api` is a plain JWT bearer resource server. It checks the `api` audience and guards the ToDo endpoints with two policies: `ApiCaller` (the token carries the `api` scope) and `InteractiveUser` (the token carries a `sub`, so a real user is behind it). +- **Client-side routing** — `app.MapFallbackToFile("/index.html")` makes the BFF return the SPA for unknown paths, so a deep link such as `/users` still boots the React app. +- **BFF 4.x configuration style** — the OIDC and cookie setup is now part of the BFF builder: `.ConfigureOpenIdConnect(o => { ... })` and `.ConfigureCookies(o => { ... })` replace the separate `AddCookie` / `AddOpenIdConnect` calls, and the default schemes come from the `BffAuthenticationSchemes` constants (`BffCookie`, `BffOpenIdConnect`). +- **Data protection** — `AddDataProtection().SetApplicationName("BFF")` gives the BFF a stable application name so its data-protected session state survives restarts and works across replicas. See the [data protection docs](https://docs.duendesoftware.com/general/data-protection). diff --git a/BFF/v4/SplitHosts/README.md b/BFF/v4/SplitHosts/README.md new file mode 100644 index 000000000..2b803d116 --- /dev/null +++ b/BFF/v4/SplitHosts/README.md @@ -0,0 +1,30 @@ +# BFF Split Hosts Sample + +This sample shows what changes when the SPA and the BFF are **not** on the same origin. Instead of one application serving both the UI and the BFF endpoints, the UI is served by a bare static-file host on `https://localhost:5011` and the BFF runs separately on `https://localhost:5010`. + +The browser therefore makes *cross-origin* requests to the BFF, with `credentials: "include"`. That is the interesting part of this sample: CORS with credentials, cookie behaviour across origins, and the return-URL validation you need so that login and logout can only ever return to the SPA. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`FrontendHost`** — a static-file server only, running on `https://localhost:5011`. Its `Program.cs` is eleven lines and it references no BFF packages at all. It serves the same `index.html`, `todo.js` and `session.js` used in the other JavaScript samples. +- **`BackendHost`** — this is the BFF, running on `https://localhost:5010`. Despite the name, it is not a downstream service — it hosts the `/bff/*` management endpoints, the ToDo API, and the CORS policy. The browser is what calls it. +- **`BackendApiHost`** — a separate JWT-protected ToDo API, running on `https://localhost:5020`. It is not used in the default configuration. + +## How to Run + +1. Run the `FrontendHost`, `BackendHost` and `BackendApiHost` projects. +1. Browse to `https://localhost:5011` — this is the UI, and it is the URL you start from. +1. Use the login button, which redirects to the BFF on `:5010` and back again. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create and delete ToDo items, and watch the cross-origin calls in the network tab. + +## What to Look For + +- **The SPA points at the BFF explicitly** — `wwwroot/todo.js` builds its requests against `https://localhost:5010/todos` and sets `credentials: "include"`, and `wwwroot/session.js` reads `https://localhost:5010/bff/user`. The `returnUrl` in the login and logout links is `https://localhost:5011`, not `/`. +- **CORS is locked down to the SPA** — `Program.cs` in `BackendHost` registers a default policy allowing only the origin `https://localhost:5011`, only the `x-csrf` and `content-type` headers, only the `DELETE` method, and crucially `AllowCredentials()`. The app then calls `app.UseCors()`. +- **The return-URL validator** — `FrontendHostReturnUrlValidator.cs` implements `IReturnUrlValidator` and only accepts a return URL on host `localhost`, port `5011`. Without this, the BFF's `returnUrl` parameter would be an open redirect. In BFF 4 this interface moved to the `Duende.Bff.Endpoints` namespace and takes a `Uri` instead of a string. +- **Why the cookie still works** — the session cookie is `__Host-bff` with `SameSite = Strict`. The `__Host-` prefix requires HTTPS and no explicit `Domain`, and `Strict` cookies are still sent cross-origin here because `:5010` and `:5011` are the *same site* (same host, different ports). Change the ports to different hostnames and the sample would need `SameSite = None` plus `Secure`. +- **Data protection matters more here** — `AddDataProtection().SetApplicationName("BFF")` gives the BFF a stable application name so the key ring protecting its session cookie is not regenerated on restart. For a split deployment with more than one BFF instance you would also need to persist and share those keys. See the [data protection docs](https://docs.duendesoftware.com/general/data-protection). +- **No BFF code in the frontend** — `FrontendHost` is deliberately package-free. The split costs you nothing in terms of what the BFF can do; it only adds the CORS and return-URL configuration on the BFF side. diff --git a/BFF/v4/TokenExchange/README.md b/BFF/v4/TokenExchange/README.md new file mode 100644 index 000000000..740caab2b --- /dev/null +++ b/BFF/v4/TokenExchange/README.md @@ -0,0 +1,29 @@ +# BFF Token Exchange Sample + +This sample shows [OAuth 2.0 Token Exchange](https://www.rfc-editor.org/rfc/rfc8693) in a BFF. Before calling the downstream API's `/api/impersonation` endpoint, the BFF takes the signed-in user's access token and exchanges it at the identity provider's token endpoint for a *different* one. + +The BFF authenticates as its own confidential client (`spa` / `secret`) and passes the user's token as the `subject_token`, with `subject_token_type=urn:ietf:params:oauth:token-type:access_token` and `grant_type=urn:ietf:params:oauth:grant-type:token-exchange`. The custom grant validator in the identity provider swaps `alice` for `bob` (and vice versa), so the API receives an access token for the *other* user while the browser still holds a session for the original one. This is the classic delegation / impersonation flow. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`TokenExchange.IdentityServer`** — a full Duende IdentityServer host, running on `https://localhost:5001`. This sample ships its own identity provider (rather than using the public demo server) so the token exchange grant can be demonstrated. `TokenExchangeGrantValidator.cs` implements the `IExtensionGrantValidator` that performs the user swap, and a checked-in signing key lives under `keys/`. +- **`TokenExchange.Bff`** — the BFF, running on `https://localhost:6001`. Hosts a static UI and hosts both the plain and the impersonating API endpoints. +- **`TokenExchange.Api`** — the downstream API, running on `https://localhost:7001`. A JWT-protected resource API whose echo endpoint shows which user the presented token actually represents. + +## How to Run + +1. Run the `TokenExchange.IdentityServer`, `TokenExchange.Bff` and `TokenExchange.Api` projects. +1. Browse to `https://localhost:6001` and log in. +1. Log in with username `alice` and password `alice` (or `bob` / `bob`). +1. Call the user-token endpoint, then the impersonation endpoint, and compare the `sub` claim that comes back each time. + +## What to Look For + +- **The exchange on the BFF side** — `ImpersonationAccessTokenRetriever.cs` implements BFF 4's asynchronous `IAccessTokenRetriever` contract (`GetAccessTokenAsync`). It reads the user's token with `HttpContext.GetUserAccessTokenAsync(...)` and then calls `HttpClient.RequestTokenExchangeTokenAsync(...)` with a `TokenExchangeTokenRequest` pointed at the identity provider's `/connect/token` endpoint. The result is returned as a `BearerTokenResult`; failures come back as an `AccessTokenRetrievalError`. +- **Wiring the retriever to an endpoint** — `/api/user-token` is a plain remote endpoint requiring a user access token. `/api/impersonation` adds `.WithAccessTokenRetriever()`, which is the only difference between the two. Both use the BFF 4 token API: `MapRemoteBffApiEndpoint(path, uri).WithAccessToken(RequiredTokenType.User)`. +- **The exchange on the identity provider side** — `TokenExchangeGrantValidator.cs` implements `IExtensionGrantValidator` with `GrantType => OidcConstants.GrantTypes.TokenExchange`. It validates the incoming `subject_token` with `ITokenValidator.ValidateAccessTokenAsync`, requires `subject_token_type` to be an access token, and returns a grant result whose subject is the *other* user, with `authenticationMethod: "swap-alice-and-bob"`. +- **The token exchange grant is not built in** — registering a custom extension grant validator via `isBuilder.AddExtensionGrantValidator()` is all that is needed; Duende IdentityServer does not ship a token exchange grant out of the box. +- **BFF 4.x configuration style** — the OIDC and cookie setup is part of the BFF builder: `.ConfigureOpenIdConnect(o => { ... })` and `.ConfigureCookies(o => { ... })` replace the separate `AddCookie` / `AddOpenIdConnect` calls, and the default schemes come from the `BffAuthenticationSchemes` constants. +- **Test users** — `TokenExchange.IdentityServer/Pages/TestUsers.cs` holds the in-memory users `alice` / `alice` and `bob` / `bob`. The `spa` / `secret` client is allowed the `authorization_code`, `client_credentials` and `token-exchange` grants. diff --git a/BFF/v4/Vue/README.md b/BFF/v4/Vue/README.md new file mode 100644 index 000000000..dc795ff38 --- /dev/null +++ b/BFF/v4/Vue/README.md @@ -0,0 +1,34 @@ +# BFF Vue Sample + +This sample shows a [Vue](https://vuejs.org/) single-page application hosted by a BFF project. The SPA is developed with [Vite](https://vite.dev/), but in production it is served as static files from the BFF itself, so everything ends up same-origin and no tokens ever reach JavaScript. + +The BFF runs the authorization code flow against the [Duende demo identity server](https://demo.duendesoftware.com), keeps the tokens server-side, and exposes the ToDo API to the browser through its own BFF endpoints. A second, standalone JWT-protected API is included so you can compare hosting the API in the BFF process against proxying to a real downstream service. + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the structure of our samples and how to run them. + +## Sample Structure + +- **`Vue.Bff`** — the BFF, running on `https://localhost:6001`. It serves the SPA, maps the `/bff/*` management endpoints, and hosts the ToDo API in the same process. +- **`Vue.Api`** — a separate JWT-protected ToDo API, running on `https://localhost:7001`. It is not used in the default configuration, but it is there so you can compare the two styles. +- **`vue.client`** — the Vite dev server, running on `https://localhost:4200`. It is a build-time dependency of `Vue.Bff` and is only used during development. + +## How to Run + +1. Run the `Vue.Bff` and `Vue.Api` projects. +1. Browse to `https://localhost:6001` and use the login button. +1. Log in at the [Duende demo identity server](https://demo.duendesoftware.com) with username `bob` and password `bob` (or `alice` / `alice`). +1. Create, update and delete ToDo items, then reload the page to see the session restored from the cookie. + +Running `Vue.Bff` starts the Vite dev server through the ASP.NET Core SPA proxy, so you get hot module replacement while developing. The Vite server proxies `/bff/*`, `/todos`, `/signin-oidc` and `/signout-callback-oidc` back to the BFF, which is why the app can use same-origin relative URLs in development and in production alike. + +## What to Look For + +- **Login, logout and the session** — `vue.client/src/auth.js` keeps the whole story in three reactive refs and three small functions. `checkAuthStatus()` calls `/bff/user` and stores the returned claims, `login()` redirects to `/bff/login`, and `logout()` navigates to the `bff:logout_url` claim value that the BFF hands back in `/bff/user`. JavaScript never sees an access token; it only learns *who* is logged in. +- **One helper for authenticated calls** — `vue.client/src/utils/api.js` exports a single `fetchApi(url, options)` wrapper that adds `credentials: 'include'` and the `X-CSRF: 1` header, and normalizes errors and `204 No Content` responses. Every view goes through it, so the CSRF requirement is never forgotten. +- **The CSRF header** — the `X-CSRF: 1` header is what lets the BFF reject cross-site request forgery on its API endpoints. Omitting it makes the BFF answer `400`. +- **The local API** — `Vue.Bff/ToDoEndpointGroup.cs` holds a small in-memory ToDo store, and `Program.cs` maps it with `.RequireAuthorization().AsBffApiEndpoint()`. That marks it as a BFF endpoint, which requires both a valid session and a valid CSRF header. +- **Switching to a remote API** — `Program.cs` contains a commented-out `MapRemoteBffApiEndpoint("/todos", "https://localhost:7001/todos").RequireAccessToken(Duende.Bff.TokenType.User)` showing the alternative: keep the same UI and let the BFF proxy the calls to a real downstream API instead of handling them locally. The UI does not change at all. +- **Validating tokens in the API** — `Vue.Api` is a plain JWT bearer resource server. It checks the `api` audience and guards the ToDo endpoints with two policies: `ApiCaller` (the token carries the `api` scope) and `InteractiveUser` (the token carries a `sub`, so a real user is behind it). It also calls `WithOpenApi()`, so you can browse the endpoint shape at `/openapi/v1.json`. +- **Client-side routing** — `vue.client/src/router/index.js` uses `createWebHistory` and lazy-loads the ToDo and user-session views. Its `beforeEnter` guards redirect to `/` when nobody is logged in, but that is only a UI convenience — the actual protection is the `RequireAuthorization()` on the BFF endpoints. +- **BFF 4.x configuration style** — the OIDC and cookie setup is now part of the BFF builder: `.ConfigureOpenIdConnect(o => { ... })` and `.ConfigureCookies(o => { ... })` replace the separate `AddCookie` / `AddOpenIdConnect` calls, and the default schemes come from the `BffAuthenticationSchemes` constants (`BffCookie`, `BffOpenIdConnect`). +- **Data protection** — `AddDataProtection().SetApplicationName("BFF")` gives the BFF a stable application name so its data-protected session state survives restarts and works across replicas. See the [data protection docs](https://docs.duendesoftware.com/general/data-protection). diff --git a/IdentityServer/v7/AspNetIdentity/README.md b/IdentityServer/v7/AspNetIdentity/README.md new file mode 100644 index 000000000..853d2a898 --- /dev/null +++ b/IdentityServer/v7/AspNetIdentity/README.md @@ -0,0 +1,106 @@ +# ASP.NET Core Identity sample + +This sample shows how to combine Duende IdentityServer with ASP.NET Core Identity, so that the +default ASP.NET Core Identity UI (login, register, manage account) is the login experience that +IdentityServer uses. + +### What the sample demonstrates + +- `AddAspNetIdentity()` to plug ASP.NET Core Identity into IdentityServer, so + IdentityServer issues tokens for ASP.NET Core Identity users +- `AddDefaultIdentity()` with a SQLite `ApplicationDbContext`, and + `options.SignIn.RequireConfirmedAccount = true` — a user whose email is not confirmed cannot sign + in +- using the default ASP.NET Core Identity UI for login, registration and account management, hosted + in the same application as the token server +- a customized Identity `Logout` page that calls + `IIdentityServerInteractionService.GetLogoutContextAsync` and exposes `SignOutIframeUrl`, so + IdentityServer's logout prompt and its sign-out iframe work with the Identity UI +- seeding a local SQLite user store from the command line, and a **Seed Database** command on the + Aspire dashboard resource that does the same thing +- a public client using `GrantTypes.Implicit` with `FrontChannelLogoutUri`, which signs in through + the Identity UI and dumps the resulting claims + +## How to Run + +1. Seed the database. In the Aspire dashboard click **Seed Database** on the `identityserver` + resource, or run the seed manually: + + ```bash + dotnet run --project IdentityServerAspNetIdentity -- /seed + ``` + + > **Note:** seeding calls `EnsureDeleted()` before `Migrate()`, so the SQLite database is + > **recreated from scratch every time**. Any user you register through the Identity UI is lost + > the next time you seed. + +1. Run the Aspire project: + + ```bash + dotnet run --project AspNetIdentity.AppHost + ``` + + The Aspire dashboard opens at `https://localhost:17237`. + +1. Open a browser tab to the `client` application at `https://localhost:5002`. +1. Click on the **Secure** tab. You will be redirected to the IdentityServer application hosted at + `https://localhost:5001`. +1. Log in with the seeded user (see below) on the ASP.NET Core Identity login page. +1. You will be redirected back to the `client` application, which shows the claims from the tokens. + Alice has `name`, `given_name`, `family_name` and `website`; Bob has those plus a custom + `location` claim of `somewhere`. +1. Use the **Logout** button in the client navigation. The custom Identity `Logout` page asks + IdentityServer whether a sign-out prompt is needed and renders the sign-out iframe so the + session ends at the token server too. + +You can also go straight to `https://localhost:5001` to use the Identity UI directly — login, +register, manage account — and IdentityServer's own home, diagnostics and grants pages. + +## Seeded users + +| Username | Password | Notes | +|----------|----------|-------| +| `AliceSmith@email.example` | `Pass123$` | Email is confirmed, no additional claims | +| `BobSmith@email.example` | `Pass123$` | Adds a custom `location` claim of `somewhere` | + +The username and the email address are the same string. Note that `RequireConfirmedAccount` is on, +which both seeded users satisfy because `EmailConfirmed = true`. + +## Projects and URLs + +| Project | URL | +|---------|-----| +| `IdentityServerAspNetIdentity` | `https://localhost:5001` | +| `Client` | `https://localhost:5002` | +| `AspNetIdentity.AppHost` (Aspire dashboard) | `https://localhost:17237` | + +The solution file is `AspNetIdentitySample.sln`; it contains all three projects plus the shared +`Aspire.ServiceDefaults`. + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) + +No database server is needed — the sample uses a local SQLite file (`Users.db`). + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `IdentityServerAspNetIdentity/Program.cs` — the `AddDefaultIdentity()`, + `AddAspNetIdentity()` and `/seed` switch calls +- `IdentityServerAspNetIdentity/SeedData.cs` — the test users +- `IdentityServerAspNetIdentity/Areas/Identity/Pages/Account/Logout.cshtml.cs` — the + IdentityServer-aware logout page +- `IdentityServerAspNetIdentity/Areas/Identity/IdentityHostingStartup.cs` — how the Identity UI area + is mounted +- `Client/Program.cs` and `Client/Pages/Secure.cshtml` — the OIDC client +- `AspNetIdentity.AppHost/AppHost.cs` — the "Seed Database" command + +To add **passkeys** on top of ASP.NET Core Identity, see the sibling +[`AspNetIdentityPasskeys`](../AspNetIdentityPasskeys) sample. diff --git a/IdentityServer/v7/AspNetIdentityPasskeys/README.md b/IdentityServer/v7/AspNetIdentityPasskeys/README.md new file mode 100644 index 000000000..64ee75a46 --- /dev/null +++ b/IdentityServer/v7/AspNetIdentityPasskeys/README.md @@ -0,0 +1,106 @@ +# ASP.NET Core Identity with Passkeys sample + +[Passkeys](https://passkeys.dev/) are a passwordless sign-in method built on WebAuthn: the browser +stores a cryptographic key pair on the device and the server stores only the public half. To sign +in, the device proves possession of the private key — there is no shared secret to phish, reuse or +leak. + +This sample adds passkey registration and sign-in to a Duende IdentityServer that uses ASP.NET Core +Identity for its user store. + +### What the sample demonstrates + +- `options.Stores.SchemaVersion = IdentitySchemaVersions.Version3` on `AddIdentity`, which is the Identity schema version that introduces the built-in passkey store + (it creates an `AspNetUserPasskeys` table) +- `app.MapPasskeyEndpoints()`, which maps two endpoints: + - `POST /Account/PasskeyCreationOptions` + - `POST /Account/PasskeyRequestOptions` +- `builder.Services.Configure(...)` with a `ValidateOrigin` predicate, + restricted in Development to `https://localhost:5001`. WebAuthn requires the origin to be checked + explicitly, and this is where you would allow your real production origins +- a custom `` **TagHelper** (`PasskeySubmitTagHelper`) that renders a button which + calls the WebAuthn API in the browser and posts the resulting credential JSON back to the server + as a form field +- `PasskeyOperation.Create` and `PasskeyOperation.Request` — the two operations the tag helper drives +- a passkey management page at `/Account/Passkeys` where a signed-in user can add, rename and remove + their own passkeys +- `AddAspNetIdentity()` to bridge ASP.NET Core Identity into IdentityServer +- `AddLicenseSummary()`, which prints the license edition to the console at startup + +> **Note:** passkeys require a **secure context**. `https://localhost` counts as one, so the sample +> works over local HTTPS, but it will not work over plain `http://` on another machine. + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project AspNetIdentityPasskeys.AppHost + ``` + + The Aspire dashboard opens at `https://localhost:17237` and shows two resources: `identityserver` + and `client`. + +1. Click the highlighted **Seed Database** button on the `identityserver` resource and wait for it to + report success. You must do this before signing in, or there will be no users. + + > Seeding calls `Database.Migrate()` only — it does **not** drop the database. That is + > deliberate here: passkeys registered in a previous run survive a re-seed, so you can keep + > experimenting with the credential you created. + +1. Open a browser tab to the `client` application at `https://localhost:5002`. Click **Secure** in + the navigation; you are redirected to the IdentityServer login page at `https://localhost:5001`. +1. Sign in with a **password** first: username `alice` (or `bob`) and password `Pass123$`. Note the + username is what you type, not the email address. +1. You are returned to the client. Now sign out (the **Logout** button in the navigation) so you can + exercise the passwordless flow. +1. On the login page, leave the username field filled in and click **Log in with a passkey**. The + browser prompts you for the device's biometric or PIN. You are signed in without a password. +1. To register a passkey, sign in and go straight to `https://localhost:5001/account/passkeys` (there + is a link to it on the IdentityServer home page). Click **Add a new passkey** and follow the + browser prompt. +1. On that page you can also **Rename** a passkey to something friendlier or **Remove** it. Removing + the last passkey does not lock you out — the password still works. +1. The `client` is a **confidential** client: it authenticates with the secret + `49C1A7E1-0C79-4A89-A3D6-A37998FB86B0` in addition to the user, because it also holds a refresh + token. + +## Credentials + +| Username | Email | Password | +|----------|-------|----------| +| `alice` | `AliceSmith@email.example` | `Pass123$` | +| `bob` | `BobSmith@email.example` | `Pass123$` | + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `IdentityServerAspNetIdentityPasskeys` | `https://localhost:5001` | Login, consent, passkey registration and sign-in | +| `Client` | `https://localhost:5002` | The web application that signs in | +| `AspNetIdentityPasskeys.AppHost` | `https://localhost:17237` | Aspire dashboard, with the **Seed Database** button | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) +- A browser that supports WebAuthn — any current Chrome, Edge, Safari or Firefox will do + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `IdentityServerAspNetIdentityPasskeys/HostingExtensions.cs` — the Identity schema version, the + `ValidateOrigin` predicate and `MapPasskeyEndpoints()` +- `IdentityServerAspNetIdentityPasskeys/Passkeys/PasskeyEndpointRouteBuilderExtensions.cs` — the two + WebAuthn option endpoints +- `IdentityServerAspNetIdentityPasskeys/Passkeys/PasskeySubmitTagHelper.cs` — the `` + tag helper +- `IdentityServerAspNetIdentityPasskeys/wwwroot/js/passkey-submit.js` — the browser-side WebAuthn call +- `IdentityServerAspNetIdentityPasskeys/Pages/Account/Passkeys.cshtml` — the management UI +- `IdentityServerAspNetIdentityPasskeys/SeedData.cs` — the user seeding diff --git a/IdentityServer/v7/DPoP/README.md b/IdentityServer/v7/DPoP/README.md new file mode 100644 index 000000000..68330bd0f --- /dev/null +++ b/IdentityServer/v7/DPoP/README.md @@ -0,0 +1,133 @@ +# DPoP (Demonstration of Proof of Possession) sample + +[DPoP](https://datatracker.ietf.org/doc/html/rfc9449) (Demonstration of Proof of Possession) binds +an access token to the cryptographic key that requested it. Instead of sending a bearer token that +anyone who obtains it can replay, the client must also send a freshly signed proof over a key it +holds. If the token is stolen, the thief cannot use it — they do not have the private key. + +This sample shows DPoP on both halves of the flow: acquiring tokens at the token endpoint, and +presenting them at a protected API. + +### What the sample demonstrates + +**Token issuance — the `dpop` client** (registered in `IdentityServerHost\src\Clients.cs`) + +- `RequireDPoP = true` on the client, which makes IdentityServer reject any token request that does + not carry a valid DPoP proof +- `AllowedGrantTypes = GrantTypes.CodeAndClientCredentials` — DPoP works with both the interactive + code flow and the machine-to-machine flow, and this sample exercises both +- a client secret (`905e4892-7610-44cb-a122-6209b38c882f`) in addition to the DPoP key; the two + authenticate different things + +**The interactive client** (`WebClient`) + +- `AddOpenIdConnectAccessTokenManagement()` combined with + `options.DPoPJsonWebKey = DPoPProofKey.Parse(...)` — an RSA-2048 JWK with `Alg = "PS256"`, which + is the proof key +- code flow with PKCE, and `options.OnSigningOut` calling `RevokeRefreshTokenAsync()` so the refresh + token is revoked on sign-out +- `AddUserAccessTokenHttpClient("client", ...)` pointing at the API; the proof is attached + automatically on each request +- a `Secure` page with a **Renew Tokens** button, a `Call API` page, and a `Renew` action that forces + token renewal (`ForceTokenRenewal = true`) + +**The machine-to-machine client** (`ClientCredentials`) + +- `AddClientCredentialsTokenManagement().AddClient("dpop", ...)` with `client.DPoPProofKey` set +- `AddClientCredentialsHttpClient("client", ...)` — a `BackgroundService` that calls the API every + five seconds, so you can watch the DPoP round trip repeatedly without touching the browser + +**The API** (`Api`) + +- `options.TokenValidationParameters.ValidTypes = ["at+jwt"]`, so only an access token is accepted +- `ConfigureDPoPTokensForScheme("token", ...)`, layering DPoP validation on top of the plain JWT + bearer scheme +- `opt.ProofTokenExpirationMode = DPoPProofExpirationMode.IssuedAt`, with the alternative `Nonce` + mode explained in a comment +- a keyed `HybridCache` registered as the proof-token replay cache + (`ServiceProviderKeys.ProofTokenReplayHybridCache`) and backed by a distributed memory cache — + this is what stops an attacker re-sending a captured proof +- `options.MapInboundClaims = false` and `ValidateAudience = false`, because the sample uses API + scopes rather than an API resource + +> **Note:** this sample does **not** ship its own IdentityServer project. It reuses the shared +> `IdentityServerHost` at the root of the v7 tree, which is also used by the other samples in this +> version. If you change the `dpop` client in `Clients.cs` while another sample is running, you +> affect them too. + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project DPoP.AppHost + ``` + + The Aspire dashboard opens at `https://localhost:17007` and shows four resources: + `identityserverhost`, `api`, `webclient` and `clientcredentials`. + +1. Open a browser tab to the `webclient` at `https://localhost:5010`. +1. Sign in. The IdentityServer login page appears at `https://localhost:5001`; log in with + `alice` / `alice` (or `bob` / `bob`) and approve the request. +1. You are returned to the WebClient. The home page is anonymous. Use the **Secure** link in the + navigation to trigger the interactive flow. +1. Click **Call API** in the navigation. The client attaches the access token *and* a DPoP proof to + `GET https://localhost:5005/identity`, and the API echoes back the token claims. +1. Open the browser dev tools, go to **Network**, and inspect the request headers. Alongside + `Authorization: DPoP ` you will find a `DPoP` header containing the signed proof JWT. This + is the visible difference from a plain bearer flow, where the header would just be `Bearer`. +1. Go back to **Secure** and click **Renew Tokens**. This forces the access token to be refreshed + (`ForceTokenRenewal = true`); call the API again and watch the new token and proof in the network + tab. +1. Watch the `clientcredentials` resource logs in the Aspire dashboard. Every five seconds it obtains + a DPoP-bound token and calls the API, logging the result. This is the client credentials half of + the sample running unattended. +1. To see the replay protection work, capture a request from the network tab and replay the exact + same `Authorization` and `DPoP` headers with `curl`. The API rejects it, because the proof has + already been seen in the replay cache. + +## Credentials + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +The `ClientCredentials` client authenticates as a machine, not a user: + +| Client id | Client secret | Scope | +|-----------|---------------|-------| +| `dpop` | `905e4892-7610-44cb-a122-6209b38c882f` | `scope1` | + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `WebClient` | `https://localhost:5010` | Interactive client, code flow + DPoP | +| `IdentityServerHost` | `https://localhost:5001` | Shared v7 IdentityServer; registers the `dpop` client | +| `Api` | `https://localhost:5005` | Protected API, DPoP-validated | +| `ClientCredentials` | _(console)_ | Machine-to-machine DPoP client, polls every 5s | +| `DPoP.AppHost` | `https://localhost:17007` | Aspire dashboard | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) + +No database or seeding step is required — the configuration is in-memory and the test users are +registered with `AddTestUsers`. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `..\IdentityServerHost\src\Clients.cs` — the `dpop` client with `RequireDPoP = true` +- `WebClient/Program.cs` — the `DPoPJsonWebKey` and the token management setup +- `WebClient/Controllers/HomeController.cs` — the `Secure`, `CallApi` and `Renew` actions +- `ClientCredentials/Program.cs` — `AddClientCredentialsTokenManagement` with a DPoP key +- `Api/Program.cs` — `ValidTypes`, `ConfigureDPoPTokensForScheme` and the replay cache +- `WebClient/TokenResponseExtensions.cs` — pretty-printing the API response diff --git a/IdentityServer/v7/Diagnostics/Aspire/README.md b/IdentityServer/v7/Diagnostics/Aspire/README.md new file mode 100644 index 000000000..afc1d36f8 --- /dev/null +++ b/IdentityServer/v7/Diagnostics/Aspire/README.md @@ -0,0 +1,103 @@ +# Diagnostics with Aspire dashboard sample + +This sample shows how to instrument Duende IdentityServer with +[OpenTelemetry](https://docs.duendesoftware.com/identityserver/observability/) so that traces, +metrics and logs flow into a central backend — here, the .NET Aspire dashboard, which ships with a +built-in OpenTelemetry collector and viewer. + +It also demonstrates the Aspire service defaults pattern: a shared `Aspire.ServiceDefaults` project +wiring up service discovery, resilience, health checks and telemetry once, so each service only has +to call `builder.AddServiceDefaults()`. + +### What the sample demonstrates + +- a shared `Aspire.ServiceDefaults` project providing: + - `AddServiceDiscovery()` and `AddStandardResilienceHandler()` on `HttpClient` + - `AddOpenTelemetry()` for traces, metrics and logs, with the OTLP exporter wired to the Aspire + dashboard + - `AddMeter("Duende.IdentityServer.Experimental")`, which subscribes to the counters Duende + IdentityServer itself publishes + - `AddHealthChecks()` plus `MapDefaultEndpoints()`, exposing `/health` and `/alive` +- a local `Aspire.ServiceDefaults` copy (rather than the shared one at the v7 root) so the wiring is + visible in a single place +- **custom application metrics** in `IdentityServer/Pages/Telemetry.cs` — a `System.Diagnostics.Metrics` + `Meter` with four counters, recorded from the UI code that a real application owns: + - `tokenservice.user_login`, tagged with the client id and identity provider + - `tokenservice.consent`, tagged with client, scope, remember flag and granted/denied + - `tokenservice.grants_revoked` + - `tokenservice.user_logout` +- the calls to those counters from the login, consent, external-login callback and grants pages +- Aspire's service discovery: `Aspire.Web` reads its authority from + `builder.Configuration["services:identityserver:https:0"]` and calls the API at + `https://apiservice` rather than a hard-coded host and port +- a three-tier topology: `Aspire.Web` (Razor Pages frontend) → `Aspire.ApiService` (protected API) → + `IdentityServer` + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project Aspire.AppHost + ``` + + The Aspire dashboard opens at `https://localhost:15264` and shows four resources: `identityserver`, + `apiservice`, `webfrontend` and the `Aspire.AppHost` orchestrator. + +1. Open a browser tab to the `webfrontend` at `https://localhost:5014`. You are redirected to the + IdentityServer at `https://localhost:5001`. +1. Log in with `alice` / `alice` and approve the consent request. The web app then calls the API + service and renders the weather forecast. +1. Go back to the Aspire dashboard and open the **Metrics** view for the `IdentityServer` resource. + Search for `tokenservice` and you should see the counters your login and consent just recorded. +1. Open the **Traces** view and look at the waterfall for the sign-in request. You can see the + `Duende.IdentityServer` activity spans, the outgoing HTTP calls from `Aspire.Web` to + `apiservice`, and the validation steps inside the token endpoint. +1. Open the **Logs** view on `IdentityServer` to see structured log records correlated by trace id. +1. Check `/health` and `/alive` on any resource to see the health checks that `AddServiceDefaults` + registered. +1. Trigger a new metric by revoking a grant on the **Grants** page, then refresh the Metrics view to + see `tokenservice.grants_revoked` increment. + +## Credentials + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `Aspire.AppHost` | `https://localhost:15264` | Aspire dashboard, orchestration and topology | +| `IdentityServer` | `https://localhost:5001` | Token service; publishes the custom `tokenservice.*` metrics | +| `Aspire.ApiService` | `https://localhost:5325` | Protected weather API | +| `Aspire.Web` | `https://localhost:5014` | Razor Pages frontend, calls the API | +| `Aspire.ServiceDefaults` | _(library)_ | Shared telemetry, discovery, resilience and health checks | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) + +No database or seeding step is required — the configuration is in-memory and the test users are +registered with `AddTestUsers`. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `Aspire.ServiceDefaults/Extensions.cs` — the entire telemetry, discovery and health check wiring +- `IdentityServer/Pages/Telemetry.cs` — the custom `Meter` and the four counters +- `IdentityServer/Pages/Account/Login/Index.cshtml.cs` — where `UserLogin` is recorded +- `IdentityServer/Pages/Consent/Index.cshtml.cs` — where `ConsentGranted` / `ConsentDenied` are recorded +- `Aspire.AppHost/Program.cs` — the resource graph and `.WithReference(...)` wiring +- `Aspire.Web/Program.cs` — service discovery via `Configuration["services:identityserver:https:0"]` + +For a plain console/OpenTelemetry setup with no Aspire, see the sibling +[`Otel`](../Otel) sample. diff --git a/IdentityServer/v7/Diagnostics/Otel/README.md b/IdentityServer/v7/Diagnostics/Otel/README.md new file mode 100644 index 000000000..339fca2fc --- /dev/null +++ b/IdentityServer/v7/Diagnostics/Otel/README.md @@ -0,0 +1,97 @@ +# Diagnostics with OpenTelemetry sample + +This is the minimal version of the diagnostics story: a single Duende IdentityServer project wired +up to OpenTelemetry with a **console exporter**, so you can see the traces IdentityServer emits +without needing a collector, a dashboard or any infrastructure. + +If you want metrics, logs, service discovery and a UI to explore them, use the sibling +[`Aspire`](../Aspire) sample instead. + +### What the sample demonstrates + +- `AddOpenTelemetry()` with `AddService("IdentityServerHost.Sample")` to name the resource +- subscribing to all five built-in IdentityServer trace sources via + `IdentityServerConstants.Tracing`: + - `Basic` — request and response lifecycle + - `Cache` — client and resource cache hits and misses + - `Services` — the services IdentityServer calls (profile, claims, keys) + - `Stores` — configuration and operational store access + - `Validation` — token, authorize and introspection request validation +- `AddConsoleExporter()` for tracing, so spans are printed rather than shipped anywhere +- `AddHttpClientInstrumentation()`, `AddAspNetCoreInstrumentation()` and + `AddSqlClientInstrumentation()` to correlate IdentityServer spans with the rest of the request +- `options.Events.RaiseErrorEvents`, `RaiseInformationEvents`, `RaiseFailureEvents` and + `RaiseSuccessEvents` all set to `true`, so the built-in IdentityServer events are raised +- `app.UseSerilogRequestLogging()` for request logging alongside the traces +- an external identity provider, so the login page offers "Sign-in with + demo.duendesoftware.com" in addition to local login + +> **Note:** the client registrations in `Config.cs` use redirect URIs on `https://localhost:44300`, +> but this project runs on `https://localhost:5001` and hosts no client application. That mismatch +> only matters if you try to use a registered client against this host; for tracing the sign-in flow +> it is harmless. + +> **Note:** this sample configures **tracing only**. There is no `.AddMeter(...)` call and no custom +> metrics — the `tokenservice.*` counters live in the [`Aspire`](../Aspire) sample. + +### How to Run + +1. Run the project: + + ```bash + dotnet run --project src + ``` + + > There is no Aspire AppHost in this sample. `src` is a standalone web project. + + The application listens on `https://localhost:5001`. + +1. Open a browser tab to `https://localhost:5001`. The home page is anonymous; you will be + redirected to the login page. +1. Log in with `alice` / `alice` (or `bob` / `bob`). +1. Watch the console. Every request now produces `Activity` spans written to stdout, including: + - the ASP.NET Core request span + - IdentityServer's own spans for the login and authorization handling + - the `Validation` spans for the token requests +1. Exercise the flows and watch new spans appear: + - click **Grants** to list the grants for the current user + - click **Diagnostics** to open the developer diagnostics page, then sign in as `alice` and issue + a token to generate a full request trace +1. The console output is the raw OpenTelemetry format. To explore the same data in a UI, switch the + console exporter for `tracing.AddOtlpExporter()` (the line is already present, commented out, in + `src/HostingExtensions.cs`) and point it at a collector. + +## Credentials + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +Test users are registered in-memory with `AddTestUsers`. + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `src` | `https://localhost:5001` | The IdentityServer; writes traces to the console | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` + +No database, seeding step or Aspire workload is required. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `src/HostingExtensions.cs` — the whole OpenTelemetry setup +- `src/Config.cs` — the in-memory client, resource and scope configuration +- `src/Pages/TestUsers.cs` — the `alice` and `bob` users + +For the full Aspire-based version with metrics and a dashboard, see [`Aspire`](../Aspire). diff --git a/IdentityServer/v7/Quickstarts/1_ClientCredentials/README.md b/IdentityServer/v7/Quickstarts/1_ClientCredentials/README.md new file mode 100644 index 000000000..4950db491 --- /dev/null +++ b/IdentityServer/v7/Quickstarts/1_ClientCredentials/README.md @@ -0,0 +1,82 @@ +# Client Credentials sample + +This sample shows how to use the OAuth 2.0 **client credentials** grant. This is a machine-to-machine +flow: there is no browser and no user, so the client authenticates with a client id and secret and +receives an access token it can use to call a protected API. + +### What the sample demonstrates + +- a client registered with `AllowedGrantTypes = GrantTypes.ClientCredentials` and a hashed + `ClientSecrets` entry (`new Secret("secret".Sha256())`) +- a protected API scope (`api1`) that the client is allowed to request +- client authentication at the token endpoint using HTTP Basic-style client id/secret credentials +- API authorization via a policy that requires the `scope` claim to contain `api1` +- JWT bearer validation against the IdentityServer's discovery metadata +- using `Duende.IdentityModel` (`GetDiscoveryDocumentAsync`, `RequestClientCredentialsTokenAsync`, + `SetBearerToken`) to run the whole exchange from a console app + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project src/AppHost + ``` + + > `AppHost` is deliberately **not** a member of `Quickstart.sln`, so `--project` is required. + + The Aspire dashboard opens at `https://localhost:17236` and lists three resources: `identityserver`, + `api` and `client`. + +1. Click the `client` resource in the dashboard. It is a console application, so its output appears in + the dashboard logs. It runs once and then exits. + +1. To watch it again, either restart the `client` resource or run it directly: + + ```bash + dotnet run --project src/Client + ``` + + It prints the access token it received, then calls the API and prints the identity claims returned + by the `/identity` endpoint. + +1. To see a failure instead, stop the `identityserver` resource and run `src/Client` again — discovery + will fail and the client prints the error. + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `IdentityServer` | `https://localhost:5001` | Issues tokens | +| `Api` | `https://localhost:6001` | Protected API, `GET /identity` | +| `Client` | _(console)_ | Requests a token and calls the API | +| `AppHost` | `https://localhost:17236` | Aspire dashboard | + +## Credentials + +There are no users in this sample — the client credentials grant has no interactive user. + +| Client id | Client secret | Scope | +|-----------|---------------|-------| +| `client` | `secret` | `api1` | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) + +No database or seeding step is required — the configuration is in-memory. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `src/IdentityServer/Config.cs` — the `client` registration +- `src/Client/Program.cs` — the entire token request, end to end +- `src/Api/Program.cs` — the `ApiScope` authorization policy + +For an interactive equivalent, see quickstart [`2_InteractiveAspNetCore`](../2_InteractiveAspNetCore). diff --git a/IdentityServer/v7/Quickstarts/2_InteractiveAspNetCore/README.md b/IdentityServer/v7/Quickstarts/2_InteractiveAspNetCore/README.md new file mode 100644 index 000000000..348878b13 --- /dev/null +++ b/IdentityServer/v7/Quickstarts/2_InteractiveAspNetCore/README.md @@ -0,0 +1,86 @@ +# Interactive ASP.NET Core sample + +This sample shows how to use the OAuth 2.0 **authorization code** grant with an interactive ASP.NET +Core web application — the most common way for a web app to obtain tokens on behalf of a signed-in +user. It also registers a client credentials client, so you can compare the two flows side by side. + +### What the sample demonstrates + +- a client registered with `AllowedGrantTypes = GrantTypes.Code` and a client secret (a confidential + client) +- an `AddOpenIdConnect` handler configured for code flow, which turns on PKCE automatically +- a custom identity resource (`verification`) carrying the `email` and `email_verified` claims +- `options.GetClaimsFromUserInfoEndpoint = true` so those claims come from the userinfo endpoint + rather than being baked into the id token +- storing the tokens in the ASP.NET Core authentication session with `options.SaveTokens = true` +- registering an external identity provider so the login page offers "Sign-in with + demo.duendesoftware.com" +- an API (`api1`) that is authorized with a policy requiring the `scope` claim + +> **Note:** this quickstart does **not** call the API. The `api1` scope is not requested by the `web` +> client, and there is no "Call API" button. Doing that is the subject of +> [`3_AspNetCoreAndApis`](../3_AspNetCoreAndApis). + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project src/AppHost + ``` + + > `AppHost` is deliberately **not** a member of `Quickstart.sln`, so `--project` is required. + + The Aspire dashboard opens at `https://localhost:17236`. + +1. Open a browser tab to the `web` application at `https://localhost:5002`. You are redirected + immediately to the IdentityServer at `https://localhost:5001`. +1. Log in with `alice` / `alice` (or `bob` / `bob`). +1. Approve the consent request. You are returned to the `web` application home page, which lists the + claims from your session. +1. Click **Signout** in the navigation to see the RP-initiated logout flow, which also runs the + front-channel sign-out against IdentityServer. + +## Credentials + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +A `client` credentials client (`client` / `secret`, scope `api1`) is also registered, but nothing in +this sample exercises it. + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `IdentityServer` | `https://localhost:5001` | Login, consent and token issuance | +| `WebClient` | `https://localhost:5002` | The interactive web application (`client` id `web`) | +| `Api` | `https://localhost:6001` | Protected API, present but not called by this quickstart | +| `Client` | _(console)_ | Client credentials demo, independent of the web app | +| `AppHost` | `https://localhost:17236` | Aspire dashboard | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) + +No database or seeding step is required — the configuration is in-memory and the test users are +registered with `AddTestUsers`. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `src/IdentityServer/Config.cs` — the `web` client and the `verification` identity resource +- `src/IdentityServer/Pages/TestUsers.cs` — the `alice` and `bob` users +- `src/WebClient/Program.cs` — the `AddOpenIdConnect` configuration + +To call a protected API from the web app, continue with +[`3_AspNetCoreAndApis`](../3_AspNetCoreAndApis). To get automatic token refresh, see +[`3a_TokenManagement`](../3a_TokenManagement). diff --git a/IdentityServer/v7/Quickstarts/3_AspNetCoreAndApis/README.md b/IdentityServer/v7/Quickstarts/3_AspNetCoreAndApis/README.md new file mode 100644 index 000000000..45e845e5d --- /dev/null +++ b/IdentityServer/v7/Quickstarts/3_AspNetCoreAndApis/README.md @@ -0,0 +1,86 @@ +# ASP.NET Core and APIs sample + +This sample extends [`2_InteractiveAspNetCore`](../2_InteractiveAspNetCore) by adding a protected +web API and calling it from the signed-in web application using the access token that the +authorization code flow produced. + +### What the sample demonstrates + +- the same code flow client as quickstart 2, now requesting the `api1` API scope **in addition** to + `openid`, `profile` and the custom `verification` resource +- `options.SaveTokens = true`, so the access token is retained in the authentication session +- manually reading the access token out of the session and attaching it as a bearer token to an + outgoing `HttpClient` request +- a protected API that validates the token with `AddJwtBearer` against the IdentityServer's + authority, and authorizes with a policy requiring the `scope` claim to contain `api1` +- `options.TokenValidationParameters.ValidateAudience = false`, because the quickstart uses an API + scope rather than an API resource, so there is no audience to validate + +> **Note:** this quickstart reads the token by hand. There is no automatic caching or refreshing. +> [`3a_TokenManagement`](../3a_TokenManagement) shows how Duende AccessTokenManagement does this for +> you. + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project src/AppHost + ``` + + > `AppHost` is deliberately **not** a member of `Quickstart.sln`, so `--project` is required. + + The Aspire dashboard opens at `https://localhost:17236`. + +1. Open a browser tab to the `web` application at `https://localhost:5002`. You are redirected to the + IdentityServer at `https://localhost:5001`. +1. Log in with `alice` / `alice` (or `bob` / `bob`) and approve the consent request. Note that this + time the consent screen also lists **My API** (`api1`). +1. You are returned to the `web` application. Click **Call API**. +1. The web app attaches the access token to a `GET https://localhost:6001/identity` request, and the + API echoes back the claims from the token — including the `client_id` and `scope` claims that + IdentityServer added. +1. Try it again after signing out and back in, or change the API to reject the request to see how the + failure surfaces. + +## Credentials + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `IdentityServer` | `https://localhost:5001` | Login, consent and token issuance | +| `WebClient` | `https://localhost:5002` | The interactive web application | +| `Api` | `https://localhost:6001` | Protected API, `GET /identity` | +| `Client` | _(console)_ | Client credentials demo, independent of the web app | +| `AppHost` | `https://localhost:17236` | Aspire dashboard | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) + +No database or seeding step is required — the configuration is in-memory and the test users are +registered with `AddTestUsers`. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `src/IdentityServer/Config.cs` — the `api1` scope and the `web` client +- `src/WebClient/Program.cs` — `SaveTokens` and the `api1` scope request +- `src/WebClient/Pages/Index.cshtml.cs` — reading the token out of the session +- `src/Api/Program.cs` — the `ApiScope` authorization policy + +For automatic token caching and refresh, continue with +[`3a_TokenManagement`](../3a_TokenManagement). For persistent storage, see +[`4_EntityFramework`](../4_EntityFramework). diff --git a/IdentityServer/v7/Quickstarts/3a_TokenManagement/README.md b/IdentityServer/v7/Quickstarts/3a_TokenManagement/README.md new file mode 100644 index 000000000..fa891ea5c --- /dev/null +++ b/IdentityServer/v7/Quickstarts/3a_TokenManagement/README.md @@ -0,0 +1,89 @@ +# Access Token Management sample + +This sample shows how to use [Duende AccessTokenManagement](https://docs.duendesoftware.com/identityserver/access-token-management/) +so the web application never has to juggle access tokens itself. Instead of reading a token out of +the session and attaching it by hand, you declare a named `HttpClient` and the library acquires, +caches, refreshes and attaches the token for you. + +It builds on [`3_AspNetCoreAndApis`](../3_AspNetCoreAndApis), which does the same call manually. + +### What the sample demonstrates + +- requesting `offline_access` so the authorization code flow returns a refresh token +- `AllowOfflineAccess = true` on the `web` client +- `AddOpenIdConnectAccessTokenManagement()` to enable the token cache and the refresh logic +- `AddUserAccessTokenHttpClient("apiClient", ...)` — a named `HttpClient` that transparently carries + the user's access token +- `GetUserAccessTokenAsync()` for the cases where you want the token itself rather than a client + +> **Note:** the `web` client here does not override `AccessTokenLifetime`, so access tokens last for the +> Duende default of one hour. You will therefore usually not observe a refresh while clicking around. +> To watch the library actually refresh, sign in, wait more than an hour, then call the API again — +> the refresh happens transparently and the API keeps returning 200. (The +> [`MvcTokenManagement` Basics sample](../../Basics/MvcTokenManagement/README.md) registers a second +> client with a deliberately short lifetime if you want to see this quickly.) + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project src/AppHost + ``` + + > `AppHost` is deliberately **not** a member of `Quickstart.sln`, so `--project` is required. + + The Aspire dashboard opens at `https://localhost:17236`. + +1. Open a browser tab to the `web` application at `https://localhost:5002`. You are redirected to the + IdentityServer at `https://localhost:5001`. +1. Log in with `alice` / `alice` (or `bob` / `bob`) and approve the consent request. The consent + screen also lists **Offline Access** alongside **My API**. +1. You are returned to the `web` application. The home page lists your claims and the properties of + your authentication session. +1. Click **Call API**. The page creates the named `apiClient` and calls + `GET https://localhost:6001/identity`; the access token is attached for you. +1. Compare this with quickstart 3: there the page did + `await HttpContext.GetTokenAsync("access_token")` and set the `Authorization` header itself. The + equivalent code is still present, commented out, in + `src/WebClient/Pages/CallApi.cshtml.cs` — swap the two to compare. + +## Credentials + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `IdentityServer` | `https://localhost:5001` | Login, consent and token issuance | +| `WebClient` | `https://localhost:5002` | The interactive web application | +| `Api` | `https://localhost:6001` | Protected API, `GET /identity` | +| `Client` | _(console)_ | Client credentials demo, independent of the web app | +| `AppHost` | `https://localhost:17236` | Aspire dashboard | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) + +No database or seeding step is required — the configuration is in-memory and the test users are +registered with `AddTestUsers`. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `src/WebClient/Program.cs` — `AddOpenIdConnectAccessTokenManagement` and the named `HttpClient` +- `src/WebClient/Pages/CallApi.cshtml.cs` — the managed call, next to the commented-out manual one +- `src/IdentityServer/Config.cs` — `AllowOfflineAccess` on the `web` client + +For persistent storage of clients, resources and grants, see +[`4_EntityFramework`](../4_EntityFramework). diff --git a/IdentityServer/v7/Quickstarts/4_EntityFramework/README.md b/IdentityServer/v7/Quickstarts/4_EntityFramework/README.md new file mode 100644 index 000000000..a4673bb8a --- /dev/null +++ b/IdentityServer/v7/Quickstarts/4_EntityFramework/README.md @@ -0,0 +1,90 @@ +# Entity Framework Core storage sample + +Up to now the quickstarts have kept clients, resources and grants in memory, which means they are +recreated every time the process restarts. This sample moves both the **configuration** data +(clients, identity resources, API scopes) and the **operational** data (authorization codes, refresh +tokens, persisted grants, consent) into SQLite using Entity Framework Core. + +### What the sample demonstrates + +- `AddConfigurationStore()` — clients, resources and CORS settings read from the database +- `AddOperationalStore()` — codes, tokens, consents and device flow data read from the database +- pointing both stores at a single SQLite file via `options.ConfigureDbContext` and + `b.UseSqlite(connectionString, sql => sql.MigrationsAssembly(...))`, with the connection string + hard-coded as `Data Source=Duende.IdentityServer.Quickstart.EntityFramework.db` +- seeding with the `Duende.IdentityServer.EntityFramework` mappers (`client.ToEntity()`) and + `Duende.IdentityServer.EntityFramework.DbContexts` (`ConfigurationDbContext`, + `PersistedGrantDbContext`) +- a separate `Data/Migrations` directory so the store migrations are not confused with your own EF + migrations +- `Database.Migrate()` plus idempotent seeding at startup, so the database is created and populated + automatically — there is no manual step +- Serilog request logging, so you can watch token endpoint traffic +- an *optional* Google external provider, enabled only when `Authentication:Google:ClientId` and + `Authentication:Google:ClientSecret` are present in configuration (they are not set by default) + +> **Note:** seeding is guarded by `if (!context.Clients.Any())` and so on. If you change `Config.cs` +> after the first run, the database already has rows and your changes are ignored. Delete +> `src/IdentityServer/Duende.IdentityServer.Quickstart.EntityFramework.db` and restart to pick them up. + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project src/AppHost + ``` + + > `AppHost` is deliberately **not** a member of `Quickstart.sln`, so `--project` is required. + + The Aspire dashboard opens at `https://localhost:17236`. + +1. Open a browser tab to the `web` application at `https://localhost:5002`. You are redirected to the + IdentityServer at `https://localhost:5001`. +1. Log in with `alice` / `alice` (or `bob` / `bob`) and approve the consent request. +1. Click **Call API** to make an authorized call to `https://localhost:6001/identity`. +1. Stop the `identityserver` resource, then start it again. You stay logged in — the session and the + refresh token survived the restart, because they are now in the database rather than in memory. + That is the main thing this quickstart is demonstrating. +1. Look at `src/IdentityServer/Duende.IdentityServer.Quickstart.EntityFramework.db` (created on first + run, and git-ignored) with any SQLite browser to see the rows. + +## Credentials + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +Test users are registered with `AddTestUsers`, which remains in-memory — only the IdentityServer +configuration and operational stores moved to the database. + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `IdentityServer` | `https://localhost:5001` | Login, consent and token issuance; owns the SQLite database | +| `WebClient` | `https://localhost:5002` | The interactive web application | +| `Api` | `https://localhost:6001` | Protected API, `GET /identity` | +| `Client` | _(console)_ | Client credentials demo, independent of the web app | +| `AppHost` | `https://localhost:17236` | Aspire dashboard | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `src/IdentityServer/HostingExtensions.cs` — the store configuration and `InitializeDatabase` +- `src/IdentityServer/Data/Migrations` — the store migrations +- `src/IdentityServer/Config.cs` — the configuration that gets seeded + +To use ASP.NET Core Identity for users instead of test users, see +[`5_AspNetIdentity`](../5_AspNetIdentity). diff --git a/IdentityServer/v7/Quickstarts/5_AspNetIdentity/README.md b/IdentityServer/v7/Quickstarts/5_AspNetIdentity/README.md new file mode 100644 index 000000000..086adbaca --- /dev/null +++ b/IdentityServer/v7/Quickstarts/5_AspNetIdentity/README.md @@ -0,0 +1,92 @@ +# ASP.NET Core Identity sample + +The previous quickstarts used `AddTestUsers`, which is an in-memory user store intended for demos. +This sample replaces it with **ASP.NET Core Identity** backed by SQLite, which is what you would +actually use in a real application — you get password hashing, account lockout, email confirmation +and roles for free. + +### What the sample demonstrates + +- `AddIdentity().AddEntityFrameworkStores()` + with a SQLite `ApplicationDbContext` +- `AddAspNetIdentity()`, which bridges ASP.NET Core Identity into IdentityServer so + the login page and user management work against the Identity user store +- a custom `ApplicationUser` with an extra `FavoriteColor` property +- `AddProfileService()`, deriving from `AspNetIdentityProfileService` to add a + `favorite_color` claim when the user has one +- a `color` identity resource (`new IdentityResource("color", new[] { "favorite_color" })`) that + makes that claim requestable, and which the `web` client is allowed to ask for +- a "Seed Database" command on the Aspire dashboard resource that runs the IdentityServer with + `/seed` to migrate and populate the user database + +> **Note:** seeding calls `EnsureDeleted()` before `Migrate()`, so **every seed run drops the +> database** and recreates the users. Any user you register yourself is lost. This is deliberate for +> a sample — it guarantees you always start from the documented accounts. + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project src/AppHost + ``` + + > `AppHost` is deliberately **not** a member of `Quickstart.sln`, so `--project` is required. + + The Aspire dashboard opens at `https://localhost:17236`. + +1. Click the highlighted **Seed Database** button on the `identityserver` resource. Wait for it to + report success. You must do this **before** signing in, or there will be no users and the login + page will reject every attempt. +1. Open a browser tab to the `web` application at `https://localhost:5002`. You are redirected to the + IdentityServer at `https://localhost:5001`. +1. Log in with `alice` / `Pass123$` (or `bob` / `Pass123$`) and approve the consent request. The + consent screen lists **My API** (`api1`) and the custom **color** resource. +1. You are returned to the `web` application. The home page lists the claims, and you should see + `favorite_color` with the value `red`, alongside `email` and `email_verified`. +1. Click **Signout**, then sign in as `bob` to see the claim **disappear** — Bob is seeded without a + `FavoriteColor`, and `CustomProfileService` only adds the claim when the value is non-empty. +1. Sign in once more and watch the `AspIdUsers.db` file in + `src/IdentityServerAspNetIdentity/` to see the hashed passwords in the `AspNetUsers` table. + +## Credentials + +| Username | Email | Password | `favorite_color` | +|----------|-------|----------|------------------| +| `alice` | `AliceSmith@email.example` | `Pass123$` | `red` | +| `bob` | `BobSmith@email.example` | `Pass123$` | _(none)_ | + +The **username** is what you type on the login page, not the email address. + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `IdentityServerAspNetIdentity` | `https://localhost:5001` | Login, consent and token issuance; owns `AspIdUsers.db` | +| `WebClient` | `https://localhost:5002` | The interactive web application | +| `Api` | `https://localhost:6001` | Protected API, registered but not called by this quickstart | +| `Client` | _(console)_ | Client credentials demo, independent of the web app | +| `AppHost` | `https://localhost:17236` | Aspire dashboard, with the **Seed Database** button | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `src/IdentityServerAspNetIdentity/HostingExtensions.cs` — `AddAspNetIdentity` and the profile service +- `src/IdentityServerAspNetIdentity/CustomProfileService.cs` — where `favorite_color` is emitted +- `src/IdentityServerAspNetIdentity/Config.cs` — the `color` identity resource +- `src/IdentityServerAspNetIdentity/SeedData.cs` — the user seeding +- `src/AppHost/AppHost.cs` — the "Seed Database" command + +For a much richer user management sample — registration, password reset, MFA and account lockout — +see the `UserManagement` folder. There is no v7 equivalent of it; the closest v7 material is +[`Basics/MvcBasic`](../../Basics/MvcBasic), which shows the login and logout pages in full. diff --git a/IdentityServer/v7/Quickstarts/6_JS_with_backend/README.md b/IdentityServer/v7/Quickstarts/6_JS_with_backend/README.md new file mode 100644 index 000000000..0359dff90 --- /dev/null +++ b/IdentityServer/v7/Quickstarts/6_JS_with_backend/README.md @@ -0,0 +1,97 @@ +# JavaScript client with a BFF sample + +This sample shows a plain JavaScript single-page application that is protected by a +[Backend-for-Frontend (BFF)](https://docs.duendesoftware.com/identityserver/bff). The browser never +sees an access token: the BFF holds the tokens in a server-side session and proxies API calls, adding +the `X-CSRF` header the API endpoints require. + +> **Note:** the BFF in this sample *is* the `JavaScriptClient` project. It serves the static SPA from +> `wwwroot` **and** acts as the backend, which is why it listens on `5003` rather than the `5002` used +> by the plain `WebClient`. + +### What the sample demonstrates + +- `AddBff()` from `Duende.BFF` to set up the BFF services +- `ConfigureOpenIdConnect(...)` with the `bff` client, requesting `api1` and `offline_access` +- `ConfigureCookies(options => options.Cookie.SameSite = SameSiteMode.Strict)` and the + `X-CSRF: 1` convention that protects against CSRF +- `app.UseBff()` — must sit between `UseAuthentication()` and `UseAuthorization()` +- `app.MapBffManagementEndpoints()`, exposing `/bff/login`, `/bff/logout` and `/bff/user` +- `app.MapRemoteBffApiEndpoint("/remote", ...).WithAccessToken(RequiredTokenType.User)` — a reverse + proxy that forwards `/remote/*` to the API and attaches the user's access token +- `app.MapGet("/local/identity", ...).AsBffApiEndpoint()` — a local endpoint in the BFF itself, + protected the same way +- `app.MapBffManagementEndpoints()`'s `bff:logout_url` claim, which the SPA reads to build its + logout link +- a public client registered with `RequireClientSecret = false` and `AllowedCorsOrigins` + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project src/AppHost + ``` + + > `AppHost` is deliberately **not** a member of `Quickstart.sln`, so `--project` is required. + + The Aspire dashboard opens at `https://localhost:17236`. + +1. Open a browser tab to `https://localhost:5003`. You see four buttons: **Login**, **Call Local + API**, **Call Remote API** and **Logout**. +1. Click **Login**. The SPA navigates to `/bff/login`, which redirects to the IdentityServer at + `https://localhost:5001`. +1. Log in with `alice` / `alice` (or `bob` / `bob`) and approve the consent request. You are returned + to the SPA. +1. Open the browser's network tab and reload. The SPA calls `GET /bff/user` and renders the result. + That response contains the user's claims — but **no access token**. This is the whole point of the + BFF pattern. +1. Click **Call Local API**. The SPA calls `GET /local/identity` with the `X-CSRF: 1` header; the + BFF answers with a local JSON payload naming you. +1. Click **Call Remote API**. The SPA calls `GET /remote/identity`, which the BFF reverse-proxies to + `https://localhost:6001/identity`, attaching the access token server-side. The API echoes the + token claims back. +1. Click **Logout**. The SPA reads the `bff:logout_url` claim and navigates to it, so the session ends + at IdentityServer as well as in the BFF. + +## Credentials + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `IdentityServer` | `https://localhost:5001` | Login, consent and token issuance | +| `JavaScriptClient` | `https://localhost:5003` | The BFF **and** the static SPA | +| `WebClient` | `https://localhost:5002` | A separate ASP.NET Core MVC client, not part of this demo | +| `Api` | `https://localhost:6001` | Protected API, `GET /identity` | +| `Client` | _(console)_ | Client credentials demo, independent of the SPA | +| `AppHost` | `https://localhost:17236` | Aspire dashboard | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) + +No database or seeding step is required — the configuration is in-memory and the test users are +registered with `AddTestUsers`. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `src/JavaScriptClient/Program.cs` — the entire BFF setup +- `src/JavaScriptClient/wwwroot/app.js` — the four button handlers and the `X-CSRF` header +- `src/IdentityServer/Config.cs` — the `bff` client registration + +To see the same SPA holding tokens in the browser instead, see +[`6_JS_without_backend`](../6_JS_without_backend). For Blazor WebAssembly with a BFF, see +[`7_Blazor`](../7_Blazor). diff --git a/IdentityServer/v7/Quickstarts/6_JS_without_backend/README.md b/IdentityServer/v7/Quickstarts/6_JS_without_backend/README.md new file mode 100644 index 000000000..d2d2e57b7 --- /dev/null +++ b/IdentityServer/v7/Quickstarts/6_JS_without_backend/README.md @@ -0,0 +1,95 @@ +# JavaScript client without a backend sample + +This is the counterpart to [`6_JS_with_backend`](../6_JS_with_backend). Here the browser application +talks to IdentityServer and the API **directly**, using the `oidc-client.js` library. There is no +backend, so the access token and refresh token are held in the browser. + +It is the simpler arrangement and the right one for a pure SPA with no server-side secrets to +protect. The trade-off is that a token in `localStorage` or `sessionStorage` is reachable by any +script running on the page, so XSS becomes a token-theft problem. + +### What the sample demonstrates + +- a **public** client: `RequireClientSecret = false`, registered with `AllowedCorsOrigins = + { "https://localhost:5003" }` so the browser origin is allowed to call the endpoints +- authorization code flow **with PKCE** driven by `oidc-client.js`, which is what makes a + secret-less client safe +- a static-file-only ASP.NET Core project — `Program.cs` is just `UseDefaultFiles()`, + `UseStaticFiles()` and `Run()`. The server has no role beyond serving two HTML files and the + JavaScript libraries. +- the SPA settings: `authority: "https://localhost:5001"`, `client_id: "js"`, + `response_type: "code"`, `scope: "openid profile api1"` +- the `callback.html` redirect target that the code flow returns to +- `signinRedirect()` / `signoutRedirect()` and `getUser()` for session handling +- calling `https://localhost:6001/identity` straight from the browser with the access token in the + `Authorization` header + +> **Note:** because the browser calls the API directly, the API's CORS configuration has to allow +> `https://localhost:5003`. That is what `AllowedCorsOrigins` on the client registration is for. + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project src/AppHost + ``` + + > `AppHost` is deliberately **not** a member of `Quickstart.sln`, so `--project` is required. + + The Aspire dashboard opens at `https://localhost:17236`. + +1. Open a browser tab to `https://localhost:5003`. You see three buttons: **Login**, **Call API** and + **Logout**. +1. Click **Login**. The browser navigates to the IdentityServer at `https://localhost:5001`. +1. Log in with `alice` / `alice` (or `bob` / `bob`) and approve the consent request. You are returned + to `https://localhost:5003/callback.html`, which `oidc-client.js` consumes and then redirects back + to `index.html`. +1. Open the browser dev tools. Go to **Application → Local Storage** and you can see the tokens the + browser is holding. This is the concrete difference from the BFF sample. +1. Click **Call API**. The SPA calls `https://localhost:6001/identity` directly with a bearer token + and renders the returned claims. The request shows up in the network tab as originating from + `localhost:5003`, not from a server. +1. Click **Logout** to end the session at IdentityServer and clear the local tokens. + +## Credentials + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `IdentityServer` | `https://localhost:5001` | Login, consent and token issuance | +| `JavaScriptClient` | `https://localhost:5003` | Static SPA only — no backend logic | +| `WebClient` | `https://localhost:5002` | A separate ASP.NET Core MVC client, not part of this demo | +| `Api` | `https://localhost:6001` | Protected API, `GET /identity` | +| `Client` | _(console)_ | Client credentials demo, independent of the SPA | +| `AppHost` | `https://localhost:17236` | Aspire dashboard | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) + +No database or seeding step is required — the configuration is in-memory and the test users are +registered with `AddTestUsers`. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `src/JavaScriptClient/wwwroot/app.js` — the `Oidc.UserManager` settings and the API call +- `src/JavaScriptClient/wwwroot/callback.html` — the code flow redirect target +- `src/JavaScriptClient/Program.cs` — how little the "backend" does +- `src/IdentityServer/Config.cs` — the public `js` client with `AllowedCorsOrigins` + +To move the tokens out of the browser and into a server-side session, see +[`6_JS_with_backend`](../6_JS_with_backend). diff --git a/IdentityServer/v7/Quickstarts/7_Blazor/README.md b/IdentityServer/v7/Quickstarts/7_Blazor/README.md new file mode 100644 index 000000000..f7094461e --- /dev/null +++ b/IdentityServer/v7/Quickstarts/7_Blazor/README.md @@ -0,0 +1,96 @@ +# Blazor WebAssembly with a BFF sample + +This sample shows how to protect a **Blazor WebAssembly** application using the +[Backend-for-Frontend (BFF) pattern](https://docs.duendesoftware.com/identityserver/bff), so that +tokens never reach the browser. + +> **Note:** this sample does **not** run its own IdentityServer. It is a confidential client of the +> public Duende demo server at `https://demo.duendesoftware.com`, so you need internet access. + +### What the sample demonstrates + +- `builder.Services.AddBff()` from `Duende.BFF` to set up the BFF services +- `app.UseBff()` — must be placed between `UseAuthentication()` and `UseAuthorization()` +- `app.MapBffManagementEndpoints()`, which exposes the `/bff/login`, `/bff/logout` and `/bff/user` + endpoints used by the frontend +- a `__Host-` prefixed, `SameSite=Strict` authentication cookie +- `options.ClaimActions.MapAll()` plus `GetClaimsFromUserInfoEndpoint`, so the BFF holds a complete + claim set +- protecting a BFF API endpoint with `.AsBffApiEndpoint()`, which requires a CSRF header from the + caller +- `AddBffBlazorClient()` in the Blazor WASM project, providing a typed `BffContext` client +- a plain `HttpClient` that adds the `X-CSRF: 1` header the BFF API endpoints expect +- hosting the Blazor WASM app from the BFF itself via `MapStaticAssets()` and + `MapFallbackToFile("index.html")`, so there is only one deployed application + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project AppHost + ``` + + > Only the BFF is started by the AppHost. The Blazor WASM project is served by the BFF itself + > (`MapFallbackToFile("index.html")`), so it is not a separate running resource. + + The Aspire dashboard opens at `https://localhost:17236` and will show a single `bff` resource. + +1. Open a browser tab to `https://localhost:7256`. You are anonymous; the home page shows an empty + claim list. +1. Click **Log in** in the navigation. This navigates to `/bff/login`, which redirects to the demo + IdentityServer at `https://demo.duendesoftware.com`. +1. Log in as `bob` / `bob` (or `alice` / `alice`) and approve the request. +1. You are returned to the app and the home page now shows the claims. Notice that the nav has gained + a logout link. +1. Click **Weather**. The page calls `GET /api/data` through the typed BFF client, which attaches the + `X-CSRF: 1` header that `.AsBffApiEndpoint()` requires. The weather rows are returned and + rendered. +1. Click **Counter** to see plain client-side Blazor state, with no BFF involved. +1. Sign out to clear the cookie and end the session at the demo server. + +## Credentials + +| Username | Password | +|----------|----------| +| `bob` | `bob` | +| `alice` | `alice` | + +These are the users of the public demo IdentityServer at `https://demo.duendesoftware.com`. + +## Projects and URLs + +| Project | URL | +|---------|-----| +| `BFF` (serves both the BFF and the Blazor WASM app) | `https://localhost:7256` | +| `AppHost` (Aspire dashboard) | `https://localhost:17236` | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- Internet access to `https://demo.duendesoftware.com` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) + +No database or seeding is required — the demo server holds the configuration. + +> **Note:** `BlazorQuickstart.slnx` contains only the `BFF` and `BlazorWasm` projects, not `AppHost`. +> If you open the solution in an IDE and press run, you will start the BFF without the Aspire +> dashboard, which is fine — just open `https://localhost:7256`. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `BFF/Program.cs` — the entire BFF setup +- `BFF/weather.json` — the static payload served by the BFF API endpoint +- `BlazorWasm/Program.cs` — `AddBffBlazorClient()` and the `X-CSRF` header +- `BlazorWasm/Layout/MainLayout.razor` — the `bff/login` link +- `BlazorWasm/Pages/Home.razor` — the claims dump +- `BlazorWasm/Pages/Weather.razor` — the call to `GET /api/data` + +For a JavaScript SPA using a BFF instead, see quickstart +[`6_JS_with_backend`](../6_JS_with_backend). diff --git a/IdentityServer/v7/UserInteraction/DynamicProviders/README.md b/IdentityServer/v7/UserInteraction/DynamicProviders/README.md new file mode 100644 index 000000000..d4594a895 --- /dev/null +++ b/IdentityServer/v7/UserInteraction/DynamicProviders/README.md @@ -0,0 +1,102 @@ +# Dynamic Identity Providers sample + +IdentityServer can add external identity providers at runtime, without restarting or reconfiguring +the server. This sample keeps those provider definitions in a database, so you can see them appear on +the login page alongside a provider registered in code. + +After seeding you get **two** external providers on the login page, which is the point of the sample: +one comes from the database, one is hard-coded. + +### What the sample demonstrates + +- storing OIDC provider definitions in the configuration database, using the `IdentityProviders` + table and the `OidcProvider` model — IdentityServer reads them and turns each one into an external + authentication scheme automatically +- `AddConfigurationStore()` and `AddOperationalStore()` with SQLite, the latter with + `options.EnableTokenCleanup = true` for automatic token cleanup +- `options.Caching.IdentityProviderCacheDuration = TimeSpan.FromMinutes(15)` together with + `AddConfigurationStoreCache()`. This is the important operational detail: IdentityServer caches the + provider list, so **adding a provider to the database does not take effect immediately**. Wait up + to 15 minutes, or restart the IdentityServer, before expecting a change to show up. +- `options.EmitStaticAudienceClaim = true` +- a second, static provider registered in code with + `AddOpenIdConnect("oidc", "Sign-in with demo.duendesoftware.com", ...)`, using + `IdentityServerConstants.ExternalCookieAuthenticationScheme` as the sign-in scheme and + `IdentityServerConstants.SignoutScheme` as the sign-out scheme +- the `ExternalProviders` / `VisibleExternalProviders` logic in the login page model, which is what + renders the buttons +- `AddDataProtection().SetApplicationName("IdentityServer")`, required so the external cookie is + shared across all the providers + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project IdentityServer.AppHost + ``` + + > The AppHost project is called `IdentityServer.AppHost`, not `DynamicProviders.AppHost`. + + The Aspire dashboard opens at `https://localhost:17196` and shows two resources: + `identityserverhost` and `client`. + +1. Click the highlighted **Seed Database** button on the `identityserverhost` resource and wait for + it to report success. This migrates the SQLite database and populates the clients, resources, + scopes and identity providers. Nothing works until you do this. +1. Open a browser tab to the `client` application at `https://localhost:44300`. Click **Secure** in + the navigation; you are redirected to the IdentityServer login page at `https://localhost:5001`. +1. Look at the login page. Below the username and password form there are now **two** external + provider buttons: + - **IdentityServer (dynamic)** — seeded into the `IdentityProviders` table + - **Sign-in with demo.duendesoftware.com** — registered in `Program.cs` +1. Log in with `alice` / `alice` (or `bob` / `bob`) and approve the request to see the local flow. +1. Sign out, then click **Sign-in with demo.duendesoftware.com**. You are redirected to + `https://demo.duendesoftware.com`, which needs internet access. Log in there with `alice` / + `alice`, approve, and you are returned to the `client` with claims from the external provider. +1. To see the dynamic provider work the same way, click **IdentityServer (dynamic)** instead. It + points at the same demo authority but uses a different client id (`login`) and a different + display name, so you can tell the two paths apart. +1. If you want to add your own provider, insert a row into the `IdentityProviders` table in the + SQLite database, then restart the IdentityServer so the cache is cleared. + +## Credentials + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +Both external providers use the public demo server at `https://demo.duendesoftware.com`, which +accepts `alice` / `alice`. + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `IdentityServerHost` | `https://localhost:5001` | Login, consent and token issuance; owns the SQLite database | +| `Client` | `https://localhost:44300` | The web application that signs in | +| `IdentityServer.AppHost` | `https://localhost:17196` | Aspire dashboard, with the **Seed Database** button | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) +- Internet access, if you want to complete an external provider sign-in + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `IdentityServerHost/Program.cs` — the store configuration, the 15 minute cache duration and the + static provider registration +- `IdentityServerHost/SeedData.cs` — the `OidcProvider` row added to the `IdentityProviders` table +- `IdentityServerHost/Pages/Account/Login/Index.cshtml.cs` — how `VisibleExternalProviders` is built +- `IdentityServer.AppHost/AppHost.cs` — the "Seed Database" command + +For the WS-Federation flavour of dynamic providers, see the sibling +[`WsFederationDynamicProviders`](../WsFederationDynamicProviders) sample. diff --git a/IdentityServer/v7/UserInteraction/ProfileService/README.md b/IdentityServer/v7/UserInteraction/ProfileService/README.md new file mode 100644 index 000000000..d96f54c30 --- /dev/null +++ b/IdentityServer/v7/UserInteraction/ProfileService/README.md @@ -0,0 +1,121 @@ +# Custom Profile Service sample + +By default IdentityServer issues the claims listed in the `UserClaims` of the identity resources and +API scopes you request, filtered against the claims present in the user's authentication session. +That is often not enough — you may need to look something up, or issue a claim only for a particular +client. A custom `IProfileService` is the extension point for that. + +This sample registers a profile service and shows several ways to shape the claims in the response. + +### What the sample demonstrates + +- `AddProfileService()`, replacing the default profile service +- `context.AddRequestedClaims(context.Subject.Claims)` — **OPTION 1A**: copy claims out of the user's + session cookie, filtered to the ones actually requested +- `context.AddRequestedClaims(user.Claims)` — **OPTION 1B**: the same, but sourced from the user store + instead of the session +- `context.IssuedClaims.Add(...)` — **OPTION 2**: emit a claim unconditionally, ignoring what was + requested +- adding a `tenant` claim based on the calling client, checked with + `if (context.Client.ClientId == "client1")` — the standard way to vary the token contents per + client +- adding a `foo` claim based on `context.Caller == IdentityServerConstants.ProfileDataCallers.ClaimsProviderAccessToken`, + which distinguishes the access token from the id token and the userinfo endpoint +- reading `context.Subject.Claims` for hints already on the session, such as a `picture` claim +- `identity.mvc.sample` style: the client is registered as `interactive.mvc.sample` with the + `secret` client secret, and requests `openid`, `email`, `scope1` and `offline_access` +- `MapDefaultEndpoints()`-style API access through the shared `SimpleApi` project, so you can see the + resulting access token in use +- a shared `Shared\Constants.cs` holding the `Urls` constants for the IdentityServer and API, linked + into the client project so the ports are defined once + +> **Note:** the `SimpleApi` project's authorization policy requires the `scope` claim to contain +> `SimpleApi`, but the `SimpleApi` scope is not defined in IdentityServer and the policy is **not** +> applied (`app.MapControllers().RequireAuthorization("SimpleApi")` is commented out). The API +> therefore accepts any access token from the authority — which is what lets this sample work +> without registering an API scope. Do not copy that leniency into production. + +> **Known quirk:** the `tenant` claim is guarded by `context.Client.ClientId == "client1"`, but the +> only client registered in `Clients.cs` is `interactive.mvc.sample`. The `tenant` claim therefore +> never appears. Change the guard to `"interactive.mvc.sample"` to see it. + +> **Known quirk:** the sample also registers an OIDC handler named **Sign-in with Google** with a +> client id but **no client secret**. Google's authorization code flow for a server-side web app +> requires a secret, so that button will not complete. + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project ProfileService.AppHost + ``` + + The Aspire dashboard opens at `https://localhost:17047` and shows three resources: + `identityserverhost`, `simpleapi` and `client`. + +1. Open a browser tab to the `client` application at `https://localhost:44300`. You are anonymous; + the home page shows an empty claim list. +1. Click **Secure** in the navigation; you are redirected to the IdentityServer login page at + `https://localhost:5001`. +1. Log in with `alice` / `alice` (or `bob` / `bob`) and approve the request. +1. You are returned to the client. The home page lists the claims in your session. +1. Click **Secure** in the navigation. That page has two buttons: + - **Call API** — calls `GET https://localhost:5002/identity` on the `SimpleApi` project with the + access token. The API echoes back the claims from the token, which is where you see the effect + of the profile service on the **access token**. + - **Refresh token** — performs a manual refresh token grant with + `RequestRefreshTokenAsync`, exchanging the refresh token for a new access token and persisting + the rotated refresh token back into the session. +1. Compare the access token claims with the session claims on the home page. Any claim the profile + service added appears in the API response. +1. To see the `tenant` claim appear, change the guard in `CustomProfileService.cs` from `"client1"` + to `"interactive.mvc.sample"`, restart the IdentityServer and repeat. `foo` appears in the access + token only, because it is guarded on `ProfileDataCallers.ClaimsProviderAccessToken`. +1. Try the **Sign-in with Google** button on the login page to see the missing-secret failure + described above. + +## Credentials + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `IdentityServerHost` | `https://localhost:5001` | Login, consent and token issuance | +| `Client` | `https://localhost:44300` | The web application that signs in and calls the API | +| `SimpleApi` | `https://localhost:5002` | Protected API, echoes the token claims | +| `ProfileService.AppHost` | `https://localhost:17047` | Aspire dashboard | + +> `SimpleApi` is shared with the other samples — this one is at `IdentityServer/v7/Apis/SimpleApi`. + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) + +No database or seeding step is required — the configuration is in-memory and the test users are +registered with `AddTestUsers`. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `IdentityServerHost/CustomProfileService.cs` — the entire sample, including all three approaches + side by side +- `IdentityServerHost/Program.cs` — `AddProfileService()` +- `IdentityServerHost/Clients.cs` — the client registrations, and the source of the `client1` + mismatch +- `Client/Shared/Constants.cs` — the shared `Urls` constants +- `..\..\Apis\SimpleApi\Program.cs` — the (disabled) `SimpleApi` scope policy + +For the ASP.NET Core Identity variant of a profile service, see the +[`5_AspNetIdentity`](../Quickstarts/5_AspNetIdentity) quickstart. diff --git a/IdentityServer/v7/UserInteraction/SpaLoginUi/README.md b/IdentityServer/v7/UserInteraction/SpaLoginUi/README.md new file mode 100644 index 000000000..7e41d7940 --- /dev/null +++ b/IdentityServer/v7/UserInteraction/SpaLoginUi/README.md @@ -0,0 +1,101 @@ +# SPA Login UI sample + +The default IdentityServer login and consent pages are server-rendered Razor Pages. That works fine +when your application owns the UI, but it means the user interface is coupled to the identity +provider: to change the look of the login screen you have to change the identity provider. + +This sample replaces those pages with **static HTML files** that talk to a small set of JSON +endpoints. You can now host the login UI on a CDN, in a design system package, or behind a completely +separate deployment, without touching the token server. + +### What the sample demonstrates + +- overriding all four user interaction URLs to point at static files: + - `options.UserInteraction.LoginUrl = "/login.html"` + - `options.UserInteraction.ConsentUrl = "/consent.html"` + - `options.UserInteraction.LogoutUrl = "/logout.html"` + - `options.UserInteraction.ErrorUrl = "/error.html"` +- `SpaEndpoints.cs` — a small controller exposing the same operations the Razor Pages do, as JSON: + - `GET /spa/context` — wraps `IInteractionService.GetAuthorizationContextAsync`, returning the + login hint, requested `idp` and `tenant`, the raw scope values, and the client id + - `POST /spa/login` — validates credentials against the test users and issues a sign-in + - `POST /spa/consent` — records the consent response + - `GET /spa/error` — wraps the error interaction + - `GET`/`POST /spa/logout` — wraps the logout interaction +- the four static pages in `wwwroot` (`login.html`, `consent.html`, `logout.html`, `error.html`), + each a plain HTML file with inline `fetch` calls to `https://localhost:5001/spa/...` +- a client registered as `interactive` with `GrantTypes.Code`, redirecting to + `https://localhost:44300/signin-oidc` +- `options.EmitStaticAudienceClaim = true` + +> **Known quirk:** `SpaEndpoints` is decorated with `[EnableCors("spa")]`, but the sample never calls +> `AddCors()` to register a policy with that name. The CORS middleware only evaluates the policy when +> the request carries an `Origin` header, so the same-origin browser flow below works fine. If you +> serve these pages from a **different** origin — which is the point of a decoupled UI — you must add +> a matching `AddCors` policy yourself or those requests will throw *"The CORS policy named 'spa' was +> not found."* + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project SpaLoginUi.AppHost + ``` + + The Aspire dashboard opens at `https://localhost:17047` and shows two resources: + `identityserverhost` and `client`. + +1. Open a browser tab to the `client` application at `https://localhost:44300`. Click **Secure** in + the navigation. +1. You are redirected to `https://localhost:5001/login.html` — a static HTML file, not a Razor Page. + View the page source in the browser: it is plain HTML with an inline script. +1. Log in with `alice` / `alice` (or `bob` / `bob`). +1. The page's `fetch` POSTs your credentials to `https://localhost:5001/spa/login`. Open the browser + network tab to watch that call. The endpoint validates the credentials, signs you in, and returns + a redirect URL which the page then navigates to. +1. If the client requests scopes you have not consented to, you land on + `https://localhost:5001/consent.html`, which calls `GET /spa/context` to load the client name and + scopes, then `POST /spa/consent` to record your answer. +1. You are returned to the client. Sign out to see `logout.html` and the `/spa/logout` calls, which + include a **Sign me out automatically** checkbox that passes a `logoutId` through to + `IIdentityServerInteractionService`. +1. To see the error page, request a scope the client is not allowed. You land on + `https://localhost:5001/error.html`, which calls `GET /spa/error` and renders the message. + +## Credentials + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `IdentityServerHost` | `https://localhost:5001` | Token service, plus the `/spa/*` JSON endpoints and static UI | +| `Client` | `https://localhost:44300` | The web application that signs in | +| `SpaLoginUi.AppHost` | `https://localhost:17047` | Aspire dashboard | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) + +No database or seeding step is required — the configuration is in-memory and the test users are +registered with `AddTestUsers`. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `IdentityServerHost/Program.cs` — the four `UserInteraction.*Url` overrides +- `IdentityServerHost/SpaEndpoints.cs` — the JSON API behind the pages +- `IdentityServerHost/wwwroot/login.html` — a static login page calling `POST /spa/login` +- `IdentityServerHost/wwwroot/consent.html` — the `GET /spa/context` and `POST /spa/consent` pair +- `IdentityServerHost/Config.cs` — the `interactive` client registration diff --git a/IdentityServer/v7/UserInteraction/StepUp/README.md b/IdentityServer/v7/UserInteraction/StepUp/README.md new file mode 100644 index 000000000..09086b0fe --- /dev/null +++ b/IdentityServer/v7/UserInteraction/StepUp/README.md @@ -0,0 +1,153 @@ +# Step-Up Authentication sample + +[Step-up authentication](https://datatracker.ietf.org/doc/draft-ietf-oauth-step-up-authn-challenge) +lets a resource ask for a *stronger* authentication than the user has already proven. A banking API +might accept an existing session for viewing a balance, but demand a fresh MFA challenge before +showing account details. + +It is built on two standard OpenID Connect / OAuth 2.0 mechanisms: + +- **`acr_values`** on the authorization request — the client asks for a specific authentication + context, here `mfa` +- **`max_age`** on the authorization request — the client asks how recently the user must have + authenticated + +The API signals its requirements back with a `WWW-Authenticate` response header, and the client +turns that into a challenge to the IdentityServer. This sample implements the full round trip. + +### What the sample demonstrates + +**The API** (`Api`, on port 7001) + +- endpoints with different authentication requirements, in `StepUpController.cs` +- four endpoints under `/step-up`, each behind a different policy: `/neither` (`[Authorize]`, + just needs a signed-in user), `/max-age` (`MaxAgeOneMinute`), `/mfa` (`MfaRequired`) and `/both` + (`RecentMfa`) +- a `MaxAgeRequirement` / `MaxAgeRequirementHandler` that reads the `auth_time` claim and succeeds + while `DateTime.UtcNow - authTime` is under the requirement's `MaxAge` +- `StepUpAuthorizationMiddlewareResultHandler`, registered as an + `IAuthorizationMiddlewareResultHandler`. When a policy fails, it inspects the failed requirements + and writes a `StepUpWWWAuthenticateHeader` — its own `WWW-Authenticate` implementation that can + carry both `max_age` and `acr_values` +- `header.AcrValues = "mfa"` and `header.MaxAge = (int)maxAgeReq.MaxAge.TotalSeconds` — the API + telling the client exactly what it needs +- the endpoints echoing back the authentication age and the `amr` values, so you can see *why* the + request was accepted or denied + +**The client** (`Client`, on port 6001) + +- `StepUpHandler`, a `DelegatingHandler` on the API `HttpClient` that parses the + `WWW-Authenticate` header, and on a step-up response re-challenges the OIDC handler with the + corresponding `acr_values` / `max_age` values +- `ctx.ProtocolMessage.AcrValues = ctx.Properties.Items["acr_values"]` and + `ctx.ProtocolMessage.MaxAge = ctx.Properties.Items["max_age"]` in an + `OpenIdConnectEvents.OnRedirectToIdentityServer` handler, which is where the values are put back + onto the authorize request +- pages for each requirement, matching the API endpoints: **Secure**, **Recent Auth**, **MFA** and + **Recent Auth with MFA** + +**The IdentityServer** (`IdentityServerHost`, on port 5001) + +- `StepUpInteractionResponseGenerator`, registered as an `IAuthorizeInteractionResponseGenerator` and + derived from `AuthorizeInteractionResponseGenerator` +- it asks two questions in `MfaRequired(request)`: did the client ask for it + (`request.AuthenticationContextReferenceClasses.Contains("mfa")`), or does this user always need it + (`AlwaysUseMfaForUser`, which here is hard-coded to `sub == "bob"`)? If MFA is required and + `AuthenticatedWithMfa` finds no `amr == "mfa"` claim on the subject, it redirects to a custom + `/Account/Mfa` page +- if the user has already declined — the `declined_mfa` claim is present — it returns + `OidcConstants.AuthorizeErrors.UnmetAuthenticationRequirements` instead, so the browser is not + bounced into a re-prompt loop +- a fake MFA page at `Pages/Account/Mfa/Index.cshtml` that appends + `JwtClaimTypes.AuthenticationMethod = "mfa"` to the user's claims and signs them back in +- `AcrDiscoveryDocumentGenerator`, registered as an `IDiscoveryResponseGenerator`, which advertises + `acr_values_supported: ["1"]` in the discovery document so clients know MFA is available +- an `AddOpenIdConnect("oidc", "Sign-in with demo.duendesoftware.com", ...)` handler, so the login + page also offers the public demo server as an external provider + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project StepUp.AppHost + ``` + + The Aspire dashboard opens at `https://localhost:17202` and shows three resources: + `identityserverhost`, `api` and `client`. + +1. Open a browser tab to the `client` application at `https://localhost:6001`. Click **Secure**; you + are redirected to the IdentityServer login page at `https://localhost:5001`. +1. Log in as `alice` with password `alice`. Alice does **not** require MFA to log in, so you are + returned to the client immediately. +1. Click **MFA**. The client calls `GET /step-up/mfa` on the API. The API rejects it with + `WWW-Authenticate: acr_values=mfa`; the client's `StepUpHandler` intercepts that and re-challenges + the IdentityServer with `acr_values=mfa`. +1. The IdentityServer's `StepUpInteractionResponseGenerator` sees the request needs MFA and Alice has + no `amr: mfa` claim, so it redirects to the fake MFA page at `/Account/Mfa` instead of the home + page. Approve it. +1. You are returned to the client and the API call now succeeds. The page shows how long ago you + authenticated and which `amr` values were used. +1. Click **Recent Auth** to make a call to `/step-up/max-age`, which requires authentication in the + past minute. The API responds with `max_age` and the client re-challenges with that parameter. + Because you just authenticated, the call may succeed immediately; wait a minute and try again to + watch the step-up actually trigger. +1. Click **Recent Auth with MFA** to call `/step-up/both`, sending `acr_values` *and* `max_age` + together. +1. Sign out and sign in as `bob` with password `bob`. Bob always requires MFA — that is the + `AlwaysUseMfaForUser` special case — so even the initial **Secure** sign-in is challenged. + Compare that with Alice. +1. Decline an MFA challenge once. The fake MFA page records a `declined_mfa` claim, and the + generator then returns `UnmetAuthenticationRequirements` rather than redirecting you back, so you + land on the client's error page instead of an endless re-prompt. There is a dedicated + **MfaDeclined** page in the client for this. + +## Credentials + +| Username | Password | Behaviour | +|----------|----------|-----------| +| `alice` | `alice` | Does not require MFA to log in | +| `bob` | `bob` | Always requires MFA | + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `IdentityServerHost` | `https://localhost:5001` | Login, the custom MFA page, `acr_values` handling | +| `Client` | `https://localhost:6001` | The web application that triggers step-up | +| `Api` | `https://localhost:7001` | Protected resource that demands stronger authentication | +| `StepUp.AppHost` | `https://localhost:17202` | Aspire dashboard | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) + +No database or seeding step is required — the configuration is in-memory and the test users are +registered with `AddTestUsers`. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `Api/Authorization/StepUpHandler.cs` — the `IAuthorizationMiddlewareResultHandler` and the + `WWW-Authenticate` header +- `Api/Authorization/MaxAgeHandler.cs` — the `auth_time` check +- `Api/Controllers/StepUpController.cs` — the endpoints with their differing requirements +- `Client/StepUpHandler.cs` — the client-side `DelegatingHandler` that re-challenges +- `IdentityServerHost/StepUpInteractionResponseGenerator.cs` — deciding when to show the MFA page +- `IdentityServerHost/AcrDiscoveryDocumentGenerator.cs` — advertising `acr_values_supported` +- `IdentityServerHost/Pages/Account/Mfa/Index.cshtml` — the fake MFA page + +> **Note:** `IdentityServerHost/README.md` in this folder is an older, project-by-project +> walkthrough of the same sample. It covers the same ground — the `WWW-Authenticate` header, the +> `acr_values` / `max_age` parameters and the custom UI — and is a good complement if you want the +> narrative rather than the API surface. It tells you to start the three projects individually; +> this README uses the Aspire AppHost, which is the current convention. + +For CIBA, which solves a related problem for native and desktop applications that cannot host a +browser, see the sibling [`Ciba`](../Ciba) sample. diff --git a/IdentityServer/v7/UserInteraction/WindowsAuthentication/README.md b/IdentityServer/v7/UserInteraction/WindowsAuthentication/README.md new file mode 100644 index 000000000..0088d4aa2 --- /dev/null +++ b/IdentityServer/v7/UserInteraction/WindowsAuthentication/README.md @@ -0,0 +1,119 @@ +# Windows authentication sample + +This sample shows how to use **Windows Authentication** (Negotiate / Kerberos / NTLM) as the external +identity provider for IdentityServer, so users sign in with their existing Windows account and there +is no local user store at all. + +### What the sample demonstrates + +- configuring `IISOptions` with `iis.AuthenticationDisplayName = "Windows"` and + `iis.AutomaticAuthentication = false` — the display name is what surfaces the scheme as an external + provider on the login page, and turning automatic authentication off means the Windows challenge is + triggered deliberately rather than on every request +- the login page at `/Account/Login` redirecting **unconditionally** to a dedicated + `/Account/Login/Windows` Razor Page. The dynamic alternative — inspect the authorization context + and redirect only when `context?.IdP == "Windows"` — is present in the same file, commented out, + so you can switch between the two +- calling `HttpContext.AuthenticateAsync("Windows")` and inspecting the resulting `WindowsPrincipal` +- using the user's **SID** as the subject identifier: + `new IdentityServerUser(wp.FindFirst(ClaimTypes.PrimarySid).Value)` +- translating the user's Windows **group** memberships into `role` claims with + `WindowsIdentity.Groups.Translate(typeof(NTAccount))`, which the sample flags with a performance + warning — loading group SIDs is expensive +- `Challenge("Windows")` for the first visit, because Windows authentication does not support a + redirect URI; the same URL is re-entered on the way back +- forcing the IdP selection from the client by sending `acr_values=idp:Windows` in an + `OnRedirectToIdentityProvider` handler, so the user never has to pick a provider +- a confidential client with `BackChannelLogoutUri`, so signing out propagates back to the client + +> [!IMPORTANT] +> This sample has **hard platform requirements**. It only runs on **Windows**, and the IdentityServer +> host only works when run under **IIS or IIS Express** with the Windows Authentication provider +> enabled. `IdentityServerHost` has an `IISExpress` launch profile and **no** `Project` profile, so +> `dotnet run --project IdentityServerHost` will not give you a working sign-in. Running it under +> the Aspire AppHost (which uses `dotnet run` and plain Kestrel) will **not** provide Windows +> authentication either. + +> **Note:** the `WindowsAuthentication.AppHost` project exists and will start the client, but it +> cannot make the token server work on its own. Treat the Aspire dashboard as optional here and +> start the IdentityServer host from Visual Studio (or any IIS Express host) instead. + +## Prerequisites + +- **Windows** — `IdentityServerHost` targets `net10.0-windows` and uses + `System.Security.Principal.WindowsIdentity` / `WindowsPrincipal`. +- **IIS Express** (or IIS) with the ASP.NET Core Hosting Bundle installed and the **Windows + Authentication** provider enabled (Negotiate and/or NTLM). The `IISExpress` launch profile in + `IdentityServerHost/Properties/launchSettings.json` already sets `iisSettings.windowsAuthentication: + true` and `anonymousAuthentication: true`. +- **Kerberos** additionally requires a domain-joined machine. A standalone or workgroup machine + will fall back to NTLM. +- A browser that supports integrated Windows authentication — Edge (IE mode) or another browser + configured to delegate to Windows. A normal Chrome or Firefox window will not complete the + handshake silently. +- .NET 10 SDK and a trusted HTTPS development certificate: `dotnet dev-certs https --trust` + +## How to Run + +1. Open `WindowsAuthentication.sln` in Visual Studio, right-click `IdentityServerHost`, choose + **Properties → Run Debug** and confirm the profile is **IISExpress**, then press F5. It listens on + `https://localhost:44324/`. +1. Start the client in a second terminal: + + ```bash + dotnet run --project Client + ``` + + or use the Aspire AppHost for the client and the dashboard: + + ```bash + dotnet run --project WindowsAuthentication.AppHost + ``` + + The Aspire dashboard is at `https://localhost:17247`. + +1. Open a browser tab to the client at `https://localhost:44300`. Note that + `app.MapDefaultControllerRoute().RequireAuthorization()` means **every** page in the client + requires an authenticated user, so there is no anonymous landing page. +1. The client's OpenID Connect handler adds `acr_values=idp:Windows` and redirects to + `https://localhost:44324/account/login`. +1. The login page immediately redirects to `/Account/Login/Windows` — **the local username and + password form is never shown**. That page calls `Challenge("Windows")`, which triggers the + Windows logon prompt or the integrated-authentication handshake. +1. After you authenticate, you land back on the client's `Secure` page. Look at the claims: + - `sub` is your Windows **SID** + - `name` is your Windows account name + - the `role` claims are your **Windows group names** (for example `BUILTIN\Users` or + `BUILTIN\Administrators`), translated from `WindowsIdentity.Groups` +1. Click **Logout** to see the back-channel logout round trip to the client's + `https://localhost:44300/logout` endpoint. + +There are no sample usernames or passwords to type — you authenticate as whichever Windows account +you are logged in to. A `TestUsers.cs` file exists in the project and the login page model +constructs a `TestUserStore` from it if one is not injected, but `AddTestUsers` is never called and +the local login path is unreachable while `OnGet` always redirects to the Windows page. + +## Projects and URLs + +| Project | URL | Notes | +|---------|-----|-------| +| `IdentityServerHost` | `https://localhost:44324/` | must be hosted by IIS / IIS Express | +| `Client` | `https://localhost:44300` | **open this one** | +| `WindowsAuthentication.AppHost` (Aspire dashboard) | `https://localhost:17247` | starts the client; the token server still needs IIS Express | + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `IdentityServerHost/Pages/Account/Login/Windows.cshtml.cs` — the whole sample: group claims, the + SID subject, and the Windows challenge +- `IdentityServerHost/Pages/Account/Login/Index.cshtml.cs` — the unconditional redirect to the + Windows page, with the conditional alternative commented out underneath +- `IdentityServerHost/Program.cs` — the `IISOptions` configuration +- `IdentityServerHost/IdentityServerHost.csproj` — the `net10.0-windows` target framework +- `IdentityServerHost/Properties/launchSettings.json` — the `IISExpress` profile +- `IdentityServerHost/Clients.cs` — the `mvcsample` client, with the back-channel logout URI +- `Client/Program.cs` — `AcrValues = "idp:Windows"` diff --git a/IdentityServer/v7/UserInteraction/WsFederationDynamicProviders/README.md b/IdentityServer/v7/UserInteraction/WsFederationDynamicProviders/README.md new file mode 100644 index 000000000..2c66a82fb --- /dev/null +++ b/IdentityServer/v7/UserInteraction/WsFederationDynamicProviders/README.md @@ -0,0 +1,124 @@ +# WS-Federation Dynamic Providers sample + +[WS-Federation](https://learn.microsoft.com/openspecs/windows_protocols/ms-wsfdr/1fc0b6b0-1a1d-45a5-8b3b-83d1e0a4e0a8) +is the protocol Active Directory Federation Services (ADFS) and other corporate federation services +speak. It predates OpenID Connect and is still the right choice when the identity provider is +Microsoft Entra ID in a Windows-only corporate network. + +This sample registers WS-Federation identity providers dynamically, from a database, so you can see +how they surface on the login page. + +### What the sample demonstrates + +- `idsvrBuilder.AddWsFedDynamicProvider()` — the Duende API that turns rows in the + `IdentityProviders` table into WS-Federation external authentication schemes +- `AddIdentityProviderStore()` — the storage abstraction that lets the + providers live in the configuration database rather than in memory +- an in-memory alternative, `AddInMemoryWsFedProviders(...)`, present but commented out in + `Program.cs` alongside the `InMemoryWsFedProviderStore.cs` file in the same folder, so you can + compare the two +- storing a WS-Federation provider as a `WsFedProvider` row in the configuration database, with a + `Scheme`, `MetadataAddress` (the federation metadata URL), `RelyingPartyId` (the `wtrealm`) and a + `DisplayName` shown on the login page button +- storing an OIDC provider as an `OidcProvider` row in the **same** table, so both protocols can be + configured dynamically side by side +- `WsFedConfigureOptions.cs`, which maps the `WsFedProvider` fields onto + `WsFederationOptions.MetadataAddress` and `WsFederationOptions.Wtrealm` +- `AddConfigurationStore()` and `AddOperationalStore()` with SQLite, plus + `options.EnableTokenCleanup = true` +- `options.EmitStaticAudienceClaim = true` +- `AddOpenIdConnect("oidc", "Sign-in with demo.duendesoftware.com", ...)` as a *second*, OIDC-based + provider, so the login page shows both protocols side by side +- `AddDataProtection().SetApplicationName("IdentityServer")` + +> **Important:** the seed registers a WS-Federation provider called **Local ADFS** pointing at +> `https://adfs4.local/federationmetadata/2007-06/federationmetadata.xml`. That host does not exist +> — it is a placeholder so the configuration is illustrative. Clicking that button will fail to +> resolve. The **IdentityServer** provider that sits alongside it is an *OIDC* provider, not +> WS-Federation, and it does work — but the sample `client` is hard-wired to select the ADFS one, so +> see the note under How to Run. + +### How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project WsFederationDynamicProviders.AppHost + ``` + + The Aspire dashboard opens at `https://localhost:17050` and shows two resources: + `identityserverhost` and `client`. + +1. Click the highlighted **Seed Database** button on the `identityserverhost` resource and wait for + it to report success. This migrates the SQLite database and populates the clients, resources, + scopes and the two WS-Federation providers. Nothing works until you do this. + + > Seeding calls `Migrate()` only and guards each step with `if (!context.Clients.Any())`-style + > checks, so it is safe to run repeatedly. Change `Config.cs` and you will need to delete the + > SQLite file to pick the changes up. + +1. Open a browser tab to the `client` application at `https://localhost:44300`. Click **Secure** in + the navigation; you are redirected to the IdentityServer login page at `https://localhost:5001`. +1. Log in with `alice` / `alice` (or `bob` / `bob`) and approve the request. +1. Sign out and look at the login page. You now see three external provider buttons: + - **IdentityServer** — a dynamic **OIDC** provider seeded into the `IdentityProviders` table, + pointing at `https://demo.duendesoftware.com` as client `login` + - **Local ADFS** — a dynamic **WS-Federation** provider, the `adfs4.local` placeholder + - **Sign-in with demo.duendesoftware.com** — a static OIDC provider registered in `Program.cs` +1. **Local ADFS** will not work: `adfs4.local` is a placeholder hostname. This is expected, and it + is the only WS-Federation provider in the sample. +1. To complete a working external sign-in, click **IdentityServer** on the login page directly. You + are redirected to `https://demo.duendesoftware.com`, log in with `alice` / `alice`, approve, and + are returned. + +> **Note:** the sample `client` hard-codes `ctx.ProtocolMessage.AcrValues = "idp:adfs"` in +> `Client/Program.cs`, which tells IdentityServer to auto-select the ADFS provider on every request. +> That is why clicking **Secure** in the client sends you straight at the broken placeholder. The +> line is there to show how a client can request a specific identity provider by name; change it to +> `"idp:demoidsrv"` and restart the client to exercise the working one. + +## Credentials + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +The seeded **IdentityServer** OIDC provider relies on the public demo server at +`https://demo.duendesoftware.com`, which accepts `alice` / `alice`. The **Local ADFS** provider has no +reachable server at all. + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `IdentityServerHost` | `https://localhost:5001` | Login, consent and token issuance; owns the SQLite database | +| `Client` | `https://localhost:44300` | The web application that signs in | +| `WsFederationDynamicProviders.AppHost` | `https://localhost:17050` | Aspire dashboard, with the **Seed Database** button | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- The [Aspire](https://learn.microsoft.com/dotnet/aspire/) workload (`dotnet workload install aspire`) +- Internet access, and a reachable ADFS or federation endpoint, to actually complete a + WS-Federation sign-in + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `IdentityServerHost/Program.cs` — `AddWsFedDynamicProvider`, `EfWsFedProviderStore`, and the + commented-out in-memory alternative +- `IdentityServerHost/SeedData.cs` — the `OidcProvider` and `WsFedProvider` rows +- `IdentityServerHost/WsFedConfigureOptions.cs` — the `WsFederationOptions` mapping +- `IdentityServerHost/WsFedProvider.cs` — the provider model +- `IdentityServerHost/InMemoryWsFedProviderStore.cs` — the alternative store +- `Client/Program.cs` — the `AcrValues = "idp:adfs"` provider selection +- `WsFederationDynamicProviders.AppHost/AppHost.cs` — the "Seed Database" command + +For OIDC-based dynamic providers, see the sibling +[`DynamicProviders`](../DynamicProviders) sample. diff --git a/IdentityServer/v8/AspNetIdentity/README.md b/IdentityServer/v8/AspNetIdentity/README.md new file mode 100644 index 000000000..a5ff700b3 --- /dev/null +++ b/IdentityServer/v8/AspNetIdentity/README.md @@ -0,0 +1,76 @@ +# ASP.NET Core Identity sample + +This sample shows how to combine Duende IdentityServer with ASP.NET Core Identity, so that the +default ASP.NET Core Identity UI (login, register, manage account) is the login experience that +IdentityServer uses. + +### What the sample demonstrates + +- `AddAspNetIdentity()` to plug ASP.NET Core Identity into IdentityServer, so IdentityServer + issues tokens for ASP.NET Core Identity users +- using the default ASP.NET Core Identity UI for login, registration and account management +- a customized Identity `Logout` page that calls `IIdentityServerInteractionService.GetLogoutContextAsync` + so IdentityServer's logout prompt and sign-out iframe work with the Identity UI +- seeding a local SQLite user store from the command line +- an interactive OIDC client that signs in through the Identity UI and dumps the resulting claims + +## How to Run + +1. Seed the database. In the Aspire dashboard click **Seed Database** on the `identityserver` + resource, or run the seed manually: + + ```bash + dotnet run --project IdentityServerAspNetIdentity -- /seed + ``` + + > **Note:** seeding calls `EnsureDeleted()` before `Migrate()`, so the SQLite database is + > **recreated from scratch every time**. Any registered users are lost. + +1. Run the Aspire project: + + ```bash + dotnet run --project AspNetIdentity.AppHost + ``` + +1. Open a browser tab to the `client` application at `https://localhost:5002`. +1. Click on the **Secure** tab. You will be redirected to the IdentityServer application hosted at + `https://localhost:5001`. +1. Log in with the seeded user (see below). +1. You will be redirected back to the `client` application, which shows the claims from the tokens. + +You can also go straight to `https://localhost:5001` to use the Identity UI directly (login, register, +manage account). + +## Seeded users + +| Username | Password | Notes | +|----------|----------|-------| +| `AliceSmith@email.example` | `Pass123$` | Email is confirmed, no additional claims | +| `BobSmith@email.example` | `Pass123$` | Adds a custom `location` claim | + +## Projects and URLs + +| Project | URL | +|---------|-----| +| `IdentityServerAspNetIdentity` | `https://localhost:5001` | +| `Client` | `https://localhost:5002` | +| `AspNetIdentity.AppHost` (Aspire dashboard) | `https://localhost:17236` | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` + +No database server is needed — the sample uses a local SQLite file (`Users.db`). + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `IdentityServerAspNetIdentity/Program.cs` — the `AddAspNetIdentity()` call and the `/seed` switch +- `IdentityServerAspNetIdentity/SeedData.cs` — the test users +- `IdentityServerAspNetIdentity/Areas/Identity/Pages/Account/Logout.cshtml.cs` — the IdentityServer-aware logout page +- `Client/Program.cs` and `Client/Pages/Secure.cshtml` — the OIDC client diff --git a/IdentityServer/v8/AspNetIdentityPasskeys/README.md b/IdentityServer/v8/AspNetIdentityPasskeys/README.md new file mode 100644 index 000000000..3931e2173 --- /dev/null +++ b/IdentityServer/v8/AspNetIdentityPasskeys/README.md @@ -0,0 +1,87 @@ +# ASP.NET Core Identity with passkeys sample + +This sample shows how to register and sign in with **passkeys** (WebAuthn) using ASP.NET Core Identity +together with Duende IdentityServer. It is a full IdentityServer quickstart host (login, consent, device, +CIBA, grants, diagnostics pages) with the ASP.NET Core Identity store added on top. + +### What the sample demonstrates + +- `AddAspNetIdentity()` combined with the standard IdentityServer quickstart configuration +- setting `options.Stores.SchemaVersion = IdentitySchemaVersions.Version3`, which is required to persist passkeys +- `SignInManager.MakePasskeyCreationOptionsAsync` / `MakePasskeyRequestOptionsAsync` to produce WebAuthn options +- `PerformPasskeyAttestationAsync` / `AddOrUpdatePasskeyAsync` to register a new passkey +- `PasskeySignInAsync` for passwordless sign-in +- a `` custom element (Razor tag helper + `wwwroot/js/passkey-submit.js`) that calls + `navigator.credentials.create` / `navigator.credentials.get` and posts the result back +- **conditional mediation** (passkey autofill) on the login page +- a passkey management UI to list, rename and remove passkeys + +## How to Run + +1. Seed the database. In the Aspire dashboard click **Seed Database** on the `identityserver` + resource, or run the seed manually: + + ```bash + dotnet run --project IdentityServerAspNetIdentityPasskeys -- /seed + ``` + + > **Note:** seeding only runs `Migrate()`, so registered passkeys survive a re-seed. + +1. Run the Aspire project: + + ```bash + dotnet run --project AspNetIdentityPasskeys.AppHost + ``` + +1. Open a browser tab to the `client` application at `https://localhost:5002` and click **Secure**, + or navigate directly to `https://localhost:5001`. +1. Log in as `alice` / `Pass123$` (or `bob` / `Pass123$`) using the password form. +1. Go to `https://localhost:5001/Account/Passkeys` and click **Add a new passkey**. Your browser will + prompt you with the system authenticator (Windows Hello, Touch ID, Android screen lock, or a + security key). You are then asked to give the passkey a name. +1. Back on the passkey list you can **Rename** or **Remove** the passkey. +1. Sign out and log back in using **Log in with a passkey** (or just use the username field and let + passkey autofill do the work). + +## Seeded users + +| Username | Email | Password | +|----------|-------|----------| +| `alice` | `AliceSmith@email.example` | `Pass123$` | +| `bob` | `BobSmith@email.example` | `Pass123$` | + +## Projects and URLs + +| Project | URL | +|---------|-----| +| `IdentityServerAspNetIdentityPasskeys` | `https://localhost:5001` | +| `Client` | `https://localhost:5002` | +| `AspNetIdentityPasskeys.AppHost` (Aspire dashboard) | `https://localhost:17237` | + +## Prerequisites and caveats + +- .NET 10 SDK and a trusted HTTPS development certificate (`dotnet dev-certs https --trust`). + Passkeys require a **secure context**, so you must use `https://localhost:5001`. +- Passkeys are bound to the exact origin **`https://localhost:5001`**. This is configured in + `HostingExtensions.cs` via `IdentityPasskeyOptions.ValidateOrigin`; if you change the port, update + that check or passkeys will stop working. +- You need a **browser that supports WebAuthn Level 3 JSON serialization** + (`PublicKeyCredential.parseCreationOptionsFromJSON` / `parseRequestOptionsFromJSON`) — current + Chrome, Edge, Safari 17.4+ or Firefox 119+. Older browsers show "Some passkey features are missing." +- You need a machine with an **enrolled platform authenticator** (Windows Hello, Touch ID, Android + screen lock) or a FIDO2 security key. A headless VM without one cannot complete the flow. +- The "Sign-in with demo.duendesoftware.com" button requires internet access; local passkey and + password sign-in work offline. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `IdentityServerAspNetIdentityPasskeys/HostingExtensions.cs` — `AddAspNetIdentity`, `SchemaVersion = Version3`, `ValidateOrigin` +- `IdentityServerAspNetIdentityPasskeys/Passkeys/PasskeyEndpointRouteBuilderExtensions.cs` — the two WebAuthn option endpoints +- `IdentityServerAspNetIdentityPasskeys/Passkeys/PasskeySubmitTagHelper.cs` and `wwwroot/js/passkey-submit.js` — the custom element and WebAuthn calls +- `IdentityServerAspNetIdentityPasskeys/Pages/Account/Passkeys.cshtml.cs` — passkey management +- `IdentityServerAspNetIdentityPasskeys/Pages/Account/Login/Index.cshtml.cs` — password and passkey sign-in diff --git a/IdentityServer/v8/DPoP/README.md b/IdentityServer/v8/DPoP/README.md new file mode 100644 index 000000000..681c0adb8 --- /dev/null +++ b/IdentityServer/v8/DPoP/README.md @@ -0,0 +1,85 @@ +# DPoP (Demonstration of Proof of Possession) sample + +This sample shows how to bind access tokens to a client using **DPoP** ([RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449)), +so that a stolen bearer token is useless without the corresponding private key. + +### What the sample demonstrates + +- a client configured with `RequireDPoP = true`, which makes IdentityServer issue a **DPoP-bound access + token**: `typ` is `at+jwt` and the token carries a `cnf.jkt` claim with the client's key thumbprint +- generating a DPoP key on the client and handing it to `Duende.AccessTokenManagement` + (`options.DPoPJsonWebKey`), so a proof is attached automatically to every token request and every API call +- resource server validation with `ConfigureDPoPTokensForScheme`, which checks the proof signature, + the `jkt` to `cnf.jkt` binding, the `htm`/`htu` method and URI, and proof freshness +- `DPoPProofExpirationMode` — this sample uses the default `IssuedAt` mode; the alternative `Nonce` mode + has the API issue a `DPoP-Nonce` that the client must echo, at the cost of an extra round trip +- **replay protection** using a keyed `HybridCache` in front of an `IDistributedCache` +- DPoP for both **interactive** (authorization code) and **machine-to-machine** (client credentials) clients +- an API that echoes back the scheme, the raw DPoP proof, the raw access token and the claims, so the + whole exchange is visible in the browser + +## How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project DPoP.AppHost + ``` + +1. Open a browser tab to the web client at `https://localhost:5010`. +1. Click on the **Secure** link in the navigation. You will be redirected to IdentityServer at + `https://localhost:5001`. +1. Log in with username `alice` and password `alice` (or `bob` / `bob`) and grant consent. +1. You will be redirected back to the `client` application's `/Secure` page, which lists the user's + claims and the authentication cookie properties. The **Renew Tokens** button forces a fresh + DPoP-bound access token. +1. Click the **Call API** button. The page shows the JSON returned by the API. Look at: + - `"scheme": "DPoP"` — the API selected the DPoP scheme, not `Bearer` + - `proofToken` — a `dpop+jwt` proof signed with `PS256`, containing `jkt`, `htm`, `htu` and `iat` + - `accessToken` — an `at+jwt` whose `cnf.jkt` matches the proof's `jkt` +1. Click **Logout**. The web client revokes the refresh token on sign-out. +1. At the same time, look at the `clientcredentials` console in the Aspire dashboard. That background + service calls the API every 5 seconds with a DPoP-bound client credentials token and logs the same + JSON, demonstrating the machine-to-machine case. + +## Seeded users + +The IdentityServer is the shared `../IdentityServerHost` host, which uses in-memory test users: + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +The `dpop` client id and secret are `dpop` / `905e4892-7610-44cb-a122-6209b38c882f` if you want to +call the token endpoint by hand. + +## Projects and URLs + +| Project | URL | Role | +|---------|-----|------| +| `IdentityServerHost` (shared, from `../IdentityServerHost`) | `https://localhost:5001` | issues DPoP-bound tokens | +| `Api` | `https://localhost:5005` | validates DPoP proofs | +| `WebClient` | `https://localhost:5010` | interactive client — **open this one** | +| `ClientCredentials` | — | background worker, no URL | +| `DPoP.AppHost` | `https://localhost:17008` | Aspire dashboard | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` + +No database, Docker container or external service is required. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `Api/Program.cs` — `ValidTypes = ["at+jwt"]`, `ConfigureDPoPTokensForScheme`, the proof expiration mode and the replay cache +- `Api/IdentityController.cs` — the response that makes DPoP visible +- `WebClient/Program.cs` — creating the RSA DPoP key and `AddUserAccessTokenHttpClient` +- `ClientCredentials/Program.cs` and `ClientCredentials/DPoPClient.cs` — DPoP for client credentials +- `../IdentityServerHost/src/Clients.cs` — the `dpop` client with `RequireDPoP = true` diff --git a/IdentityServer/v8/Diagnostics/Aspire/README.md b/IdentityServer/v8/Diagnostics/Aspire/README.md new file mode 100644 index 000000000..fe924d80a --- /dev/null +++ b/IdentityServer/v8/Diagnostics/Aspire/README.md @@ -0,0 +1,86 @@ +# Aspire diagnostics sample + +This sample shows how to get **logs, metrics and traces** out of Duende IdentityServer using .NET Aspire +and OpenTelemetry, and how to wire the surrounding services together with Aspire's service discovery, +resilience and health check defaults. + +### What the sample demonstrates + +- `builder.AddServiceDefaults()` in every service, which configures: + - **OpenTelemetry logs** via `builder.Logging.AddOpenTelemetry(...)` + - **OpenTelemetry metrics** from `AddRuntimeInstrumentation()`, `AddBuiltInMeters()` and + `AddMeter("Duende.IdentityServer", "Duende.IdentityServer.Experimental", "IdentityServer")` + - **OpenTelemetry traces** from `AddAspNetCoreInstrumentation()`, `AddHttpClientInstrumentation()`, + `AddGrpcClientInstrumentation()` and `AddSource("Duende.IdentityServer")` + - OTLP exporters, which Aspire enables automatically by setting `OTEL_EXPORTER_OTLP_ENDPOINT` + - health checks at `/health` and `/alive` + - **service discovery** (`AddServiceDiscovery()`) and the **standard resilience handler** for every `HttpClient` +- custom application metrics emitted from the IdentityServer UI pages, using a meter named after this + host: `tokenservice.user_login`, `tokenservice.user_logout`, `tokenservice.consent`, `tokenservice.grants_revoked` +- `Duende.AccessTokenManagement` combined with a deliberately short `AccessTokenLifetime` so that tokens + renew constantly and you can watch a steady stream of token requests in the dashboard +- Aspire service discovery in action: the web frontend resolves the API by its logical name + (`https://apiservice`) rather than a hard-coded port + +## How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project Aspire.AppHost + ``` + +1. The Aspire dashboard opens automatically at `https://localhost:15264`. +1. On the **Resources** page, click the endpoint link for the `webfrontend` resource to open the app + (or navigate to `https://localhost:5014`). +1. Log in as `alice` / `alice` and approve consent. The home page shows a table of weather data + fetched from the API service. +1. Now explore the dashboard while clicking around the app: + - **Traces** — filter by the `identityserver` service to see spans from the `Duende.IdentityServer` + activity source across the authorize and token endpoints, plus the outgoing calls from the web + frontend to the API service. + - **Metrics** — look for counters under `Duende.IdentityServer.*` and `IdentityServer.tokenservice.*`. + The `user_login` and `consent` counters increment as you sign in and grant consent. + - **Logs** — structured logs from all three services. Because the client access token lifetime is + 70 seconds, you will see a token request roughly every 10 seconds while the session is active. + - **Resources** — each service exposes `/health` and `/alive`. + +## Seeded users + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +## Projects and URLs + +| Project | URL | Exposed in dashboard | +|---------|-----|----------------------| +| `IdentityServer` | `https://localhost:5001` | yes | +| `Aspire.Web` | `https://localhost:5014` | yes | +| `Aspire.ApiService` | `https://localhost:5325` | no (internal only) | +| `Aspire.AppHost` (Aspire dashboard) | `https://localhost:15264` | — | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` + +No database, Docker container or external service is required. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `Aspire.ServiceDefaults/Extensions.cs` — all the OpenTelemetry, health check, service discovery and resilience wiring +- `Aspire.AppHost/Program.cs` — the service topology +- `IdentityServer/Pages/Telemetry.cs` — the custom `tokenservice.*` counters +- `IdentityServer/HostingExtensions.cs` — the IdentityServer setup and the short token lifetime +- `Aspire.Web/Program.cs` and `Aspire.Web/WeatherApiClient.cs` — service discovery and the user access token handler +- `Aspire.ApiService/Program.cs` — the `weather` scope fallback policy + +For a console-only version of the same idea, with no Aspire and no dashboard, see the +[OpenTelemetry sample](../Otel/README.md). diff --git a/IdentityServer/v8/Diagnostics/Otel/README.md b/IdentityServer/v8/Diagnostics/Otel/README.md new file mode 100644 index 000000000..5315e4780 --- /dev/null +++ b/IdentityServer/v8/Diagnostics/Otel/README.md @@ -0,0 +1,74 @@ +# OpenTelemetry console tracing sample + +This is the minimal version of the [Aspire diagnostics sample](../Aspire/README.md): it shows how to turn on +OpenTelemetry **tracing** inside IdentityServer with no collector, no Aspire and no dashboard. The spans are +printed straight to the console. + +### What the sample demonstrates + +- `builder.Services.AddOpenTelemetry().WithTracing(...)` with a **console exporter**, so spans show up in + the terminal +- subscribing to IdentityServer's **fine-grained tracing sources** via + `IdentityServerConstants.Tracing.Basic`, `.Cache`, `.Services`, `.Stores` and `.Validation` +- `AddHttpClientInstrumentation()`, `AddAspNetCoreInstrumentation()` and `AddSqlClientInstrumentation()` +- setting the OpenTelemetry `resource` service name with `ConfigureResource(r => r.AddService(...))` +- an OTLP exporter you can switch on by uncommenting a single line, if you would rather send spans to a collector +- structured logging through Serilog, including `UseSerilogRequestLogging()` + +## How to Run + +1. Run the project: + + ```bash + dotnet run --project src + ``` + +1. The browser opens at `https://localhost:5001`. **Watch the terminal** — that is where the traces appear. +1. Log in as `alice` / `alice` (or `bob` / `bob`) using the **Login** link. +1. Navigate around the app. Each request produces a Serilog request log line followed by a stream of + OpenTelemetry spans showing the activity, trace and span ids and the originating source + (`Duende.IdentityServer.*`, ASP.NET Core, or `System.Net.Http`). +1. To generate traces on the token endpoint as well, request a token from another terminal: + + ```bash + curl --fail-with-body -X POST "https://localhost:5001/connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials&client_id=m2m.client&client_secret=511536EF-F270-4058-80CA-1C89C192F69A&scope=scope1" + ``` + +1. `https://localhost:5001/diagnostics` shows the claims in the current authentication cookie + (restricted to requests from the local machine). + +## Seeded users + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +## Projects and URLs + +| Project | URL | +|---------|-----| +| `src` | `https://localhost:5001` | + +There is no AppHost for this sample — it is a single project. + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `src/HostingExtensions.cs` — the whole OpenTelemetry setup +- `src/Program.cs` — the Serilog bootstrap logger and `UseSerilog` +- `src/Config.cs` — the clients and scopes, including the `m2m.client` used in the `curl` example above + +If you want metrics, logs and a dashboard rather than console traces only, use the +[Aspire sample](../Aspire/README.md) instead. diff --git a/IdentityServer/v8/Quickstarts/7_Blazor/README.md b/IdentityServer/v8/Quickstarts/7_Blazor/README.md new file mode 100644 index 000000000..e1eb98786 --- /dev/null +++ b/IdentityServer/v8/Quickstarts/7_Blazor/README.md @@ -0,0 +1,88 @@ +# Blazor WebAssembly with a BFF sample + +This sample shows how to protect a **Blazor WebAssembly** application using the +[Backend-for-Frontend (BFF) pattern](https://docs.duendesoftware.com/identityserver/bff), so that +tokens never reach the browser. + +### What the sample demonstrates + +- `builder.Services.AddBff()` from `Duende.BFF` to set up the BFF services +- `app.UseBff()` — must be placed between `UseAuthentication()` and `UseAuthorization()` +- `app.MapBffManagementEndpoints()`, which exposes the `/bff/login`, `/bff/logout` and `/bff/user` endpoints + used by the frontend +- `AddOpenIdConnectAccessTokenManagement()` so the BFF caches, refreshes and forwards tokens automatically +- protecting a BFF API endpoint with `.AsBffApiEndpoint()`, which requires a CSRF header from the caller +- `AddBffBlazorClient()` in the Blazor WASM project, providing a typed `BffContext` client +- `AddCascadingAuthenticationState()` so `AuthorizeView` works in Razor components +- a `__Host-` prefixed, `SameSite=Strict` authentication cookie + +> **Note:** this sample does **not** run its own IdentityServer. It is a confidential client of the public +> Duende demo server at `https://demo.duendesoftware.com`, so you need internet access. + +## How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project AppHost + ``` + + > Only the BFF is started by the AppHost. The Blazor WASM project is served by the BFF itself + > (`MapFallbackToFile("index.html")`), so it is not a separate running resource. + + The Aspire dashboard opens at `https://localhost:17236` and will show a single `bff` resource. + +1. Open a browser tab to `https://localhost:7256`. You are anonymous; the home page shows an empty claim list. +1. Click **bff/login** in the navigation. This redirects to the demo IdentityServer. +1. Log in as `bob` / `bob` (or `alice` / `alice`) and approve the request. +1. You are returned to the app and the home page now shows the claims. Notice that the nav has gained a + **bff/logout** link, taken from the `bff:logout_url` claim that `MapBffManagementEndpoints` emits. +1. Click **Weather**. The page calls `GET /api/data` through the typed BFF client, which attaches the + `X-CSRF: 1` header that `.AsBffApiEndpoint()` requires. The weather rows are returned and rendered. +1. Click **Counter** to see plain client-side Blazor state, with no BFF involved. +1. Click **bff/logout** to clear the cookie and end the session at the demo server. + +## Credentials + +| Username | Password | +|----------|----------| +| `bob` | `bob` | +| `alice` | `alice` | + +These are the users of the public demo IdentityServer at `https://demo.duendesoftware.com`. + +## Projects and URLs + +| Project | URL | +|---------|-----| +| `BFF` (serves both the BFF and the Blazor WASM app) | `https://localhost:7256` | +| `AppHost` (Aspire dashboard) | `https://localhost:17236` | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- Internet access to `https://demo.duendesoftware.com` + +No database or seeding is required — the demo server holds the configuration. + +> **Note:** `BlazorQuickstart.slnx` contains only the `BFF` and `BlazorWasm` projects, not `AppHost`. +> If you open the solution in an IDE and press run, you will start the BFF without the Aspire dashboard, +> which is fine — just open `https://localhost:7256`. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `BFF/Program.cs` — the entire BFF setup +- `BFF/weather.json` — the static payload served by the BFF API endpoint +- `BlazorWasm/Program.cs` — `AddBffBlazorClient()` +- `BlazorWasm/Layout/MainLayout.razor` — the `bff/login` and `bff/logout` links +- `BlazorWasm/Pages/Home.razor` — the claims dump +- `BlazorWasm/Pages/Weather.razor` — the call to `GET /api/data` + +For a JavaScript SPA using a BFF instead, see quickstart +[`6_JS_with_backend`](../6_JS_with_backend). diff --git a/IdentityServer/v8/Quickstarts/JavaScriptClient/README.md b/IdentityServer/v8/Quickstarts/JavaScriptClient/README.md new file mode 100644 index 000000000..f9bd1253b --- /dev/null +++ b/IdentityServer/v8/Quickstarts/JavaScriptClient/README.md @@ -0,0 +1,85 @@ +# JavaScript client sample + +This sample shows a **pure JavaScript SPA** talking directly to IdentityServer from the browser, with +tokens held in the browser. It is the standalone version of the older JavaScript quickstart. + +### What the sample demonstrates + +- an OpenID Connect client built with the [`oidc-client`](https://github.com/IdentityModel/oidc-client-js) + library (`Oidc.UserManager`), which is vendored in `wwwroot/lib/oidc-client.js` +- the **authorization code flow with PKCE** via `signinRedirect()` and `signinRedirectCallback()` +- tokens stored in the browser's `sessionStorage` rather than in a server-side session +- a deliberate, hand-rolled `XMLHttpRequest` that attaches `Authorization: Bearer ` to the API + call, so you can see exactly how the raw token is sent +- RP-initiated logout via `signoutRedirect()` + +> **Note:** this is a legacy sample. The project targets `net6.0` and uses the older `Startup` class +> pattern, and it is a *client only* — it contains neither an IdentityServer nor an API. It is not part +> of the repository solution and will need a newer target framework to build on a current SDK. +> +> For a maintained equivalent that runs end to end, use the +> [`6_JS_without_backend`](../6_JS_without_backend) quickstart. If you are looking for the modern +> approach with tokens kept server-side, use [`6_JS_with_backend`](../6_JS_with_backend) instead. + +## How to Run + +Because this project is a client only, you must start the IdentityServer and the API it expects first. +The matching host is the [`6_JS_without_backend`](../6_JS_without_backend) quickstart, which serves: + +| Service | URL | Registered by | +|---------|-----|---------------| +| IdentityServer | `https://localhost:5001` | `6_JS_without_backend` — client `js`, redirect `https://localhost:5003/callback.html` | +| API (`/identity`) | `https://localhost:6001` | `6_JS_without_backend` | +| This SPA | `https://localhost:5003` | this project | + +1. Start the `6_JS_without_backend` quickstart (it has its own Aspire AppHost) so the IdentityServer and + the API are running. +1. Start this project: + + ```bash + dotnet run --project JavaScriptClient.csproj + ``` + +1. The browser opens at `https://localhost:5003/index.html`. The results pane logs **User not logged in**. +1. Click **Login**. You are redirected to `https://localhost:5001/connect/authorize` with + `response_type=code`, a PKCE `code_challenge`, and `redirect_uri=https://localhost:5003/callback.html`. +1. Log in at the IdentityServer as `alice` / `alice` (or `bob` / `bob`) and consent. +1. The IdentityServer redirects to `https://localhost:5003/callback.html?code=...&state=...`, which + calls `signinRedirectCallback()` and returns to `index.html`. The results pane now shows + **User logged in** and the pretty-printed user profile. The access token lives in `sessionStorage`. +1. Click **Call API**. The sample issues a `GET https://localhost:6001/identity` with the bearer token and + shows the JSON identity payload. + - A `401` means the token's audience or scope is wrong. + - A CORS error in the browser dev tools means the API's allowed-origins list is missing + `https://localhost:5003`. +1. Click **Logout**. `signoutRedirect()` goes to the IdentityServer's end-session endpoint, the + `User signed out of IdentityServer` event fires, and you land back on `index.html` logged out. + +## Seeded users + +These are the in-memory test users of the `6_JS_without_backend` IdentityServer: + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +## Prerequisites + +- .NET SDK able to build a `net6.0` web project +- A running IdentityServer at `https://localhost:5001` with the `js` client registered, and an API at + `https://localhost:6001` — in this repository, the `6_JS_without_backend` quickstart provides both +- Trusted HTTPS development certificates for ports 5001, 6001 and 5003: `dotnet dev-certs https --trust` +- CORS configured on the API for the origin `https://localhost:5003` + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `wwwroot/app.js` — the OIDC configuration and every call +- `wwwroot/index.html` — the login, call API and logout buttons +- `wwwroot/callback.html` — reads `?code` and `?state`, then completes the redirect +- `wwwroot/lib/oidc-client.js` — the vendored `oidc-client` library diff --git a/IdentityServer/v8/UserInteraction/DynamicProviders/README.md b/IdentityServer/v8/UserInteraction/DynamicProviders/README.md new file mode 100644 index 000000000..4bebf2aef --- /dev/null +++ b/IdentityServer/v8/UserInteraction/DynamicProviders/README.md @@ -0,0 +1,85 @@ +# Dynamic identity providers sample + +This sample shows how to add **dynamic external identity providers** to IdentityServer. Instead of being +registered in code, the providers live as rows in the configuration store, so you can add or change an +external IdP without recompiling or redeploying. + +### What the sample demonstrates + +- an OIDC provider stored in the configuration database (`SeedData.cs` creates an `OidcProvider` with + scheme `demoidsrv` pointing at `https://demo.duendesoftware.com`) +- a **static** OIDC scheme registered in code alongside it, so you can compare the two side by side on the + same login page +- `AddConfigurationStoreCache()` with `Caching.IdentityProviderCacheDuration`, which is what makes + dynamic providers (and the rest of the configuration data) cached rather than re-read on every request +- discovering providers on the login page by combining two sources: the static schemes from + `IAuthenticationSchemeProvider.GetAllSchemesAsync()` and the dynamic schemes from + `IIdentityProviderStore.GetAllSchemeNamesAsync()` +- honouring `client.EnableLocalLogin` and `client.IdentityProviderRestrictions` when building the login page +- auto-provisioning external users through the external login callback + +## How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project IdentityServer.AppHost + ``` + + > The AppHost project is named `IdentityServer.AppHost`, not `DynamicProviders.AppHost`. + +1. In the Aspire dashboard, click **Seed Database** on the `identityserverhost` resource. The clients, + resources and the dynamic provider all live in SQLite, so nothing works until you do this. You can + also run it manually with `dotnet run --project IdentityServerHost -- /seed`. +1. Open a browser tab to the client at `https://localhost:44300`. It redirects to the login page at + `https://localhost:5001/account/login`. +1. The login page shows a **Local Account** card and an **External Account** card with two buttons: + - **Sign-in with demo.duendesoftware.com** — the statically registered `oidc` scheme + - **IdentityServer (dynamic)** — the `demoidsrv` provider loaded from the configuration store +1. Log in locally as `alice` / `alice` to see the local flow. +1. Click either external button to be redirected to `https://demo.duendesoftware.com`, log in as + `alice` / `alice`, and you will be returned to the client with the external user auto-provisioned. +1. Visit `https://localhost:5001/diagnostics` to see the session claims, and `https://localhost:5001/grants` + to see the grants that have been stored. + +## Seeded users + +Local users (in-memory test users): + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +The same credentials also work on the external `https://demo.duendesoftware.com` provider. + +## Projects and URLs + +| Project | URL | +|---------|-----| +| `IdentityServerHost` | `https://localhost:5001` | +| `Client` | `https://localhost:44300` — **open this one** | +| `IdentityServer.AppHost` (Aspire dashboard) | `https://localhost:17197` | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` +- Internet access to `https://demo.duendesoftware.com` for the external providers. Local login works + without it. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `IdentityServerHost/Pages/Account/Login/Index.cshtml.cs` — merging static and dynamic schemes +- `IdentityServerHost/SeedData.cs` — the `OidcProvider` row that creates the dynamic provider +- `IdentityServerHost/Program.cs` — `AddConfigurationStoreCache()` and the identity provider cache duration +- `IdentityServerHost/Pages/ExternalLogin/Challenge.cshtml.cs` and `Callback.cshtml.cs` — the external flow +- `IdentityServer.AppHost/AppHost.cs` — the **Seed Database** command + +For a WS-Federation version of dynamic providers, see +[`WsFederationDynamicProviders`](../WsFederationDynamicProviders/README.md). diff --git a/IdentityServer/v8/UserInteraction/ProfileService/README.md b/IdentityServer/v8/UserInteraction/ProfileService/README.md new file mode 100644 index 000000000..2ccc42f94 --- /dev/null +++ b/IdentityServer/v8/UserInteraction/ProfileService/README.md @@ -0,0 +1,85 @@ +# Profile service sample + +This sample shows how to control the **claims that end up in your tokens** by implementing a custom +`IProfileService`. + +### What the sample demonstrates + +- registering a custom profile service with `AddProfileService()` +- `GetProfileDataAsync` — the hook that decides what is emitted into the id_token and access token +- `IsActiveAsync` — the hook that decides whether a session is still valid +- adding claims from the **session cookie** (`context.Subject.Claims`) and from a **user store** + (`_users.FindBySubjectId(...)`) +- the `context.Caller` property, which tells you whether IdentityServer is building the token for the + client (`ClaimsProviderAccessToken`) or for the userinfo endpoint (`ClaimsProviderUserInfo`) +- the `context.Application.Identifier` property, which lets you emit different claims per client + +`CustomProfileService.cs` is heavily commented and shows four separate techniques: + +1. emit only the claims the client actually asked for, either from the session or from the user store +2. always emit a `picture` claim, regardless of the requested claim types +3. emit a `tenant` claim when the calling client is `client1` +4. emit a `foo` claim when the caller is `ClaimsProviderAccessToken` + +## How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project ProfileService.AppHost + ``` + +1. In the Aspire dashboard, click the endpoint link for the `client` resource to open + `https://localhost:44300`, or navigate there directly. +1. You will be redirected to the login page at `https://localhost:5001/account/login`. Log in as + `alice` / `alice`. +1. You are returned to the client's `Home/Secure` page, which lists the user claims and the tokens from + the authentication cookie. +1. Click **Call API**. The client calls the API at `https://localhost:5002/identity` and displays the JSON. + Notice the `foo: bar` claim — that is technique 4, and it appears in the **access token** but not in + the id_token. +1. Click **Refresh token** to perform a refresh token grant and re-issue the session. +1. You can also inspect what was issued directly at `https://localhost:5001/diagnostics` (the access token + is shown there for a locally signed request). + +## Seeded users + +| Username | Password | +|----------|----------| +| `alice` | `alice` | +| `bob` | `bob` | + +## Projects and URLs + +| Project | URL | +|---------|-----| +| `IdentityServerHost` | `https://localhost:5001` | +| `Client` | `https://localhost:44300` — **open this one** | +| `SimpleApi` (shared, from `../../Apis/SimpleApi`) | `https://localhost:5002` | +| `ProfileService.AppHost` (Aspire dashboard) | `https://localhost:17047` | + +## Prerequisites + +- .NET 10 SDK +- A trusted HTTPS development certificate: `dotnet dev-certs https --trust` + +No database seeding is needed — all configuration is in memory. + +> **Note on the `tenant` claim:** technique 3 only fires when `context.Application.Identifier == "client1"`, +> but the only client registered in this sample is `interactive.mvc.sample`. To see the `tenant` claim you +> need to rename that client to `client1` or add a client with that id. + +> **Note on the Google button:** the login page offers "Sign-in with Google", but the baked-in client id +> has no secret, so the external flow will not complete. Local login is the path to use. + +## More Information + +Please take a look [here](https://docs.duendesoftware.com/identityserver/samples) to learn about the +structure of our samples and how to run them. + +The interesting parts of this sample are: + +- `IdentityServerHost/CustomProfileService.cs` — the entire sample +- `IdentityServerHost/Program.cs` — the `AddProfileService()` registration +- `IdentityServerHost/Clients.cs` — the `interactive.mvc.sample` client +- `Client/Controllers/HomeController.cs` — the **Call API** and **Refresh token** actions diff --git a/IdentityServer/v8/UserInteraction/SpaLoginUi/README.md b/IdentityServer/v8/UserInteraction/SpaLoginUi/README.md new file mode 100644 index 000000000..39d680cd4 --- /dev/null +++ b/IdentityServer/v8/UserInteraction/SpaLoginUi/README.md @@ -0,0 +1,77 @@ +# SPA login UI sample + +This sample shows how to replace IdentityServer's default Razor Pages user interaction pages with your +**own** pages — here, hand-written static HTML plus a small JSON API. The result is a login experience that +looks and behaves like a single-page application. + +### What the sample demonstrates + +- pointing IdentityServer's interaction URLs at static files instead of Razor Pages: + `LoginUrl = "/login.html"`, `ConsentUrl = "/consent.html"`, `LogoutUrl = "/logout.html"`, + `ErrorUrl = "/error.html"` +- a controller that implements each interaction with `IIdentityServerInteractionService`: + - `GetAuthorizationContextAsync` — who is asking for what, used to render the login and consent pages + - `GrantConsentAsync` / `DenyAuthorizationAsync` — recording the user's consent decision + - `GetErrorContextAsync` — rendering the error page + - `GetLogoutContextAsync` — including the sign-out iframe URL for front-channel logout +- signing the user in and out with `HttpContext.SignInAsync` / `SignOutAsync` against a `TestUserStore` +- serving the static pages with `app.UseDefaultFiles()` and `app.UseStaticFiles()` +- a `RequireConsent = true` client, so the consent page is always exercised + +## How to Run + +1. Run the Aspire project: + + ```bash + dotnet run --project SpaLoginUi.AppHost + ``` + +1. In the Aspire dashboard, click the endpoint link for the `client` resource to open + `https://localhost:44300`, or navigate there directly. +1. You are redirected to `https://localhost:5001/login.html?returnUrl=...` — a plain static HTML page, + not a Razor page. +1. Log in as `alice` / `alice` and click **Login**. The page posts JSON to `/spa/login` and then + navigates to the returned `validReturnUrl`. +1. You are then shown `https://localhost:5001/consent.html?returnUrl=...`. The page calls + `GET /spa/context` and renders the requesting client and the requested scopes. Optionally tick + **Remember My Decision** and click **Yes, Allow**. Clicking **No, Do Not Allow** instead takes you back + to the client with an access denied error. +1. You land back on the client application showing your claims. +1. Click **Logout** in the client navigation. You are redirected to + `https://localhost:5001/logout.html?logoutId=...`. If the client requests a logout prompt you will be + asked to confirm; otherwise the page loads the sign-out iframe in a hidden `