Skip to content
This repository has been archived by the owner on Nov 19, 2024. It is now read-only.

Commit

Permalink
Add Authority to ClientCredentials options
Browse files Browse the repository at this point in the history
If authority is set, we use it to retrieve the discovery document, and use that to configure the token endpoint.

Because this is an async operation, we have a new abstraction for retrieval of the token endpoint
  • Loading branch information
josephdecock committed May 3, 2024
1 parent 5d114aa commit cc00081
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ public void Configure(string? name, ClientCredentialsClient options)
}

var oidc = _configurationService.GetOpenIdConnectConfigurationAsync(scheme).GetAwaiter().GetResult();


options.Authority = oidc.Authority;
options.TokenEndpoint = oidc.TokenEndpoint;
options.ClientId = oidc.ClientId;
options.ClientSecret = oidc.ClientSecret;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

namespace Duende.AccessTokenManagement.OpenIdConnect;

// Note that this is duplicated in Duende.AccessTokenManagement, but we can't
// share the code because it is internal.
internal static class StringExtensions
{
[DebuggerStepThrough]
Expand All @@ -19,5 +21,4 @@ public static bool IsPresent([NotNullWhen(true)]this string? value)
{
return !string.IsNullOrWhiteSpace(value);
}

}
12 changes: 11 additions & 1 deletion src/Duende.AccessTokenManagement/ClientCredentialsClient.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http;
using IdentityModel.Client;
using Microsoft.Extensions.Options;

namespace Duende.AccessTokenManagement;

