From 472e3952b8e6acc7f8325447c99ec6ca574ba97b Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Fri, 15 Nov 2024 08:57:38 -0600 Subject: [PATCH] Add support for .NET 9 to access token management --- Directory.Packages.props | 12 ++++-- access-token-management/global.json | 2 +- ...AccessTokenManagement.OpenIdConnect.csproj | 2 +- .../OpenIdConnectConfigurationService.cs | 12 ++++++ .../AccessTokenManagement.csproj | 2 +- .../AccessTokenManagement.Tests.csproj | 7 +++- .../Framework/AppHost.cs | 6 ++- .../Framework/IdentityServerHost.cs | 3 ++ .../UserTokenManagementTests.cs | 38 +++++++++++++++++++ .../DPoP/DPoPProofTokenFactory.cs | 3 +- .../TokenRevocationExtensions.cs | 2 +- 11 files changed, 77 insertions(+), 12 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 36e67be3..b76f152a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,21 +11,24 @@ 6.0.1 6.0.0 - 7.1.2 - 6.1.8 + 6.35.0 + 6.3.10 8.0.1 8.0.0 - 7.1.2 + + + [7.1.2,8.0.0) + 7.0.8 9.0.0 9.0.0 - 8.0.0 + 8.0.1 7.0.8 @@ -57,6 +60,7 @@ + diff --git a/access-token-management/global.json b/access-token-management/global.json index 72d38cd2..30094e81 100644 --- a/access-token-management/global.json +++ b/access-token-management/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.100", + "version": "9.0.100", "rollForward": "latestMajor", "allowPrerelease": false } diff --git a/access-token-management/src/AccessTokenManagement.OpenIdConnect/AccessTokenManagement.OpenIdConnect.csproj b/access-token-management/src/AccessTokenManagement.OpenIdConnect/AccessTokenManagement.OpenIdConnect.csproj index d3b83bcd..21017f93 100644 --- a/access-token-management/src/AccessTokenManagement.OpenIdConnect/AccessTokenManagement.OpenIdConnect.csproj +++ b/access-token-management/src/AccessTokenManagement.OpenIdConnect/AccessTokenManagement.OpenIdConnect.csproj @@ -1,6 +1,6 @@ - net8.0 + net8.0;net9.0 Duende.AccessTokenManagement.OpenIdConnect $(PackageId) $(PackageId) diff --git a/access-token-management/src/AccessTokenManagement.OpenIdConnect/OpenIdConnectConfigurationService.cs b/access-token-management/src/AccessTokenManagement.OpenIdConnect/OpenIdConnectConfigurationService.cs index b5c6bbf2..98faf591 100644 --- a/access-token-management/src/AccessTokenManagement.OpenIdConnect/OpenIdConnectConfigurationService.cs +++ b/access-token-management/src/AccessTokenManagement.OpenIdConnect/OpenIdConnectConfigurationService.cs @@ -71,7 +71,19 @@ public async Task GetOpenIdConnectConfiguratio Authority = options.Authority, TokenEndpoint = configuration.TokenEndpoint, + + // This conditional compilation is required because the + // OpenIdConnectConfiguration type in + // Microsoft.IdentityModel.Protocols.OpenIdConnect has a breaking + // change in version 8.0.0 that library. The revocation endpoint was + // added as a strongly typed property, which means it is no longer + // included in the AdditionalData. In our .NET 9 build, we require + // wilson >8.0.0, and in our .NET 8 build, we require wilson <8.0.0. +#if NET9_0_OR_GREATER + RevocationEndpoint = configuration.RevocationEndpoint, +#else RevocationEndpoint = configuration.AdditionalData.TryGetValue(OidcConstants.Discovery.RevocationEndpoint, out var value) ? value?.ToString() : null, +#endif ClientId = options.ClientId, ClientSecret = options.ClientSecret, diff --git a/access-token-management/src/AccessTokenManagement/AccessTokenManagement.csproj b/access-token-management/src/AccessTokenManagement/AccessTokenManagement.csproj index 815b7bf1..9484e426 100644 --- a/access-token-management/src/AccessTokenManagement/AccessTokenManagement.csproj +++ b/access-token-management/src/AccessTokenManagement/AccessTokenManagement.csproj @@ -1,6 +1,6 @@ - net8.0 + net8.0;net9.0 Duende.AccessTokenManagement $(PackageId) $(PackageId) diff --git a/access-token-management/test/AccessTokenManagement.Tests/AccessTokenManagement.Tests.csproj b/access-token-management/test/AccessTokenManagement.Tests/AccessTokenManagement.Tests.csproj index 3341f822..d4c0b974 100644 --- a/access-token-management/test/AccessTokenManagement.Tests/AccessTokenManagement.Tests.csproj +++ b/access-token-management/test/AccessTokenManagement.Tests/AccessTokenManagement.Tests.csproj @@ -1,6 +1,6 @@  - net8.0 + net8.0;net9.0 enable enable Duende.AccessTokenManagement @@ -14,6 +14,11 @@ + + + + diff --git a/access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs b/access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs index e16477c3..9dfc182f 100644 --- a/access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs +++ b/access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs @@ -58,6 +58,11 @@ private void ConfigureServices(IServiceCollection services) }) .AddOpenIdConnect("oidc", options => { + options.Events.OnRedirectToIdentityProviderForSignOut = async e => + { + await e.HttpContext.RevokeRefreshTokenAsync(); + }; + options.Authority = _identityServerHost.Url(); options.ClientId = ClientId; @@ -212,7 +217,6 @@ public async Task LogoutAsync(string? sid = null) response = await BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe((HttpStatusCode)302); // root - response.Headers.Location!.ToString().ToLowerInvariant().ShouldBe("/"); response = await BrowserClient.GetAsync(Url(response.Headers.Location.ToString())); return response; diff --git a/access-token-management/test/AccessTokenManagement.Tests/Framework/IdentityServerHost.cs b/access-token-management/test/AccessTokenManagement.Tests/Framework/IdentityServerHost.cs index 52c8b9fd..4e44ca68 100644 --- a/access-token-management/test/AccessTokenManagement.Tests/Framework/IdentityServerHost.cs +++ b/access-token-management/test/AccessTokenManagement.Tests/Framework/IdentityServerHost.cs @@ -53,6 +53,9 @@ private void ConfigureServices(IServiceCollection services) // Artificially low durations to force retries options.DPoP.ServerClockSkew = TimeSpan.Zero; options.DPoP.ProofTokenValidityDuration = TimeSpan.FromSeconds(1); + + // Disable PAR (this keeps test setup simple, and we don't need to integration test PAR here - it is covered by IdentityServer itself) + options.Endpoints.EnablePushedAuthorizationEndpoint = false; }) .AddInMemoryClients(Clients) .AddInMemoryIdentityResources(IdentityResources) diff --git a/access-token-management/test/AccessTokenManagement.Tests/UserTokenManagementTests.cs b/access-token-management/test/AccessTokenManagement.Tests/UserTokenManagementTests.cs index f64f47ce..b9bf122f 100644 --- a/access-token-management/test/AccessTokenManagement.Tests/UserTokenManagementTests.cs +++ b/access-token-management/test/AccessTokenManagement.Tests/UserTokenManagementTests.cs @@ -4,6 +4,8 @@ using System.Net.Http.Json; using System.Text.Json; using Duende.AccessTokenManagement.OpenIdConnect; +using Duende.IdentityModel; +using IdentityModel.Client; using RichardSzalay.MockHttp; namespace Duende.AccessTokenManagement.Tests; @@ -421,4 +423,40 @@ public async Task Multiple_users_have_distinct_tokens_across_refreshes() thirdToken.sub.ShouldNotBe(secondToken.sub); thirdToken.token.ShouldNotBe(firstToken.token); } + + + [Fact] + public async Task Logout_should_revoke_refresh_tokens() + { + await AppHost.InitializeAsync(); + await AppHost.LoginAsync("alice"); + + var response = await AppHost.BrowserClient.GetAsync(AppHost.Url("/user_token")); + var token = await response.Content.ReadFromJsonAsync(); + var refreshToken = token?.RefreshToken; + + refreshToken.ShouldNotBeNull(); + + var introspectionParams = new TokenIntrospectionRequest + { + Token = refreshToken, + TokenTypeHint = OidcConstants.TokenTypes.RefreshToken, + ClientId = "web", + ClientSecret = "secret", + Address = IdentityServerHost.Url("/connect/introspect") + }; + + var introspectionResponse = await IdentityServerHost.HttpClient.IntrospectTokenAsync(introspectionParams); + introspectionResponse.ShouldNotBeNull(); + introspectionResponse.IsError.ShouldBeFalse(introspectionResponse.Error); + introspectionResponse.IsActive.ShouldBeTrue(); + + await AppHost.BrowserClient.GetAsync(AppHost.Url("/logout")); + + var postLogoutIntrospectionResponse = await IdentityServerHost.HttpClient.IntrospectTokenAsync(introspectionParams); + postLogoutIntrospectionResponse.ShouldNotBeNull(); + postLogoutIntrospectionResponse.IsError.ShouldBeFalse(introspectionResponse.Error); + postLogoutIntrospectionResponse.IsActive.ShouldBeFalse(); + + } } \ No newline at end of file diff --git a/identity-model-oidc-client/src/IdentityModel.OidcClient.Extensions/DPoP/DPoPProofTokenFactory.cs b/identity-model-oidc-client/src/IdentityModel.OidcClient.Extensions/DPoP/DPoPProofTokenFactory.cs index 93f98008..9ba42b63 100644 --- a/identity-model-oidc-client/src/IdentityModel.OidcClient.Extensions/DPoP/DPoPProofTokenFactory.cs +++ b/identity-model-oidc-client/src/IdentityModel.OidcClient.Extensions/DPoP/DPoPProofTokenFactory.cs @@ -4,7 +4,6 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; -using Duende.IdentityModel; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; @@ -24,7 +23,7 @@ public DPoPProofTokenFactory(string proofKey) { _jwk = new JsonWebKey(proofKey); - if (_jwk.Alg.IsNullOrEmpty()) + if (string.IsNullOrEmpty(_jwk.Alg)) { throw new ArgumentException("alg must be set on proof key"); } diff --git a/identity-model/test/IdentityModel.Tests/HttpClientExtensions/TokenRevocationExtensions.cs b/identity-model/test/IdentityModel.Tests/HttpClientExtensions/TokenRevocationExtensions.cs index 3cfb7f60..9b5a486d 100644 --- a/identity-model/test/IdentityModel.Tests/HttpClientExtensions/TokenRevocationExtensions.cs +++ b/identity-model/test/IdentityModel.Tests/HttpClientExtensions/TokenRevocationExtensions.cs @@ -12,7 +12,7 @@ namespace Duende.IdentityModel.HttpClientExtensions { public class TokenRevocationExtensionsTests { - private const string Endpoint = "http://server/endoint"; + private const string Endpoint = "http://server/endpoint"; [Fact] public async Task Http_request_should_have_correct_format()