Skip to content

Commit

Permalink
Merge pull request #531 from DuendeSoftware/dom/foss-update
Browse files Browse the repository at this point in the history
  • Loading branch information
leastprivilege authored Oct 14, 2024
2 parents 35d4b70 + 0f4964d commit b881727
Show file tree
Hide file tree
Showing 237 changed files with 842 additions and 71 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: '0.122.0'
hugo-version: '0.135.0'
- name: Run hugo
run: |
echo $PWD
Expand Down
File renamed without changes.
File renamed without changes.
6 changes: 3 additions & 3 deletions IdentityModel/config.toml → FOSS/config.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
baseURL = "https://docs.duendesoftware.com/identitymodel"
baseURL = "https://docs.duendesoftware.com/foss"
languageCode = "en-us"
title = "IdentityModel Documentation"
title = "Duende Open Source Documentation"

theme = "hugo-theme-learn"
# For search functionality
[outputs]
home = [ "HTML", "RSS", "JSON"]

[params]
editURL = "https://github.com/DuendeSoftware/docs.duendesoftware.com/edit/main/IdentityModel/docs/content/"
editURL = "https://github.com/DuendeSoftware/docs.duendesoftware.com/edit/main/FOSS/docs/content/"
70 changes: 70 additions & 0 deletions FOSS/content/AccessTokenManagement/Advanced/DPoP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
+++
title = "DPop"
weight = 40
chapter = false
+++

[DPoP](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop) specifies how to bind an asymmetric key stored within a JSON Web Key (JWK) to an access token. This will make the access token bound to the key such that if the access token were to leak, it cannot be used without also having access to the private key of the corresponding JWK.

The "Duende.AccessTokenManagement" library supports DPoP.

## DPoP Key

The main piece that your hosting application needs to concern itself with is how to obtain (and manage) the DPoP key. This key (and signing algorithm) will be either a "RS", "PS", or "ES" style key, and needs to be in the form of a JSON Web Key (or JWK). Consult the specification for more details.

The creation and management of this DPoP key is up to the policy of the client. For example is can be dynamically created when the client starts up, and can be periodically rotated. The main constraint is that it must be stored for as long as the client uses any access tokens (and possibly refresh tokens) that they are bound to, which this library will manage for you.

Creating a JWK in .NET is simple:

```cs
var rsaKey = new RsaSecurityKey(RSA.Create(2048));
var jwkKey = JsonWebKeyConverter.ConvertFromSecurityKey(rsaKey);
jwkKey.Alg = "PS256";
var jwk = JsonSerializer.Serialize(jwkKey);
```

## Key Configuration

Once you have a JWK you wish to use, then it must be configured or made available to this library. That can be done in one of two ways:

* Configure the key at startup by setting the `DPoPJsonWebKey` property on either the `ClientCredentialsTokenManagementOptions` or `UserTokenManagementOptions` (depending on which of the two styles you are using from this library).
* Implement the `IDPoPKeyStore` interface to produce the key at runtime.

Here's a sample configuring the key in an application using `AddOpenIdConnectAccessTokenManagement` in the startup code:

```cs
services.AddOpenIdConnectAccessTokenManagement(options =>
{
options.DPoPJsonWebKey = jwk;
});
```

Similarly for an application using `AddClientCredentialsTokenManagement` it would look like this:

```cs
services.AddClientCredentialsTokenManagement()
.AddClient("client_name", options =>
{
options.DPoPJsonWebKey = jwk;
});
```

## Proof Tokens at the token server's token endpoint

Once the key has been configured for the client, then the library will use it to produce a DPoP proof token when calling the token server (including token renewals if relevant).
There is nothing explicit needed on behalf of the developer using this library.

### dpop_jkt at the token server's authorize endpoint

When using DPoP and `AddOpenIdConnectAccessTokenManagement`, this library will also automatically include the `dpop_jkt` parameter to the authorize endpoint.

## Proof Tokens at the API

Once the library has obtained a DPoP bound access token for the client, then if your application is using any of the `HttpClient` client factory helpers (e.g. `AddClientCredentialsHttpClient` or `AddUserAccessTokenHttpClient`) then those outbound HTTP requests will automatically include a DPoP proof token for the associated DPoP access token.

## Considerations

A point to keep in mind when using DPoP and `AddOpenIdConnectAccessTokenManagement` is that the DPoP proof key is created per user session.
This proof key must be store somewhere, and the `AuthenticationProperties` used by both the OIDC and cookie handlers is what is used to store this key.
This implies that the OIDC `state` parameter will increase in size, as well the resultant cookie that represents the user's session.
The storage of each of these can be customized with the properties on the options `StateDataFormat` and `SessionStore` respectively.
8 changes: 8 additions & 0 deletions FOSS/content/AccessTokenManagement/Advanced/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
+++
title = "Advanced"
weight = 50
chapter = true
+++

Advanced
========
60 changes: 60 additions & 0 deletions FOSS/content/AccessTokenManagement/Advanced/client_assertions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
+++
title = "Client Assertions"
weight = 30
chapter = false
+++