Expand All @@ -11,6 +15,12 @@ namespace Duende.AccessTokenManagement;
/// </summary>
public class ClientCredentialsClient
{
/// <summary>
/// The address of the OAuth authority. If this is set, the TokenEndpoint
/// will be set using discovery.
/// </summary>
public string? Authority { get; set; }

/// <summary>
/// The address of the token endpoint
/// </summary>
Expand Down Expand Up @@ -60,4 +70,4 @@ public class ClientCredentialsClient
/// The string representation of the JSON web key to use for DPoP.
/// </summary>
public string? DPoPJsonWebKey { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,27 @@ public class ClientCredentialsTokenEndpointService : IClientCredentialsTokenEndp
private readonly IClientAssertionService _clientAssertionService;
private readonly IDPoPKeyStore _dPoPKeyMaterialService;
private readonly IDPoPProofService _dPoPProofService;
private readonly ITokenEndpointRetriever _tokenEndpointRetriever;
private readonly ILogger<ClientCredentialsTokenEndpointService> _logger;

/// <summary>
/// ctor
/// </summary>
/// <param name="httpClientFactory"></param>
/// <param name="clientAssertionService"></param>
/// <param name="dPoPKeyMaterialService"></param>
/// <param name="dPoPProofService"></param>
/// <param name="logger"></param>
/// <param name="options"></param>
public ClientCredentialsTokenEndpointService(
IHttpClientFactory httpClientFactory,
IOptionsMonitor<ClientCredentialsClient> options,
IClientAssertionService clientAssertionService,
IDPoPKeyStore dPoPKeyMaterialService,
IDPoPProofService dPoPProofService,
ITokenEndpointRetriever tokenEndpointRetriever,
ILogger<ClientCredentialsTokenEndpointService> logger)
{
_httpClientFactory = httpClientFactory;
_options = options;
_clientAssertionService = clientAssertionService;
_dPoPKeyMaterialService = dPoPKeyMaterialService;
_dPoPProofService = dPoPProofService;
_tokenEndpointRetriever = tokenEndpointRetriever;
_logger = logger;
}

Expand All @@ -58,14 +55,14 @@ public virtual async Task<ClientCredentialsToken> RequestToken(
{
var client = _options.Get(clientName);

if (string.IsNullOrWhiteSpace(client.TokenEndpoint) || string.IsNullOrEmpty(client.ClientId))
if ((string.IsNullOrWhiteSpace(client.TokenEndpoint) && string.IsNullOrWhiteSpace(client.Authority))|| string.IsNullOrEmpty(client.ClientId))
{
throw new InvalidOperationException("unknown client");
}

var request = new ClientCredentialsTokenRequest
{
Address = client.TokenEndpoint,
Address = await _tokenEndpointRetriever.GetAsync(client),
Scope = client.Scope,
ClientId = client.ClientId,
ClientSecret = client.ClientSecret,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Duende.AccessTokenManagement;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.DependencyInjection;

Expand Down Expand Up @@ -38,6 +39,7 @@ public static ClientCredentialsTokenManagementBuilder AddClientCredentialsTokenM
public static ClientCredentialsTokenManagementBuilder AddClientCredentialsTokenManagement(this IServiceCollection services)
{
services.TryAddSingleton<ITokenRequestSynchronization, TokenRequestSynchronization>();
services.TryAddSingleton<ITokenEndpointRetriever, TokenEndpointRetriever>();

services.TryAddTransient<IClientCredentialsTokenManagementService, ClientCredentialsTokenManagementService>();
services.TryAddTransient<IClientCredentialsTokenCache, DistributedClientCredentialsTokenCache>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Threading.Tasks;

namespace Duende.AccessTokenManagement;

/// <summary>
/// Retrieves the token endpoint either using discovery or static configuration
/// </summary>
public interface ITokenEndpointRetriever
{
/// <summary>
/// Gets the token endpoint
/// </summary>
Task<string> GetAsync(ClientCredentialsClient client);
}
24 changes: 24 additions & 0 deletions src/Duende.AccessTokenManagement/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

namespace Duende.AccessTokenManagement;

// Note that this is duplicated in Duende.AccessTokenManagement.OpenIdConnect,
// but we can't share the code because it is internal.
internal static class StringExtensions
{
[DebuggerStepThrough]
public static bool IsMissing([NotNullWhen(false)]this string? value)
{
return string.IsNullOrWhiteSpace(value);
}

[DebuggerStepThrough]
public static bool IsPresent([NotNullWhen(true)]this string? value)
{
return !string.IsNullOrWhiteSpace(value);
}
}
45 changes: 45 additions & 0 deletions src/Duende.AccessTokenManagement/TokenEndpointRetriever.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using IdentityModel.Client;

namespace Duende.AccessTokenManagement;

/// <inheritdoc/>
public class TokenEndpointRetriever : ITokenEndpointRetriever
{
private readonly Dictionary<string, DiscoveryCache> _caches = new();

private DiscoveryCache GetDiscoCache(string authority)
{
if (!_caches.ContainsKey(authority))
{
_caches[authority] = new DiscoveryCache(authority);
}
return _caches[authority];
}

/// <inheritdoc/>
public async Task<string> GetAsync(ClientCredentialsClient client)
{
if (client.Authority.IsPresent())
{
var discoCache = GetDiscoCache(client.Authority);
var disco = await discoCache.GetAsync();
if(disco.IsError)
{
throw new InvalidOperationException("Failed to retrieve disco");
}
return disco.TokenEndpoint ?? throw new InvalidOperationException("Disco does not contain token endpoint");
}
else if (client.TokenEndpoint.IsPresent())
{
return client.TokenEndpoint;
}
else
{
throw new InvalidOperationException("No token endpoint or authority configured");
}

}
}
1 change: 1 addition & 0 deletions test/Tests/Framework/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ private void ConfigureServices(IServiceCollection services)
}
});

services.AddSingleton<ITokenEndpointRetriever>(new TestTokenEndpointRetriever(_identityServerHost.Url("/connect/token")));
}

private void Configure(IApplicationBuilder app)
Expand Down
12 changes: 12 additions & 0 deletions test/Tests/Framework/TestTokenEndpointRetreiver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

namespace Duende.AccessTokenManagement.Tests;

public class TestTokenEndpointRetriever(string tokenEndpoint = "https://identityserver/connect/token") : ITokenEndpointRetriever
{
public Task<string> GetAsync(ClientCredentialsClient client)
{
return Task.FromResult(tokenEndpoint);
}
}

0 comments on commit cc00081

Please sign in to comment.