-
Notifications
You must be signed in to change notification settings - Fork 35
Description
Affected component
Duende.AccessTokenManagement
Version
4.0.1
Describe the bug
We have had some issues getting Client assertion to work:
Issue 1: Missing adding additional service to ClientCredentialsClient options
As we have an OIDC provider we want to get token endpoint from Discovery endpoint. The AddClient does not allow for aditional services when configuring options. In addition the name AddClient
is a bit confusing. Would rather prefer AddTokenClientOption, AddClientCredentialsOption or ConfigureClientCredentials
We have done a "workaround" now:
services
.AddOptions<ClientCredentialsClient>("TokenClientName")
.Configure<IDiscoveryDocumentStore>((options, discoveryStore) =>
{
var discoveryDocument = discoveryStore.Get(apiOption.ClientAuthentication.Authority);
options.TokenEndpoint = discoveryDocument?.TokenEndpoint is not null ? new Uri(discoveryDocument.TokenEndpoint) : null;
options.ClientId = ClientId.Parse(apiOption.ClientAuthentication.ClientId);
options.Scope = Scope.Parse(apiOption.ClientAuthentication.Scope);
.....
})
Issue 2: Using ClientCredentials parameter to set ClientAssertion
ClientAssertion jwt has an expiration time. When using ClientAssertion parameter token request will fail after the access_token is expired because the assertion is expired.
Can this be solved by using IOptionsSnapshot instead of IOptionsMonitor in the ClientCredentialsTokenClient
?
builder.Services
.AddClientCredentialsTokenManagement()
.AddClient(clientConfiguration.ClientName + ".clientassertion", options =>
{
options.TokenEndpoint = clientConfiguration.TokenEndpoint;
options.ClientId = clientConfiguration.ClientId;
options.Scope = clientConfiguration.Scope;
options.Parameters = new Parameters()
{
{ OidcConstants.TokenRequest.ClientAssertionType, OidcConstants.ClientAssertionTypes.JwtBearer },
{ OidcConstants.TokenRequest.ClientAssertion, ClientAssertionTokenHandler.CreateJwtToken(clientConfiguration.Authority, clientConfiguration.ClientId, clientConfiguration.Secret) }
};
});
Issue 3: Adding aditional parameters to ClientCredentialsClient
We tried to add private JWK parameter to the options.Parameters so it could be used in a ClientAssertionService to get privateJwk to genereate ClientAssertion. But then the privateJwk parameter will be added to the token request. The solution was to add an separate Option for Clientasseriton paramteres to be used in the ClientAssertionService
services
.AddOptions<ClientAssertionOptions>(helseIdProtectedApi!.ClientName)
.Configure<IDiscoveryDocumentStore>((options, discoveryStore) =>
{
var discoveryDocument = discoveryStore.Get(helseIdProtectedApi!.Authentication.Authority);
options.Issuer = discoveryDocument?.Issuer ?? string.Empty;
options.PrivateJwk = helseIdProtectedApi.Authentication.PrivateJwk;
});
services
.AddOptions<ClientCredentialsClient>(helseIdProtectedApi!.ClientName)
.Configure<IDiscoveryDocumentStore>((options, discoveryStore) =>
{
var discoveryDocument = discoveryStore.Get(helseIdProtectedApi!.Authentication.Authority);
options.TokenEndpoint = discoveryDocument?.TokenEndpoint is not null ? new Uri(discoveryDocument.TokenEndpoint) : null;
options.ClientId = ClientId.Parse(helseIdProtectedApi.Authentication.ClientId);
options.Scope = Scope.Parse(helseIdProtectedApi.Authentication.Scope);
var assertion = ClientAssertionTokenHandler.CreateJwtToken(discoveryDocument?.Issuer!, helseIdProtectedApi.Authentication.ClientId, helseIdProtectedApi.Authentication.PrivateJwk);
options.Parameters = new Parameters()
{
{ OidcConstants.TokenRequest.ClientAssertionType, OidcConstants.ClientAssertionTypes.JwtBearer },
{ OidcConstants.TokenRequest.ClientAssertion, assertion }
};
//To enable DPoP
//options.DPoPJsonWebKey = helseIdProtectedApi.Authentication.PrivateJwk;
});
services.AddClientCredentialsHttpClient(helseIdProtectedApi!.ClientName, ClientCredentialsClientName.Parse(helseIdProtectedApi.ClientName), client =>
{
client.BaseAddress = new Uri(helseIdProtectedApi?.BaseAddress!);
});
ClientAssertionService:
public class ClientCredentialsAssertionService : IClientAssertionService
{
private readonly ILogger<ClientCredentialsAssertionService> _logger;
private readonly IOptionsMonitor<ClientAssertionOptions> _clientAssertionOptions;
private readonly IOptionsMonitor<ClientCredentialsClient> _clientCredentialsClient;
/// <inheritdoc/>
public ClientCredentialsAssertionService(
ILogger<ClientCredentialsAssertionService> logger,
IOptionsMonitor<ClientAssertionOptions> clientAssertionOptions,
IOptionsMonitor<ClientCredentialsClient> clientCredentialsClient)
{
_logger = logger;
_clientAssertionOptions = clientAssertionOptions;
_clientCredentialsClient = clientCredentialsClient;
}
/// <inheritdoc/>
public Task<ClientAssertion?> GetClientAssertionAsync(ClientCredentialsClientName? clientName = null, TokenRequestParameters? parameters = null, CancellationToken ct = default)
{
var client = _clientCredentialsClient.Get(clientName);
if (client != null && client.ClientSecret == null)
{
var clientAssertionOptions = _clientAssertionOptions.Get(clientName);
if (string.IsNullOrEmpty(clientAssertionOptions.Issuer))
{
_logger.LogError("Could not resolve issuer for {clientName}. Missing parameter", clientName);
return Task.FromResult<ClientAssertion?>(null);
}
if (string.IsNullOrEmpty(clientAssertionOptions.PrivateJwk))
{
_logger.LogError("Could not resolve JWK for {clientName}. Missing parameter", clientName);
return Task.FromResult<ClientAssertion?>(null);
}
var jwt = ClientAssertionTokenHandler.CreateJwtToken(clientAssertionOptions.Issuer, client?.ClientId ?? "", clientAssertionOptions.PrivateJwk);
return Task.FromResult<ClientAssertion?>(new ClientAssertion
{
Type = clientAssertionOptions.ClientAssertionType,
Value = jwt
});
}
if (client is null) _logger.LogError("Could not resolve options for client {clientName}", clientName);
return Task.FromResult<ClientAssertion?>(null);
}
}
Steps to reproduce
See description above.
Expected behavior
See the description above.
Additional context
No response