From 84adf4f3dfdcba3d1121c54cc5dceddc2357f4d9 Mon Sep 17 00:00:00 2001 From: Maarten Balliauw Date: Fri, 16 Jan 2026 12:48:05 +0100 Subject: [PATCH] Introduce examples using minimal APIs where ever API controllers are used #490 --- .../docs/bff/fundamentals/apis/local.mdx | 35 +++++-------- src/content/docs/bff/fundamentals/tokens.md | 52 ++++++++----------- .../apis/aspnetcore/authorization.md | 31 ++++------- src/content/docs/identityserver/apis/index.md | 22 +++++--- .../docs/identityserver/tokens/internal.md | 22 ++------ 5 files changed, 65 insertions(+), 97 deletions(-) diff --git a/src/content/docs/bff/fundamentals/apis/local.mdx b/src/content/docs/bff/fundamentals/apis/local.mdx index 21e0c307e..936962ec6 100644 --- a/src/content/docs/bff/fundamentals/apis/local.mdx +++ b/src/content/docs/bff/fundamentals/apis/local.mdx @@ -37,33 +37,24 @@ Your Embedded endpoints can leverage services like the HTTP client factory and D The following is a simplified example showing how Embedded endpoints can get managed access tokens and use them to make requests to remote APIs. ```csharp -// MyApiController.cs -[Route("myApi")] -public class MyApiController : ControllerBase +// Program.cs +app.MapGet("/myApi", async (IHttpClientFactory httpClientFactory, HttpContext context) => { - private readonly IHttpClientFactory _httpClientFactory; - - public MyApiController(IHttpClientFactory httpClientFactory) - { - _httpClientFactory = httpClientFactory; - } + var id = context.Request.Query["id"]; - public async Task Get(string id) - { - // create HTTP client - var client = _httpClientFactory.CreateClient(); + // create HTTP client + var client = httpClientFactory.CreateClient(); - // get current user access token and set it on HttpClient - var token = await HttpContext.GetUserAccessTokenAsync(); - client.SetBearerToken(token); + // get current user access token and set it on HttpClient + var token = await context.GetUserAccessTokenAsync(); + client.SetBearerToken(token); - // call remote API - var response = await client.GetAsync($"https://remoteServer/remoteApi?id={id}"); + // call remote API + var response = await client.GetAsync($"https://remoteServer/remoteApi?id={id}"); - // maybe process response and return to frontend - return new JsonResult(await response.Content.ReadAsStringAsync()); - } -} + // maybe process response and return to frontend + return Results.Text(await response.Content.ReadAsStringAsync()); +}); ``` The example above is simplified to demonstrate the way that you might obtain a token. Embedded endpoints will typically enforce constraints on the way the API is called, aggregate multiple calls, or perform other business logic. Embedded endpoints that merely forward requests from the frontend to the remote API may not be needed at all. Instead, you could proxy the requests through the BFF using either the [simple http forwarder](/bff/fundamentals/apis/remote.mdx) or [YARP](/bff/fundamentals/apis/yarp.md). diff --git a/src/content/docs/bff/fundamentals/tokens.md b/src/content/docs/bff/fundamentals/tokens.md index 4918a7488..13081e195 100644 --- a/src/content/docs/bff/fundamentals/tokens.md +++ b/src/content/docs/bff/fundamentals/tokens.md @@ -15,20 +15,20 @@ Duende.BFF includes an automatic token management feature. This uses the access For most scenarios, there is no additional configuration necessary. The token management will infer the configuration and token endpoint URL from the metadata of the OpenID Connect provider. -The easiest way to retrieve the current access token is to use an extension method on *HttpContext*: +The easiest way to retrieve the current access token is to use an extension method on `HttpContext`: ```csharp var token = await HttpContext.GetUserAccessTokenAsync(); ``` -You can then use the token to set it on an *HttpClient* instance: +You can then use the token to set it on an `HttpClient`instance: ```csharp var client = new HttpClient(); client.SetBearerToken(token); ``` -We recommend to leverage the *HttpClientFactory* to fabricate HTTP clients that are already aware of the token management plumbing. For this you would register a named client in your application startup e.g. like this: +We recommend to use the `HttpClientFactory` to create HTTP clients that are already aware of the token management plumbing. For this you would register a named client in your application startup e.g. like this: ```csharp // Program.cs @@ -42,27 +42,16 @@ builder.Services.AddUserAccessTokenHttpClient("apiClient", configureClient: clie And then retrieve a client instance like this: ```csharp -[Route("myApi")] -public class MyApiController : ControllerBase +app.MapGet("/myApi", async (IHttpClientFactory httpClientFactory, HttpContext context) => { - private readonly IHttpClientFactory _httpClientFactory; - - public MyController(IHttpClientFactory httpClientFactory) - { - _httpClientFactory = httpClientFactory; - } - - public async Task Get(string id) - { - // create HTTP client with automatic token management - var client = _httpClientFactory.CreateClient("apiClient"); - - // call remote API - var response = await client.GetAsync("remoteApi"); - - // rest omitted - } -} + // create HTTP client with automatic token management + var client = httpClientFactory.CreateClient("apiClient"); + + // call remote API + var response = await client.GetAsync("remoteApi"); + + // rest omitted +}); ``` If you prefer to use typed clients, you can do that as well: @@ -75,25 +64,26 @@ services.AddHttpClient(client => }).AddUserAccessTokenHandler(); ``` -And then use that client, for example like this on a controller's action method: +And then use that client, for example like this on an endpoint: ```csharp -public async Task CallApiAsUserTyped( - [FromServices] MyTypedClient client) +app.MapGet("/myApi", async (MyTypedClient client) => { var response = await client.GetData(); - + // rest omitted -} +}); ``` -The client will internally always try to use a current and valid access token. If for any reason this is not possible, the 401 status code will be returned to the caller. +The client will internally always try to use a current and valid access token. If for any reason this is not possible, the 401 status code will be returned to the caller. ### Reuse of Refresh Tokens -We recommend that you configure IdentityServer to issue reusable refresh tokens to BFF clients. Because the BFF is a confidential client, it does not need one-time use refresh tokens. Reusable refresh tokens are desirable because they avoid performance and user experience problems associated with one time use tokens. See the discussion on [rotating refresh tokens](/identityserver/tokens/refresh.md) and the [OAuth 2.0 Security Best Current Practice](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.2.2) for more details. + +We recommend that you configure IdentityServer to issue reusable refresh tokens to BFF clients. Because the BFF is a confidential client, it does not need one-time use refresh tokens. Reusable refresh tokens are desirable because they avoid performance and user experience problems associated with one time use tokens. See the discussion on [rotating refresh tokens](/identityserver/tokens/refresh.md) and the [OAuth 2.0 Security Best Current Practice](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.2.2) for more details. ### Manually revoking refresh tokens -Duende.BFF revokes refresh tokens automatically at logout time. This behavior can be disabled with the *RevokeRefreshTokenOnLogout* option. + +Duende.BFF revokes refresh tokens automatically at logout time. This behavior can be disabled with the _RevokeRefreshTokenOnLogout_ option. If you want to manually revoke the current refresh token, you can use the following code: diff --git a/src/content/docs/identityserver/apis/aspnetcore/authorization.md b/src/content/docs/identityserver/apis/aspnetcore/authorization.md index 5015c2fcd..64667a934 100644 --- a/src/content/docs/identityserver/apis/aspnetcore/authorization.md +++ b/src/content/docs/identityserver/apis/aspnetcore/authorization.md @@ -42,40 +42,29 @@ builder.Services.AddAuthorization(options => app.MapControllers().RequireAuthorization("read_access"); ``` -...or imperatively inside the controller: +...or imperatively inside the endpoint handler: ```cs -public class DataController : ControllerBase +app.MapGet("/", async (IAuthorizationService authz, ClaimsPrincipal user) => { - IAuthorizationService _authz; + var allowed = await authz.AuthorizeAsync(user, "read_access"); - public DataController(IAuthorizationService authz) + if (!allowed.Succeeded) { - _authz = authz; + return Results.Forbid(); } - public async Task Get() - { - var allowed = _authz.CheckAccess(User, "read_access"); - - // rest omitted - } -} + // rest omitted +}); ``` ... or declaratively: ```cs -public class DataController : ControllerBase +app.MapGet("/", () => { - [Authorize("read_access")] - public async Task Get() - { - var allowed = authz.CheckAccess(User, "read_access"); - - // rest omitted - } -} + // rest omitted +}).RequireAuthorization("read_access"); ``` #### Scope Claim Format diff --git a/src/content/docs/identityserver/apis/index.md b/src/content/docs/identityserver/apis/index.md index a4a7a63b5..fcd6a230b 100644 --- a/src/content/docs/identityserver/apis/index.md +++ b/src/content/docs/identityserver/apis/index.md @@ -45,7 +45,7 @@ var scopes = new List new Client { // rest omitted - AllowedScopes = { IdentityServerConstants.LocalApi.ScopeName }, + AllowedScopes = { IdentityServerConstants.LocalApi.ScopeName }, } ``` @@ -60,6 +60,15 @@ To enable token validation for local APIs, add the following to your IdentitySer builder.Services.AddLocalApiAuthentication(); ``` +To protect an API endpoint, call `RequireAuthorization` with the `LocalApi.PolicyName` policy: + +```cs +app.MapGet("/localApi", () => +{ + // omitted +}).RequireAuthorization(LocalApi.PolicyName); +``` + To protect an API controller, decorate it with an `Authorize` attribute using the `LocalApi.PolicyName` policy: ```cs @@ -77,6 +86,7 @@ public class LocalApiController : ControllerBase Authorized clients can then request a token for the `IdentityServerApi` scope and use it to call the API. ## Discovery + You can also add your endpoints to the discovery document if you want, e.g.like this:: ```cs @@ -88,7 +98,8 @@ builder.Services.AddIdentityServer(options => ``` ## Advanced -Under the covers, the `AddLocalApiAuthentication` helper does a couple of things: + +Under the hood, the `AddLocalApiAuthentication` helper does a couple of things: * adds an authentication handler that validates incoming tokens using IdentityServer's built-in token validation engine (the name of this handler is `IdentityServerAccessToken` or `IdentityServerConstants.LocalApi.AuthenticationScheme` * configures the authentication handler to require a scope claim inside the access token of value `IdentityServerApi` @@ -96,8 +107,8 @@ Under the covers, the `AddLocalApiAuthentication` helper does a couple of things This covers the most common scenarios. You can customize this behavior in the following ways: -* Add the authentication handler yourself by calling `services.AddAuthentication().AddLocalApi(...)` - * this way you can specify the required scope name yourself, or (by specifying no scope at all) accept any token from the current IdentityServer instance +* Add the authentication handler yourself by calling `services.AddAuthentication().AddLocalApi(...)`. + This way you can specify the required scope name yourself, or (by specifying no scope at all) accept any token from the current IdentityServer instance * Do your own scope validation/authorization in your controllers using custom policies or code, e.g.: @@ -115,6 +126,7 @@ builder.Services.AddAuthorization(options => ``` ## Claims Transformation + You can provide a callback to transform the claims of the incoming token after validation. Either use the helper method, e.g.: @@ -129,5 +141,3 @@ builder.Services.AddLocalApiAuthentication(principal => ``` ...or implement the event on the options if you add the authentication handler manually. - - diff --git a/src/content/docs/identityserver/tokens/internal.md b/src/content/docs/identityserver/tokens/internal.md index 2e57204c8..1a8154a46 100644 --- a/src/content/docs/identityserver/tokens/internal.md +++ b/src/content/docs/identityserver/tokens/internal.md @@ -15,30 +15,18 @@ Sometimes, extensibility code running on your IdentityServer needs access tokens not necessary to use the protocol endpoints. The tokens can be issued internally. `IIdentityServerTools` is a collection of useful internal tools that you might need when writing extensibility code -for IdentityServer. To use it, inject it into your code, e.g. a controller:: +for IdentityServer. To use it, inject it into your code, e.g. an endpoint: -```cs -public MyController(IIdentityServerTools tools) +```csharp +app.MapGet("/myAction", async (IIdentityServerTools tools) => { - _tools = tools; -} -``` - -The `IssueJwtAsync` method allows creating JWT tokens using the IdentityServer token creation engine. The -`IssueClientJwtAsync` is an easier -version of that for creating tokens for server-to-server communication (e.g. when you have to call an IdentityServer -protected API from your code): - -```cs -public async Task MyAction() -{ - var token = await _tools.IssueClientJwtAsync( + var token = await tools.IssueClientJwtAsync( clientId: "client_id", lifetime: 3600, audiences: new[] { "backend.api" }); // more code -} +}); ``` The `IIdentityServerTools` interface was added in v7 to allow mocking. Previous versions referenced the