diff --git a/access-token-management/test/AccessTokenManagement.Tests/AccessTokenHandlerTests.cs b/access-token-management/test/AccessTokenManagement.Tests/AccessTokenHandlerTests.cs index a254e2110..3eb7f67b8 100644 --- a/access-token-management/test/AccessTokenManagement.Tests/AccessTokenHandlerTests.cs +++ b/access-token-management/test/AccessTokenManagement.Tests/AccessTokenHandlerTests.cs @@ -12,9 +12,9 @@ public class AccessTokenHandlerTests AccessTokenHandlerSubject _subject; - public AccessTokenHandlerTests() + public AccessTokenHandlerTests(ITestOutputHelper output) { - _subject = new AccessTokenHandlerSubject(_testDPoPProofService, new TestDPoPNonceStore(), new TestLoggerProvider().CreateLogger("AccessTokenHandlerSubject")); + _subject = new AccessTokenHandlerSubject(_testDPoPProofService, new TestDPoPNonceStore(), new TestLoggerProvider(output.WriteLine, "AccessTokenHandler").CreateLogger("AccessTokenHandlerSubject")); _subject.InnerHandler = _testHttpMessageHandler; } diff --git a/access-token-management/test/AccessTokenManagement.Tests/ClientTokenManagementApiTests.cs b/access-token-management/test/AccessTokenManagement.Tests/ClientTokenManagementApiTests.cs index be19894e8..861b3c276 100644 --- a/access-token-management/test/AccessTokenManagement.Tests/ClientTokenManagementApiTests.cs +++ b/access-token-management/test/AccessTokenManagement.Tests/ClientTokenManagementApiTests.cs @@ -11,25 +11,26 @@ namespace Duende.AccessTokenManagement.Tests; -public class ClientTokenManagementApiTests : IntegrationTestBase +public class ClientTokenManagementApiTests(ITestOutputHelper output) : IntegrationTestBase(output), IAsyncLifetime { - private static readonly string _jwkJson; + private static readonly string _jwkJson = CreateJWKJson(); - private HttpClient _client; - private IClientCredentialsTokenManagementService _tokenService; - private IHttpClientFactory _clientFactory; - private ClientCredentialsClient _clientOptions; + private IClientCredentialsTokenManagementService _tokenService = null!; + private IHttpClientFactory _clientFactory = null!; + private ClientCredentialsClient _clientOptions = null!; - static ClientTokenManagementApiTests() + private static string CreateJWKJson() { var key = CryptoHelper.CreateRsaSecurityKey(); var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(key); jwk.Alg = "RS256"; - _jwkJson = JsonSerializer.Serialize(jwk); + var jwkJson = JsonSerializer.Serialize(jwk); + return jwkJson; } - public ClientTokenManagementApiTests() + public override async ValueTask InitializeAsync() { + await base.InitializeAsync(); var services = new ServiceCollection(); services.AddDistributedMemoryCache(); @@ -50,11 +51,11 @@ public ClientTokenManagementApiTests() }); var provider = services.BuildServiceProvider(); - _client = provider.GetRequiredService().CreateClient("test"); _tokenService = provider.GetRequiredService(); _clientFactory = provider.GetRequiredService(); _clientOptions = provider.GetRequiredService>().Get("test"); } + public class ApiHandler : DelegatingHandler { private HttpMessageHandler? _innerHandler; diff --git a/access-token-management/test/AccessTokenManagement.Tests/Framework/ApiHost.cs b/access-token-management/test/AccessTokenManagement.Tests/Framework/ApiHost.cs index 5d6014503..a0ef01c5d 100644 --- a/access-token-management/test/AccessTokenManagement.Tests/Framework/ApiHost.cs +++ b/access-token-management/test/AccessTokenManagement.Tests/Framework/ApiHost.cs @@ -15,8 +15,13 @@ public class ApiHost : GenericHost private readonly IdentityServerHost _identityServerHost; public event Action ApiInvoked = ctx => { }; - public ApiHost(IdentityServerHost identityServerHost, string scope, string baseAddress = "https://api", string resource = "urn:api") - : base(baseAddress) + public ApiHost( + WriteTestOutput writeTestOutput, + IdentityServerHost identityServerHost, + string scope, + string baseAddress = "https://api", + string resource = "urn:api") + : base(writeTestOutput, baseAddress) { _identityServerHost = identityServerHost; _identityServerHost.ApiScopes.Add(new ApiScope(scope)); diff --git a/access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs b/access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs index d11870d2b..ed953fc0c 100644 --- a/access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs +++ b/access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs @@ -11,9 +11,65 @@ using Duende.AccessTokenManagement.OpenIdConnect; using RichardSzalay.MockHttp; using System.Net.Http.Json; +using System.Security.Claims; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging; namespace Duende.AccessTokenManagement.Tests; +public class EagerTokenRefresher( + IStoreTokensInAuthenticationProperties tokensInProps, + IOptions options, + IUserTokenRequestSynchronization sync, + IUserTokenEndpointService tokenEndpointService, + IUserTokenStore userAccessTokenStore, + TimeProvider clock, + ILogger logger) + +{ + public async Task RefreshTokenIfNeeded(ClaimsPrincipal? user, AuthenticationProperties contextProperties, + CancellationToken cancellationToken) + { + var userToken = tokensInProps.GetUserToken(contextProperties); + var dtRefresh = userToken.Expiration.Subtract(options.Value.RefreshBeforeExpiration); + var utcNow = clock.GetUtcNow(); + + if (userToken.AccessToken == null || userToken.RefreshToken == null) + return; + + var parameters = new UserTokenRequestParameters(); + + if (dtRefresh < utcNow) + { + await sync.SynchronizeAsync(userToken.RefreshToken!, async () => + { + try + { + var refreshedToken = + await tokenEndpointService.RefreshAccessTokenAsync(userToken, parameters, cancellationToken).ConfigureAwait(false); + if (refreshedToken.IsError) + { + logger.LogError("Error refreshing access token. Error = {error}", refreshedToken.Error); + } + else + { + tokensInProps.SetUserToken(refreshedToken, contextProperties, parameters); + } + + } + catch (Exception ex) + { + + Console.WriteLine("Exception: " + ex.ToString()); + } + return null; + }).ConfigureAwait(false); + } + + } + +} + public class AppHost : GenericHost { public string ClientId; @@ -22,13 +78,16 @@ public class AppHost : GenericHost private readonly ApiHost _apiHost; private readonly Action? _configureUserTokenManagementOptions; + public bool AutoRefreshToken { get; set; } = false; + public AppHost( + WriteTestOutput writeTestOutput, IdentityServerHost identityServerHost, ApiHost apiHost, string clientId, string baseAddress = "https://app", Action? configureUserTokenManagementOptions = default) - : base(baseAddress) + : base(writeTestOutput, baseAddress) { _identityServerHost = identityServerHost; _apiHost = apiHost; @@ -44,11 +103,22 @@ private void ConfigureServices(IServiceCollection services) { services.AddRouting(); services.AddAuthorization(); + services.AddTransient(); services.AddAuthentication("cookie") .AddCookie("cookie", options => { options.Cookie.Name = "bff"; + + options.Events.OnValidatePrincipal += async context => + { + if (AutoRefreshToken) + { + var refresher = context.HttpContext.RequestServices.GetRequiredService(); + + await refresher.RefreshTokenIfNeeded(context.Principal, context.Properties, context.HttpContext.RequestAborted); + } + }; }); services.AddAuthentication(options => @@ -93,11 +163,11 @@ private void ConfigureServices(IServiceCollection services) IdentityServerHttpHandler.When("/.well-known/*") .Respond(identityServerHandler); - options.BackchannelHttpHandler = IdentityServerHttpHandler; + options.BackchannelHttpHandler = new LoggingHttpHandler(IdentityServerHttpHandler); } else { - options.BackchannelHttpHandler = identityServerHandler; + options.BackchannelHttpHandler = new LoggingHttpHandler(identityServerHandler); } options.ProtocolValidator.RequireNonce = false; @@ -117,7 +187,7 @@ private void ConfigureServices(IServiceCollection services) services.AddUserAccessTokenHttpClient("callApi", configureClient: client => { client.BaseAddress = new Uri(_apiHost.Url()); }) - .ConfigurePrimaryHttpMessageHandler(() => _apiHost.HttpMessageHandler); + .ConfigurePrimaryHttpMessageHandler(() => new LoggingHttpHandler(_apiHost.HttpMessageHandler)); } private void Configure(IApplicationBuilder app) @@ -221,4 +291,25 @@ public async Task LogoutAsync(string? sid = null) response = await BrowserClient.GetAsync(Url(response.Headers.Location!.ToString())); return response; } +} + +public class LoggingHttpHandler(HttpMessageHandler inner) : DelegatingHandler(inner) +{ + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + try + { + Console.WriteLine("--> " + request.RequestUri!.ToString()); + + var response = await base.SendAsync(request, cancellationToken); + + Console.WriteLine("<-- " + response.RequestMessage!.RequestUri!.ToString() + " - " + response.StatusCode); + return response; + } + catch (Exception ex) + { + Console.WriteLine("Exception: " + ex.ToString()); + throw; + } + } } \ No newline at end of file diff --git a/access-token-management/test/AccessTokenManagement.Tests/Framework/GenericHost.cs b/access-token-management/test/AccessTokenManagement.Tests/Framework/GenericHost.cs index 47e70f63c..510c5e908 100644 --- a/access-token-management/test/AccessTokenManagement.Tests/Framework/GenericHost.cs +++ b/access-token-management/test/AccessTokenManagement.Tests/Framework/GenericHost.cs @@ -14,15 +14,13 @@ namespace Duende.AccessTokenManagement.Tests; -public class GenericHost +public class GenericHost(WriteTestOutput writeOutput, string baseAddress = "https://server") : IAsyncDisposable { - public GenericHost(string baseAddress = "https://server") - { - if (baseAddress.EndsWith("/")) baseAddress = baseAddress.Substring(0, baseAddress.Length - 1); - _baseAddress = baseAddress; - } - protected readonly string _baseAddress; + protected readonly string BaseAddress = baseAddress.EndsWith("/") + ? baseAddress.Substring(0, baseAddress.Length - 1) + : baseAddress; + IServiceProvider _appServices = default!; public Assembly HostAssembly { get; set; } = default!; @@ -33,7 +31,8 @@ public GenericHost(string baseAddress = "https://server") public HttpClient HttpClient { get; set; } = default!; public HttpMessageHandler HttpMessageHandler { get; set; } = default!; - public TestLoggerProvider Logger { get; set; } = new TestLoggerProvider(); + private TestLoggerProvider Logger { get; } = new(writeOutput, baseAddress + " - "); + public T Resolve() @@ -47,11 +46,13 @@ public string Url(string? path = null) { path = path ?? String.Empty; if (!path.StartsWith("/")) path = "/" + path; - return _baseAddress + path; + return BaseAddress + path; } public async Task InitializeAsync() { + if (Server != null) throw new InvalidOperationException("Already initialized"); + var hostBuilder = new HostBuilder() .ConfigureWebHost(builder => { @@ -178,4 +179,23 @@ public Task IssueSessionCookieAsync(string sub, params Claim[] claims) { return IssueSessionCookieAsync(claims.Append(new Claim("sub", sub)).ToArray()); } + + public async ValueTask DisposeAsync() + { + await CastAndDispose(Server); + await CastAndDispose(BrowserClient); + await CastAndDispose(HttpClient); + await CastAndDispose(HttpMessageHandler); + await CastAndDispose(Logger); + + return; + + static async ValueTask CastAndDispose(IDisposable resource) + { + if (resource is IAsyncDisposable resourceAsyncDisposable) + await resourceAsyncDisposable.DisposeAsync(); + else + resource?.Dispose(); + } + } } \ No newline at end of file diff --git a/access-token-management/test/AccessTokenManagement.Tests/Framework/IdentityServerHost.cs b/access-token-management/test/AccessTokenManagement.Tests/Framework/IdentityServerHost.cs index 4e44ca689..162217483 100644 --- a/access-token-management/test/AccessTokenManagement.Tests/Framework/IdentityServerHost.cs +++ b/access-token-management/test/AccessTokenManagement.Tests/Framework/IdentityServerHost.cs @@ -15,8 +15,8 @@ namespace Duende.AccessTokenManagement.Tests; public class IdentityServerHost : GenericHost { - public IdentityServerHost(string baseAddress = "https://identityserver") - : base(baseAddress) + public IdentityServerHost(WriteTestOutput writeTestOutput, string baseAddress = "https://identityserver") + : base(writeTestOutput, baseAddress) { OnConfigureServices += ConfigureServices; OnConfigure += Configure; @@ -108,7 +108,7 @@ public string CreateIdToken(string sub, string clientId) { var descriptor = new SecurityTokenDescriptor { - Issuer = _baseAddress, + Issuer = BaseAddress, Audience = clientId, Claims = new Dictionary { diff --git a/access-token-management/test/AccessTokenManagement.Tests/Framework/IntegrationTestBase.cs b/access-token-management/test/AccessTokenManagement.Tests/Framework/IntegrationTestBase.cs index 8edda45fa..e0baba823 100644 --- a/access-token-management/test/AccessTokenManagement.Tests/Framework/IntegrationTestBase.cs +++ b/access-token-management/test/AccessTokenManagement.Tests/Framework/IntegrationTestBase.cs @@ -8,15 +8,15 @@ namespace Duende.AccessTokenManagement.Tests; -public class IntegrationTestBase +public class IntegrationTestBase : IAsyncDisposable { protected readonly IdentityServerHost IdentityServerHost; protected ApiHost ApiHost; protected AppHost AppHost; - public IntegrationTestBase(string clientId = "web", Action? configureUserTokenManagementOptions = null) + public IntegrationTestBase(ITestOutputHelper output, string clientId = "web", Action? configureUserTokenManagementOptions = null) { - IdentityServerHost = new IdentityServerHost(); + IdentityServerHost = new IdentityServerHost(output.WriteLine); IdentityServerHost.Clients.Add(new Client { @@ -67,17 +67,27 @@ public IntegrationTestBase(string clientId = "web", Action(TState state) - where TState : notnull + public IDisposable BeginScope(TState state) where TState : notnull { return this; } @@ -40,10 +44,19 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except } } - public List LogEntries = new List(); + public List LogEntries { get; } = new(); private void Log(string msg) { + try + { + Console.WriteLine(_watch.Elapsed.TotalMilliseconds.ToString("0000") + "ms - " + _name + msg); + //_writeOutput?.Invoke(_watch.Elapsed.TotalMilliseconds.ToString("0000") + "ms - " + _name + msg); + } + catch (Exception) + { + Console.WriteLine("Logging Failed: " + msg); + } LogEntries.Add(msg); } @@ -55,4 +68,6 @@ public ILogger CreateLogger(string categoryName) public void Dispose() { } -} \ No newline at end of file +} + +public delegate void WriteTestOutput(string message); diff --git a/access-token-management/test/AccessTokenManagement.Tests/UserTokenManagementTests.cs b/access-token-management/test/AccessTokenManagement.Tests/UserTokenManagementTests.cs index 2b27c5749..d3cc3f559 100644 --- a/access-token-management/test/AccessTokenManagement.Tests/UserTokenManagementTests.cs +++ b/access-token-management/test/AccessTokenManagement.Tests/UserTokenManagementTests.cs @@ -10,14 +10,25 @@ namespace Duende.AccessTokenManagement.Tests; -public class UserTokenManagementTests : IntegrationTestBase +public class MyTest(ITestOutputHelper output) { - public UserTokenManagementTests() : base("web") - { } + [Fact] + public void Can_Write() + { + for (int i = 0; i < 100; i++) + { + output.WriteLine(new string('a', 100)); + } + } + +} +public class UserTokenManagementTests(ITestOutputHelper output) : IntegrationTestBase(output, "web") +{ [Fact] public async Task Anonymous_user_should_return_user_token_error() { + await InitializeAsync(); var response = await AppHost.BrowserClient!.GetAsync(AppHost.Url("/user_token")); var token = await response.Content.ReadFromJsonAsync(); @@ -27,6 +38,7 @@ public async Task Anonymous_user_should_return_user_token_error() [Fact] public async Task Anonymous_user_should_return_client_token() { + await InitializeAsync(); var response = await AppHost.BrowserClient!.GetAsync(AppHost.Url("/client_token")); var token = await response.Content.ReadFromJsonAsync(); @@ -57,7 +69,7 @@ public async Task Standard_initial_token_response_should_return_expected_values( .WithFormData("grant_type", "authorization_code") .Respond("application/json", JsonSerializer.Serialize(initialTokenResponse)); - await AppHost.InitializeAsync(); + await InitializeAsync(); await AppHost.LoginAsync("alice"); // 1st request @@ -102,7 +114,7 @@ public async Task Missing_expires_in_should_result_in_long_lived_token() .WithFormData("grant_type", "authorization_code") .Respond("application/json", JsonSerializer.Serialize(initialTokenResponse)); - await AppHost.InitializeAsync(); + await InitializeAsync(); await AppHost.LoginAsync("alice"); var response = await AppHost.BrowserClient.GetAsync(AppHost.Url("/user_token")); @@ -135,7 +147,7 @@ public async Task Missing_initial_refresh_token_response_should_return_access_to .WithFormData("grant_type", "authorization_code") .Respond("application/json", JsonSerializer.Serialize(initialTokenResponse)); - await AppHost.InitializeAsync(); + await InitializeAsync(); await AppHost.LoginAsync("alice"); var response = await AppHost.BrowserClient.GetAsync(AppHost.Url("/user_token")); @@ -168,7 +180,7 @@ public async Task Missing_initial_refresh_token_and_expired_access_token_should_ .WithFormData("grant_type", "authorization_code") .Respond("application/json", JsonSerializer.Serialize(initialTokenResponse)); - await AppHost.InitializeAsync(); + await InitializeAsync(); await AppHost.LoginAsync("alice"); var response = await AppHost.BrowserClient.GetAsync(AppHost.Url("/user_token")); @@ -236,7 +248,7 @@ public async Task Short_token_lifetime_should_trigger_refresh() .Respond("application/json", JsonSerializer.Serialize(refreshTokenResponse2)); // setup host - await AppHost.InitializeAsync(); + await InitializeAsync(); await AppHost.LoginAsync("alice"); // first request should trigger refresh @@ -273,6 +285,79 @@ public async Task Short_token_lifetime_should_trigger_refresh() token.Expiration.ShouldNotBe(DateTimeOffset.MaxValue); } + + [Fact] + public async Task Short_lifetime_with_auto_token_refresh() + { + // This test makes an initial token request using code flow and then + // refreshes the token a couple of times. + + // We mock the expiration of the first few token responses to be short + // enough that we will automatically refresh immediately when attempting + // to use the tokens, while the final response gets a long refresh time, + // allowing us to verify that the token is not refreshed. + + var mockHttp = new MockHttpMessageHandler(); + AppHost.IdentityServerHttpHandler = mockHttp; + + AppHost.AutoRefreshToken = true; + + // Respond to code flow with a short token lifetime so that we trigger refresh on 1st use + var initialTokenResponse = new + { + id_token = IdentityServerHost.CreateIdToken("1", "web"), + access_token = "initial_access_token", + token_type = "token_type", + expires_in = 10, + refresh_token = "initial_refresh_token", + }; + mockHttp.When("/connect/token") + .WithFormData("grant_type", "authorization_code") + .Respond("application/json", JsonSerializer.Serialize(initialTokenResponse)); + + // Respond to refresh with a short token lifetime so that we trigger another refresh on 2nd use + var refreshTokenResponse = new + { + access_token = "refreshed1_access_token", + token_type = "token_type1", + expires_in = 10, + refresh_token = "refreshed1_refresh_token", + }; + mockHttp.When("/connect/token") + .WithFormData("grant_type", "refresh_token") + .WithFormData("refresh_token", "initial_refresh_token") + .Respond("application/json", JsonSerializer.Serialize(refreshTokenResponse)); + + // Respond to second refresh with a long token lifetime so that we don't trigger another refresh on 3rd use + var refreshTokenResponse2 = new + { + access_token = "refreshed2_access_token", + token_type = "token_type2", + expires_in = 3600, + refresh_token = "refreshed2_refresh_token", + }; + mockHttp.When("/connect/token") + .WithFormData("grant_type", "refresh_token") + .WithFormData("refresh_token", "refreshed1_refresh_token") + .Respond("application/json", JsonSerializer.Serialize(refreshTokenResponse2)); + + // setup host + await InitializeAsync(); + await AppHost.LoginAsync("alice"); + + // first request should trigger refresh + var response = await AppHost.BrowserClient.GetAsync(AppHost.Url("/user_token")); + var token = await response.Content.ReadFromJsonAsync(); + + token.ShouldNotBeNull(); + token.IsError.ShouldBeFalse(); + token.AccessToken.ShouldBe("refreshed2_access_token"); + token.AccessTokenType.ShouldBe("token_type2"); + token.RefreshToken.ShouldBe("refreshed2_refresh_token"); + token.Expiration.ShouldNotBe(DateTimeOffset.MaxValue); + + } + [Fact] public async Task Resources_get_distinct_tokens() { @@ -319,7 +404,7 @@ public async Task Resources_get_distinct_tokens() .Respond("application/json", JsonSerializer.Serialize(resource2TokenResponse)); // setup host - await AppHost.InitializeAsync(); + await InitializeAsync(); await AppHost.LoginAsync("alice"); // first request - no resource @@ -389,7 +474,7 @@ public async Task Refresh_responses_without_refresh_token_use_old_refresh_token( .Respond("application/json", JsonSerializer.Serialize(refreshTokenResponse)); // setup host - await AppHost.InitializeAsync(); + await InitializeAsync(); await AppHost.LoginAsync("alice"); // first request should trigger refresh @@ -406,7 +491,7 @@ public async Task Multiple_users_have_distinct_tokens_across_refreshes() { // setup host AppHost.ClientId = "web.short"; - await AppHost.InitializeAsync(); + await InitializeAsync(); await AppHost.LoginAsync("alice"); var firstResponse = await AppHost.BrowserClient.GetAsync(AppHost.Url("/call_api")); var firstToken = await firstResponse.Content.ReadFromJsonAsync(); @@ -428,7 +513,7 @@ public async Task Multiple_users_have_distinct_tokens_across_refreshes() [Fact] public async Task Logout_should_revoke_refresh_tokens() { - await AppHost.InitializeAsync(); + await InitializeAsync(); await AppHost.LoginAsync("alice"); var response = await AppHost.BrowserClient.GetAsync(AppHost.Url("/user_token")); diff --git a/access-token-management/test/AccessTokenManagement.Tests/UserTokenManagementWithDPoPTests.cs b/access-token-management/test/AccessTokenManagement.Tests/UserTokenManagementWithDPoPTests.cs index 73f76d17d..f6a79e13a 100644 --- a/access-token-management/test/AccessTokenManagement.Tests/UserTokenManagementWithDPoPTests.cs +++ b/access-token-management/test/AccessTokenManagement.Tests/UserTokenManagementWithDPoPTests.cs @@ -11,27 +11,26 @@ namespace Duende.AccessTokenManagement.Tests; -public class UserTokenManagementWithDPoPTests : IntegrationTestBase +public class UserTokenManagementWithDPoPTests(ITestOutputHelper output) + : IntegrationTestBase(output, "dpop", opt => + { + opt.DPoPJsonWebKey = _privateJWK; + }) { // (An example jwk from RFC7517) const string _privateJWK = "{\"kty\":\"RSA\",\"n\":\"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw\",\"e\":\"AQAB\",\"d\":\"X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q\",\"p\":\"83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs\",\"q\":\"3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk\",\"dp\":\"G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0\",\"dq\":\"s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk\",\"qi\":\"GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU\",\"alg\":\"RS256\",\"kid\":\"2011-04-29\"}"; - public UserTokenManagementWithDPoPTests() : base("dpop", opt => - { - opt.DPoPJsonWebKey = _privateJWK; - }){} - [Fact] public async Task dpop_jtk_is_attached_to_authorize_requests() { - await AppHost.InitializeAsync(); + await InitializeAsync(); await AppHost.LoginAsync("alice", verifyDpopThumbprintSent: true); } [Fact] public async Task dpop_token_refresh_should_succeed() { - await AppHost.InitializeAsync(); + await InitializeAsync(); await AppHost.LoginAsync("alice"); // The DPoP proof token is valid for 1 second, and that validity is checked with the server nonce. @@ -104,7 +103,7 @@ public async Task dpop_nonce_is_respected_during_code_exchange() .Respond("application/json", JsonSerializer.Serialize(tokenResponse)); - await AppHost.InitializeAsync(); + await InitializeAsync(); await AppHost.LoginAsync("alice"); // This API call triggers a refresh diff --git a/foss.v3.ncrunchsolution b/foss.v3.ncrunchsolution index 13107d394..fe9b0700d 100644 --- a/foss.v3.ncrunchsolution +++ b/foss.v3.ncrunchsolution @@ -1,7 +1,7 @@  True - True + False True True diff --git a/identity-model-oidc-client/test/IdentityModel.OidcClient.Tests/DPoP/Framework/GenericHost.cs b/identity-model-oidc-client/test/IdentityModel.OidcClient.Tests/DPoP/Framework/GenericHost.cs index c1b882060..67ab6e55a 100644 --- a/identity-model-oidc-client/test/IdentityModel.OidcClient.Tests/DPoP/Framework/GenericHost.cs +++ b/identity-model-oidc-client/test/IdentityModel.OidcClient.Tests/DPoP/Framework/GenericHost.cs @@ -11,7 +11,7 @@ namespace Duende.IdentityModel.OidcClient.DPoP.Framework; -public class GenericHost +public class GenericHost : IAsyncDisposable { public GenericHost(string baseAddress = "https://server") { @@ -100,4 +100,21 @@ void ConfigureApp(IApplicationBuilder app) OnConfigure(app); } + + public async ValueTask DisposeAsync() + { + if (Server != null) await CastAndDispose(Server); + if (HttpClient != null) await CastAndDispose(HttpClient); + if (Logger != null) await CastAndDispose(Logger); + + return; + + static async ValueTask CastAndDispose(IDisposable resource) + { + if (resource is IAsyncDisposable resourceAsyncDisposable) + await resourceAsyncDisposable.DisposeAsync(); + else + resource?.Dispose(); + } + } } \ No newline at end of file diff --git a/identity-model-oidc-client/test/IdentityModel.OidcClient.Tests/DPoP/Framework/IntegrationTestBase.cs b/identity-model-oidc-client/test/IdentityModel.OidcClient.Tests/DPoP/Framework/IntegrationTestBase.cs index d6ccacab9..3ba8c18ab 100644 --- a/identity-model-oidc-client/test/IdentityModel.OidcClient.Tests/DPoP/Framework/IntegrationTestBase.cs +++ b/identity-model-oidc-client/test/IdentityModel.OidcClient.Tests/DPoP/Framework/IntegrationTestBase.cs @@ -3,7 +3,7 @@ namespace Duende.IdentityModel.OidcClient.DPoP.Framework; -public class IntegrationTestBase +public class IntegrationTestBase : IAsyncLifetime { protected readonly IdentityServerHost IdentityServerHost; protected ApiHost ApiHost; @@ -11,9 +11,18 @@ public class IntegrationTestBase public IntegrationTestBase() { IdentityServerHost = new IdentityServerHost(); - IdentityServerHost.InitializeAsync().Wait(); - ApiHost = new ApiHost(IdentityServerHost); - ApiHost.InitializeAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await ApiHost.DisposeAsync(); + await IdentityServerHost.DisposeAsync(); + } + + public async ValueTask InitializeAsync() + { + await ApiHost.InitializeAsync(); + await IdentityServerHost.InitializeAsync(); } } \ No newline at end of file