If your token client is using a client assertion instead of a shared secret, you can provide the assertion in two ways

* use the request parameter mechanism to pass a client assertion to the management
* implement the `IClientAssertionService` interface to centralize client assertion creation

Here's a sample client assertion service using the Microsoft JWT library:

```cs
public class ClientAssertionService : IClientAssertionService
{
private readonly IOptionsSnapshot<ClientCredentialsClient> _options;

public ClientAssertionService(IOptionsSnapshot<ClientCredentialsClient> options)
{
_options = options;
}

public Task<ClientAssertion?> GetClientAssertionAsync(
string? clientName = null, TokenRequestParameters? parameters = null)
{
if (clientName == "invoice")
{
var options = _options.Get(clientName);

var descriptor = new SecurityTokenDescriptor
{
Issuer = options.ClientId,
Audience = options.TokenEndpoint,
Expires = DateTime.UtcNow.AddMinutes(1),
SigningCredentials = GetSigningCredential(),

Claims = new Dictionary<string, object>
{
{ JwtClaimTypes.JwtId, Guid.NewGuid().ToString() },
{ JwtClaimTypes.Subject, options.ClientId! },
{ JwtClaimTypes.IssuedAt, DateTime.UtcNow.ToEpochTime() }
}
};

var handler = new JsonWebTokenHandler();
var jwt = handler.CreateToken(descriptor);

return Task.FromResult<ClientAssertion?>(new ClientAssertion
{
Type = OidcConstants.ClientAssertionTypes.JwtBearer,
Value = jwt
});
}

return Task.FromResult<ClientAssertion?>(null);
}
}
```

124 changes: 124 additions & 0 deletions FOSS/content/AccessTokenManagement/Advanced/client_credentials.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
+++
title = "Customizing Client Credentials Token Management"
weight = 10
chapter = false
+++

The most common way to use the access token management for machine to machine communication is described [here](worker-applications) - however you may want to customize certain aspects of it - here's what you can do.

### Client options

You can add token client definitions to your host while configuring the DI container, e.g.:

```cs
services.AddClientCredentialsTokenManagement()
.AddClient("invoices", client =>
{
client.TokenEndpoint = "https://sts.company.com/connect/token";

client.ClientId = "4a632e2e-0466-4e5a-a094-0455c6105f57";
client.ClientSecret = "e8ae294a-d5f3-4907-88fa-c83b3546b70c";
client.ClientCredentialStyle = ClientCredentialStyle.AuthorizationHeader;

client.Scope = "list";
client.Resource = "urn:invoices";
})
```

You can set the following options:

* `TokenEndpoint` - URL of the OAuth token endpoint where this token client requests tokens from
* `ClientId` - client ID
* `ClientSecret` - client secret (if a shared secret is used)
* `ClientCredentialStyle` - Specifies how the client ID / secret is sent to the token endpoint. Options are using using the authorization header, or POST body values (defaults to header)
* `Scope` - the requested scope of access (if any)
* `Resource` - the resource indicator (if any)

Internally the standard .NET options system is used to register the configuration. This means you can also register clients like this:

```cs
services.Configure<ClientCredentialsClient>("invoices", client =>
{
client.TokenEndpoint = "https://sts.company.com/connect/token";

client.ClientId = "4a632e2e-0466-4e5a-a094-0455c6105f57";
client.ClientSecret = "e8ae294a-d5f3-4907-88fa-c83b3546b70c";

client.Scope = "list";
client.Resource = "urn:invoices";
});
```

Or use the `IConfigureNamedOptions` if you need access to the DI container during registration, e.g.:

```cs
public class ClientCredentialsClientConfigureOptions : IConfigureNamedOptions<ClientCredentialsClient>
{
private readonly DiscoveryCache _cache;

public ClientCredentialsClientConfigureOptions(DiscoveryCache cache)
{
_cache = cache;
}

public void Configure(string name, ClientCredentialsClient options)
{
if (name == "invoices")
{
var disco = _cache.GetAsync().GetAwaiter().GetResult();

options.TokenEndpoint = disco.TokenEndpoint;

client.ClientId = "4a632e2e-0466-4e5a-a094-0455c6105f57";
client.ClientSecret = "e8ae294a-d5f3-4907-88fa-c83b3546b70c";

client.Scope = "list";
client.Resource = "urn:invoices";
}
}
}
```

..and register the config options e.g. like this:

```cs
services.AddClientCredentialsTokenManagement();

services.AddSingleton(new DiscoveryCache("https://sts.company.com"));
services.AddSingleton<IConfigureOptions<ClientCredentialsClient>,
ClientCredentialsClientConfigureOptions>();
```

#### Backchannel communication

By default all backchannel communication will be done using a named client from the HTTP client factory. The name is `Duende.AccessTokenManagement.BackChannelHttpClient` which is also a constant called `ClientCredentialsTokenManagementDefaults.BackChannelHttpClientName`.

