Skip to content

Issues when using Client Assertion in with ClientCredentialsTokenManagement #276

@jennyhougen

Description

@jennyhougen

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions