Skip to content

Commit 25a1f01

Browse files
authored
Merge pull request #224 from DuendeSoftware/beh/allow-client-assertions-for-atmopenidconnect
Allow client assertions for ATM OpenIdConnect
2 parents 2e6e044 + 29b6a2a commit 25a1f01

7 files changed

Lines changed: 69 additions & 13 deletions

File tree

access-token-management/samples/WebJarJwt/Startup.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Duende.AccessTokenManagement;
55
using Duende.AccessTokenManagement.OpenIdConnect;
6+
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
67
using Microsoft.IdentityModel.Tokens;
78
using Serilog;
89
using Serilog.Events;
@@ -53,6 +54,9 @@ internal static WebApplication ConfigureServices(this WebApplicationBuilder buil
5354
NameClaimType = "name",
5455
RoleClaimType = "role"
5556
};
57+
58+
// Disable PAR because it is incompatible with currently wired up OidcEvents
59+
options.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Disable;
5660
});
5761

5862
builder.Services.AddOpenIdConnectAccessTokenManagement();

access-token-management/samples/WebJarJwt/Views/Home/Secure.cshtml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
|
1717
<a href="./CallApiAsClientFactory">HTTP client factory</a>
1818
|
19-
<a href="./CallApiAsClientFactoryTypes">HTTP client factory (typed)</a>
19+
<a href="./CallApiAsClientFactoryTyped">HTTP client factory (typed)</a>
2020

2121

2222
<h2>Claims</h2>
@@ -37,4 +37,4 @@
3737
<dt>@prop.Key</dt>
3838
<dd>@prop.Value</dd>
3939
}
40-
</dl>
40+
</dl>

access-token-management/src/AccessTokenManagement.OpenIdConnect/Internal/OpenIdConnectConfigurationService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public async Task<OpenIdConnectClientConfiguration> GetOpenIdConnectConfiguratio
6060
TokenEndpoint = new Uri(configuration.TokenEndpoint),
6161
RevocationEndpoint = configuration.RevocationEndpoint == null ? null : new Uri(configuration.RevocationEndpoint),
6262
ClientId = ClientId.Parse(options.ClientId ?? throw new InvalidOperationException("ClientId is null")),
63-
ClientSecret = ClientSecret.Parse(options.ClientSecret ?? throw new InvalidOperationException("ClientSecret is null")),
63+
ClientSecret = options.ClientSecret == null ? null : ClientSecret.Parse(options.ClientSecret),
6464
HttpClient = options.Backchannel,
6565
};
6666
}

access-token-management/src/AccessTokenManagement.OpenIdConnect/OpenIdConnectClientConfiguration.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public sealed class OpenIdConnectClientConfiguration
3333
/// <summary>
3434
/// The client secret
3535
/// </summary>
36-
public required ClientSecret ClientSecret { get; set; }
36+
public ClientSecret? ClientSecret { get; set; }
3737

3838
/// <summary>
3939
/// The HTTP client associated with the OIDC handler (if based on scheme configuration)

access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ namespace Duende.AccessTokenManagement.Tests;
1818
public class AppHost : GenericHost
1919
{
2020
public string ClientId;
21+
public string? ClientSecret;
2122

2223
private readonly IdentityServerHost _identityServerHost;
2324
private readonly ApiHost _apiHost;
@@ -35,6 +36,7 @@ public AppHost(
3536
_identityServerHost = identityServerHost;
3637
_apiHost = apiHost;
3738
ClientId = clientId;
39+
ClientSecret = "secret";
3840
_configureUserTokenManagementOptions = configureUserTokenManagementOptions;
3941
OnConfigureServices += ConfigureServices;
4042
OnConfigure += Configure;
@@ -68,7 +70,7 @@ private void ConfigureServices(IServiceCollection services)
6870
options.Authority = _identityServerHost.Url();
6971

7072
options.ClientId = ClientId;
71-
options.ClientSecret = "secret";
73+
options.ClientSecret = ClientSecret;
7274
options.ResponseType = "code";
7375
options.ResponseMode = "query";
7476

access-token-management/test/AccessTokenManagement.Tests/PublicApiVerificationTests.VerifyPublicApi_OpenIdConnect.verified.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@
5050
public OpenIdConnectClientConfiguration() { }
5151
[System.Runtime.CompilerServices.RequiredMember]
5252
public Duende.AccessTokenManagement.ClientId ClientId { get; set; }
53-
[System.Runtime.CompilerServices.RequiredMember]
54-
public Duende.AccessTokenManagement.ClientSecret ClientSecret { get; set; }
53+
public Duende.AccessTokenManagement.ClientSecret? ClientSecret { get; set; }
5554
public System.Net.Http.HttpClient? HttpClient { get; set; }
5655
public System.Uri? RevocationEndpoint { get; set; }
5756
public Duende.AccessTokenManagement.Scheme Scheme { get; set; }
@@ -137,4 +136,4 @@
137136
public Duende.AccessTokenManagement.Scheme? ChallengeScheme { get; set; }
138137
public Duende.AccessTokenManagement.Scheme? SignInScheme { get; set; }
139138
}
140-
}
139+
}