You can register your own HTTP client with the factory using the above name and thus provide your own custom HTTP client.

The client registration object has two additional properties to customize the HTTP client:

* `HttpClientName` - if set, this HTTP client name from the factory will be used instead of the default one
* `HttpClient` - allows setting an instance of `HttpClient` to use. Will take precedence over a client name

### Token caching

By default, tokens will be cached using the `IDistributedCache` abstraction in ASP.NET Core. You can either use the in-memory cache version, or plugin a real distributed cache like Redis.

```cs
services.AddDistributedMemoryCache();
```

The built-in cache uses two settings from the options:

```cs
services.AddClientCredentialsTokenManagement(options =>
{
options.CacheLifetimeBuffer = 60;
options.CacheKeyPrefix = "Duende.AccessTokenManagement.Cache::";
});
```

`CacheLifetimeBuffer` is a value in seconds that will be subtracted from the token lifetime, e.g. if a token is valid for one hour, it will be cached for 59 minutes only. The cache key prefix is used to construct the unique key for the cache item based on client name, requested scopes and resource.

Finally, you can also replace the caching implementation altogether by registering your own `IClientCredentialsTokenCache`.

86 changes: 86 additions & 0 deletions FOSS/content/AccessTokenManagement/Advanced/user_tokens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
+++
title = "Customizing User Token Management"
weight = 20
chapter = false
+++

The most common way to use the access token management for interactive web applications is described [here](web-applications) - however you may want to customise certain aspects of it - here's what you can do.

### General options
You can pass in some global options when registering token management in DI.

* `ChallengeScheme` - by default the OIDC configuration is inferred from the default challenge scheme. This is recommended approach. If for some reason your OIDC handler is not the default challenge scheme, you can set the scheme name on the options
* `UseChallengeSchemeScopedTokens` - the general assumption is that you only have one OIDC handler configured. If that is not the case, token management needs to maintain multiple sets of token artefacts simultaneously. You can opt-in to that feature using this setting.
* `ClientCredentialsScope` - when requesting client credentials tokens from the OIDC provider, the scope parameter will not be set since its value cannot be inferred from the OIDC configuration. With this setting you can set the value of the scope parameter.
* `ClientCredentialsResource` - same as previous, but for the resource parameter
* `ClientCredentialStyle` - specifies how client credentials are transmitted to the OIDC provider

```cs
builder.Services.AddOpenIdConnectAccessTokenManagement(options =>
{
options.ChallengeScheme = "schmeName";
options.UseChallengeSchemeScopedTokens = false;

options.ClientCredentialsScope = "api1 api2";
options.ClientCredentialsResource = "urn:resource";
options.ClientCredentialStyle = ClientCredentialStyle.PostBody;
});
```



### Per request parameters

You can also modify token management parameters on a per-request basis.

The `UserTokenRequestParameters` class can be used for that:

* `SignInScheme` - allows specifying a sign-in scheme. This is used by the default token store
* `ChallengeScheme` - allows specifying a challenge scheme. This is used to infer token service configuration
* `ForceRenewal` - forces token retrieval even if a cached token would be available
* `Scope` - overrides the globally configured scope parameter
* `Resource` - override the globally configured resource parameter
* `Assertion` - allows setting a client assertion for the request

The request parameters can be passed via the manual API

```cs
var token = await _tokenManagementService.GetAccessTokenAsync(User, new UserAccessTokenRequestParameters { ... });
```

..the extension methods

```cs
var token = await HttpContext.GetUserAccessTokenAsync(
new UserTokenRequestParameters { ... });
```

...or the HTTP client factory

```cs
// registers HTTP client that uses the managed user access token
builder.Services.AddUserAccessTokenHttpClient("invoices",
parameters: new UserTokenRequestParameters { ... },
configureClient: client =>
{
client.BaseAddress = new Uri("https://api.company.com/invoices/");
});

// registers a typed HTTP client with token management support
builder.Services.AddHttpClient<InvoiceClient>(client =>
{
client.BaseAddress = new Uri("https://api.company.com/invoices/");
})
.AddUserAccessTokenHandler(new UserTokenRequestParameters { ... });
```



### Token storage

By default the user's access and refresh token will be store in the ASP.NET Core autentication session (implemented by the cookie handler).

You can modify this in two ways

* the cookie handler itself has an extensible storage mechanism via the `TicketStore` mechanism
* replace the store altogether by providing an `IUserTokenStore` implementation
14 changes: 14 additions & 0 deletions FOSS/content/AccessTokenManagement/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
+++
title = "AccessTokenManagement"
weight = 10
chapter = true
+++

AccessTokenManagement
========

This library provides automatic access token management features for .NET worker and ASP.NET Core web applications

* automatic acquisition and lifetime management of client credentials based access tokens for machine to machine communication
* automatic access token lifetime management using a refresh token for API calls on-behalf of the currently logged-in user
* revocation of access tokens
Loading

0 comments on commit b881727

Please sign in to comment.