Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 13 additions & 22 deletions src/content/docs/bff/fundamentals/apis/local.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<IActionResult> 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).
Expand Down
52 changes: 21 additions & 31 deletions src/content/docs/bff/fundamentals/tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<IActionResult> 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:
Expand All @@ -75,25 +64,26 @@ services.AddHttpClient<MyTypedApiClient>(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<IActionResult> 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:

Expand Down
31 changes: 10 additions & 21 deletions src/content/docs/identityserver/apis/aspnetcore/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<IActionResult> 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<IActionResult> Get()
{
var allowed = authz.CheckAccess(User, "read_access");

// rest omitted
}
}
// rest omitted
}).RequireAuthorization("read_access");
```

#### Scope Claim Format
Expand Down
22 changes: 16 additions & 6 deletions src/content/docs/identityserver/apis/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ var scopes = new List<ApiScope>
new Client
{
// rest omitted
AllowedScopes = { IdentityServerConstants.LocalApi.ScopeName },
AllowedScopes = { IdentityServerConstants.LocalApi.ScopeName },
}
```

Expand All @@ -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
Expand All @@ -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
Expand All @@ -88,16 +98,17 @@ 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`
* sets up an authorization policy that checks for a scope claim of value `IdentityServerApi`

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.:


Expand All @@ -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.:

Expand All @@ -129,5 +141,3 @@ builder.Services.AddLocalApiAuthentication(principal =>
```

...or implement the event on the options if you add the authentication handler manually.


22 changes: 5 additions & 17 deletions src/content/docs/identityserver/tokens/internal.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<IActionResult> 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
Expand Down