access-token-management/test/AccessTokenManagement.Tests/UserTokenManagementTests.cs

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
using Duende.IdentityModel;
1010
using Duende.IdentityModel.Client;
11+
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
1112
using Microsoft.Extensions.DependencyInjection;
1213
using RichardSzalay.MockHttp;
1314
using OidcConstants = Duende.IdentityModel.OidcConstants;
@@ -78,10 +79,10 @@ Task<ClaimsPrincipal> LocalTransformPrincipalAsync(ClaimsPrincipal principal, Ca
7879
// the custom token transform
7980
await AppHost.BrowserClient!.GetAsync(AppHost.Url("/user_token"));
8081

81-
// Verify that the transform is used.
82+
// Verify that the transform is used.
8283
transformed.ShouldBeTrue();
8384

84-
// The transformed principal should now be used.
85+
// The transformed principal should now be used.
8586
var claims = await AppHost.BrowserClient!.GetFromJsonAsync<Dictionary<string, string>>(AppHost.Url("/user"));
8687
claims![JwtClaimTypes.Name].ShouldBe("transformed");
8788
}
@@ -235,7 +236,7 @@ public async Task Short_token_lifetime_should_trigger_refresh()
235236
// We mock the expiration of the first few token responses to be short
236237
// enough that we will automatically refresh immediately when attempting
237238
// to use the tokens, while the final response gets a long refresh time,
238-
// allowing us to verify that the token is not refreshed.
239+
// allowing us to verify that the token is not refreshed.
239240

240241
var mockHttp = new MockHttpMessageHandler();
241242
AppHost.IdentityServerHttpHandler = mockHttp;
@@ -337,7 +338,7 @@ public async Task Resources_get_distinct_tokens()
337338
.WithFormData("grant_type", "authorization_code")
338339
.Respond("application/json", JsonSerializer.Serialize(initialTokenResponse));
339340

340-
// resource 1 specified
341+
// resource 1 specified
341342
var resource1TokenResponse = new
342343
{
343344
access_token = "urn:api1_access_token",
@@ -350,7 +351,7 @@ public async Task Resources_get_distinct_tokens()
350351
.WithFormData("resource", "urn:api1")
351352
.Respond("application/json", JsonSerializer.Serialize(resource1TokenResponse));
352353

353-
// resource 2 specified
354+
// resource 2 specified
354355
var resource2TokenResponse = new
355356
{
356357
access_token = "urn:api2_access_token",
@@ -500,4 +501,54 @@ public async Task Logout_should_revoke_refresh_tokens()
500501
postLogoutIntrospectionResponse.IsActive.ShouldBeFalse();
501502

502503
}
504+
505+
[Fact]
506+
public async Task Can_request_user_token_using_client_assertions()
507+
{
508+
var mockHttp = new MockHttpMessageHandler();
509+
AppHost.IdentityServerHttpHandler = mockHttp;
510+
AppHost.ClientSecret = null;
511+
AppHost.OnConfigureServices += services =>
512+
{
513+
services.AddSingleton<IClientAssertionService>(new TestClientAssertionService("test", "service_type", "service_value"));
514+
services.PostConfigure<OpenIdConnectOptions>("oidc", options =>
515+
{
516+
options.Events.OnAuthorizationCodeReceived = async context =>
517+
{
518+
var clientAssertionService =
519+
context.HttpContext.RequestServices.GetRequiredService<IClientAssertionService>();
520+
var assertion = await clientAssertionService.GetClientAssertionAsync(ClientCredentialsClientName.Parse("test")) ?? throw new InvalidOperationException("Client assertion is null");
521+
522+
context.TokenEndpointRequest!.ClientAssertionType = assertion.Type;
523+
context.TokenEndpointRequest.ClientAssertion = assertion.Value;
524+
};
525+
});
526+
};
527+
var expectedRequestFormData = new Dictionary<string, string>
528+
{
529+
{ OidcConstants.TokenRequest.ClientAssertionType, "service_type" },
530+
{ OidcConstants.TokenRequest.ClientAssertion, "service_value" },
531+
};
532+
var initialTokenResponse = new
533+
{
534+
id_token = IdentityServerHost.CreateIdToken("1", "web"),
535+
access_token = "initial_access_token",
536+
token_type = "clientAssertionsWork",
537+
expires_in = 3600,
538+
refresh_token = "initial_refresh_token",
539+
};
540+
541+
// response for re-deeming code
542+
mockHttp.When("/connect/token")
543+
.WithFormData(expectedRequestFormData)
544+
.Respond("application/json", JsonSerializer.Serialize(initialTokenResponse));
545+
await InitializeAsync();
546+
await AppHost.LoginAsync("alice");
547+
548+
var response = await AppHost.BrowserClient.GetAsync(AppHost.Url("/user_token"));
549+
var token = await response.Content.ReadFromJsonAsync<UserTokenModel>();
550+
551+
token.ShouldNotBeNull();
552+
token.AccessTokenType.ShouldBe("clientAssertionsWork");
553+
}
503554
}

0 commit comments

Comments
 (0)