From f7d6f09c4e7e73e84a4a9b8f140a74c187c1fdbb Mon Sep 17 00:00:00 2001 From: Damian Hickey Date: Fri, 20 Feb 2026 22:45:10 +0100 Subject: [PATCH] Make CT required in IUserInfoResponseGenerator, ISessionCoordinationService.ValidateSessionAsync, and IReturnUrlParser, flow through implementations, callers, and tests --- .../IdentityServer/Endpoints/UserInfoEndpoint.cs | 2 +- .../Default/UserInfoResponseGenerator.cs | 12 +++++++----- .../IUserInfoResponseGenerator.cs | 3 ++- .../DefaultIdentityServerInteractionService.cs | 2 +- .../Default/DefaultSessionCoordinationService.cs | 8 +++++--- .../Services/Default/OidcReturnUrlParser.cs | 6 +++--- .../Services/Default/ReturnUrlParser.cs | 5 +++-- .../ServerSideSessionRefreshTokenService.cs | 2 +- .../IdentityServer/Services/IReturnUrlParser.cs | 3 ++- .../Services/ISessionCoordinationService.cs | 4 +++- .../Validation/Default/TokenValidator.cs | 2 +- .../Common/MockReturnUrlParser.cs | 2 +- .../Common/StubSessionCoordinationService.cs | 2 +- .../UserInfoResponseGeneratorTests.cs | 16 +++++++++------- .../Validation/IsLocalUrlTests.cs | 6 ++++-- 15 files changed, 44 insertions(+), 31 deletions(-) diff --git a/identity-server/src/IdentityServer/Endpoints/UserInfoEndpoint.cs b/identity-server/src/IdentityServer/Endpoints/UserInfoEndpoint.cs index bf916559f..ec888f615 100644 --- a/identity-server/src/IdentityServer/Endpoints/UserInfoEndpoint.cs +++ b/identity-server/src/IdentityServer/Endpoints/UserInfoEndpoint.cs @@ -89,7 +89,7 @@ internal class UserInfoEndpoint : IEndpointHandler // generate response _logger.LogTrace("Calling into userinfo response generator: {type}", _responseGenerator.GetType().FullName); - var response = await _responseGenerator.ProcessAsync(validationResult); + var response = await _responseGenerator.ProcessAsync(validationResult, context.RequestAborted); _logger.LogDebug("End userinfo request"); return new UserInfoResult(response); diff --git a/identity-server/src/IdentityServer/ResponseHandling/Default/UserInfoResponseGenerator.cs b/identity-server/src/IdentityServer/ResponseHandling/Default/UserInfoResponseGenerator.cs index 4924f740b..7a34830e3 100644 --- a/identity-server/src/IdentityServer/ResponseHandling/Default/UserInfoResponseGenerator.cs +++ b/identity-server/src/IdentityServer/ResponseHandling/Default/UserInfoResponseGenerator.cs @@ -51,9 +51,10 @@ public class UserInfoResponseGenerator : IUserInfoResponseGenerator /// Creates the response. /// /// The userinfo request validation result. + /// The cancellation token. /// /// Profile service returned incorrect subject value - public virtual async Task> ProcessAsync(UserInfoRequestValidationResult validationResult) + public virtual async Task> ProcessAsync(UserInfoRequestValidationResult validationResult, CT ct) { using var activity = Tracing.BasicActivitySource.StartActivity("UserInfoResponseGenerator.Process"); @@ -62,7 +63,7 @@ public class UserInfoResponseGenerator : IUserInfoResponseGenerator // extract scopes and turn into requested claim types var scopes = validationResult.TokenValidationResult.Claims.Where(c => c.Type == JwtClaimTypes.Scope).Select(c => c.Value); - var validatedResources = await GetRequestedResourcesAsync(scopes); + var validatedResources = await GetRequestedResourcesAsync(scopes, ct); var requestedClaimTypes = await GetRequestedClaimTypesAsync(validatedResources); Logger.LogDebug("Requested claim types: {claimTypes}", requestedClaimTypes.ToSpaceSeparatedString()); @@ -75,7 +76,7 @@ public class UserInfoResponseGenerator : IUserInfoResponseGenerator requestedClaimTypes); context.RequestedResources = validatedResources; - await Profile.GetProfileDataAsync(context, default); + await Profile.GetProfileDataAsync(context, ct); var profileClaims = context.IssuedClaims; // construct outgoing claims @@ -109,8 +110,9 @@ public class UserInfoResponseGenerator : IUserInfoResponseGenerator /// Gets the identity resources from the scopes. /// /// + /// The cancellation token. /// - protected internal virtual async Task GetRequestedResourcesAsync(IEnumerable scopes) + protected internal virtual async Task GetRequestedResourcesAsync(IEnumerable scopes, CT ct) { if (scopes == null || !scopes.Any()) { @@ -121,7 +123,7 @@ public class UserInfoResponseGenerator : IUserInfoResponseGenerator Logger.LogDebug("Scopes in access token: {scopes}", scopeString); // if we ever parameterized identity scopes, then we would need to invoke the resource validator's parse API here - var identityResources = await Resources.FindEnabledIdentityResourcesByScopeAsync(scopes, default); + var identityResources = await Resources.FindEnabledIdentityResourcesByScopeAsync(scopes, ct); var resources = new Resources(identityResources, Enumerable.Empty(), Enumerable.Empty()); var result = new ResourceValidationResult(resources); diff --git a/identity-server/src/IdentityServer/ResponseHandling/IUserInfoResponseGenerator.cs b/identity-server/src/IdentityServer/ResponseHandling/IUserInfoResponseGenerator.cs index 0b0241076..a3f46b92c 100644 --- a/identity-server/src/IdentityServer/ResponseHandling/IUserInfoResponseGenerator.cs +++ b/identity-server/src/IdentityServer/ResponseHandling/IUserInfoResponseGenerator.cs @@ -15,6 +15,7 @@ public interface IUserInfoResponseGenerator /// Creates the response. /// /// The userinfo request validation result. + /// The cancellation token. /// - Task> ProcessAsync(UserInfoRequestValidationResult validationResult); + Task> ProcessAsync(UserInfoRequestValidationResult validationResult, CT ct); } diff --git a/identity-server/src/IdentityServer/Services/Default/DefaultIdentityServerInteractionService.cs b/identity-server/src/IdentityServer/Services/Default/DefaultIdentityServerInteractionService.cs index f2dde4c64..aeb454f17 100644 --- a/identity-server/src/IdentityServer/Services/Default/DefaultIdentityServerInteractionService.cs +++ b/identity-server/src/IdentityServer/Services/Default/DefaultIdentityServerInteractionService.cs @@ -48,7 +48,7 @@ internal class DefaultIdentityServerInteractionService : IIdentityServerInteract { using var activity = Tracing.ServiceActivitySource.StartActivity("DefaultIdentityServerInteractionService.GetAuthorizationContext"); - var result = await _returnUrlParser.ParseAsync(returnUrl); + var result = await _returnUrlParser.ParseAsync(returnUrl, default); if (result != null) { diff --git a/identity-server/src/IdentityServer/Services/Default/DefaultSessionCoordinationService.cs b/identity-server/src/IdentityServer/Services/Default/DefaultSessionCoordinationService.cs index fae2e0c93..e8fbb80cf 100644 --- a/identity-server/src/IdentityServer/Services/Default/DefaultSessionCoordinationService.cs +++ b/identity-server/src/IdentityServer/Services/Default/DefaultSessionCoordinationService.cs @@ -194,7 +194,7 @@ public class DefaultSessionCoordinationService : ISessionCoordinationService /// - public virtual async Task ValidateSessionAsync(SessionValidationRequest request) + public virtual async Task ValidateSessionAsync(SessionValidationRequest request, CT ct) { if (ServerSideSessionStore != null) { @@ -208,7 +208,7 @@ public class DefaultSessionCoordinationService : ISessionCoordinationService { SubjectId = request.SubjectId, SessionId = request.SessionId - }, default); + }, ct); var valid = sessions.Count > 0 && sessions.Any(x => x.Expires == null || DateTime.UtcNow < x.Expires.Value); @@ -238,6 +238,7 @@ public class DefaultSessionCoordinationService : ISessionCoordinationService //result in the cookie never being renewed and expiring in a surprising way. Renewing //the ticket also updates the session, so we don't need to do both. if (Options.Authentication.CookieSlidingExpiration && +#pragma warning disable CA2016 // ITicketStore interface has no CT parameter await ServerSideTicketStore.RetrieveAsync(session.Key) is { Properties: { IsPersistent: true, AllowRefresh: null or true } } ticket) { @@ -245,10 +246,11 @@ public class DefaultSessionCoordinationService : ISessionCoordinationService ticket.Properties.IssuedUtc = session.Renewed; ticket.Properties.ExpiresUtc = session.Expires; await ServerSideTicketStore.RenewAsync(session.Key, ticket); +#pragma warning restore CA2016 } else { - await ServerSideSessionStore.UpdateSessionAsync(session, default); + await ServerSideSessionStore.UpdateSessionAsync(session, ct); } } } diff --git a/identity-server/src/IdentityServer/Services/Default/OidcReturnUrlParser.cs b/identity-server/src/IdentityServer/Services/Default/OidcReturnUrlParser.cs index 0d7e74b3c..aa49df23a 100644 --- a/identity-server/src/IdentityServer/Services/Default/OidcReturnUrlParser.cs +++ b/identity-server/src/IdentityServer/Services/Default/OidcReturnUrlParser.cs @@ -38,7 +38,7 @@ internal class OidcReturnUrlParser : IReturnUrlParser _authorizationParametersMessageStore = authorizationParametersMessageStore; } - public async Task ParseAsync(string returnUrl) + public async Task ParseAsync(string returnUrl, CT ct) { using var activity = Tracing.ValidationActivitySource.StartActivity("OidcReturnUrlParser.Parse"); @@ -48,11 +48,11 @@ internal class OidcReturnUrlParser : IReturnUrlParser if (_authorizationParametersMessageStore != null) { var messageStoreId = parameters[Constants.AuthorizationParamsStore.MessageStoreIdParameterName]; - var entry = await _authorizationParametersMessageStore.ReadAsync(messageStoreId, default); + var entry = await _authorizationParametersMessageStore.ReadAsync(messageStoreId, ct); parameters = entry?.Data.FromFullDictionary() ?? new NameValueCollection(); } - var user = await _userSession.GetUserAsync(default); + var user = await _userSession.GetUserAsync(ct); var result = await _validator.ValidateAsync(parameters, user); if (!result.IsError) { diff --git a/identity-server/src/IdentityServer/Services/Default/ReturnUrlParser.cs b/identity-server/src/IdentityServer/Services/Default/ReturnUrlParser.cs index 6ffafecdd..b4f9c65ae 100644 --- a/identity-server/src/IdentityServer/Services/Default/ReturnUrlParser.cs +++ b/identity-server/src/IdentityServer/Services/Default/ReturnUrlParser.cs @@ -23,14 +23,15 @@ public class ReturnUrlParser /// Parses the return URL. /// /// The return URL. + /// The cancellation token. /// - public virtual async Task ParseAsync(string returnUrl) + public virtual async Task ParseAsync(string returnUrl, CT ct) { using var activity = Tracing.ValidationActivitySource.StartActivity("ReturnUrlParser.Parse"); foreach (var parser in _parsers) { - var result = await parser.ParseAsync(returnUrl); + var result = await parser.ParseAsync(returnUrl, ct); if (result != null) { return result; diff --git a/identity-server/src/IdentityServer/Services/Default/ServerSideSessionRefreshTokenService.cs b/identity-server/src/IdentityServer/Services/Default/ServerSideSessionRefreshTokenService.cs index 2b20ea0a1..0a09a54c0 100644 --- a/identity-server/src/IdentityServer/Services/Default/ServerSideSessionRefreshTokenService.cs +++ b/identity-server/src/IdentityServer/Services/Default/ServerSideSessionRefreshTokenService.cs @@ -57,7 +57,7 @@ internal class ServerSideSessionRefreshTokenService : IRefreshTokenService SessionId = result.RefreshToken.SessionId, Client = result.Client, Type = SessionValidationType.RefreshToken - }); + }, ct); if (!valid) { diff --git a/identity-server/src/IdentityServer/Services/IReturnUrlParser.cs b/identity-server/src/IdentityServer/Services/IReturnUrlParser.cs index 23a0b73b1..3b6220b06 100644 --- a/identity-server/src/IdentityServer/Services/IReturnUrlParser.cs +++ b/identity-server/src/IdentityServer/Services/IReturnUrlParser.cs @@ -17,8 +17,9 @@ public interface IReturnUrlParser /// Parses a return URL. /// /// The return URL. + /// The cancellation token. /// - Task ParseAsync(string returnUrl); + Task ParseAsync(string returnUrl, CT ct); /// /// Determines whether the return URL is valid. diff --git a/identity-server/src/IdentityServer/Services/ISessionCoordinationService.cs b/identity-server/src/IdentityServer/Services/ISessionCoordinationService.cs index b84d38018..f6e64d391 100644 --- a/identity-server/src/IdentityServer/Services/ISessionCoordinationService.cs +++ b/identity-server/src/IdentityServer/Services/ISessionCoordinationService.cs @@ -27,7 +27,9 @@ public interface ISessionCoordinationService /// Validates client request, and if valid extends server-side session. /// Returns false if the session is invalid, true otherwise. /// - Task ValidateSessionAsync(SessionValidationRequest request); + /// The session validation request. + /// The cancellation token. + Task ValidateSessionAsync(SessionValidationRequest request, CT ct); } /// diff --git a/identity-server/src/IdentityServer/Validation/Default/TokenValidator.cs b/identity-server/src/IdentityServer/Validation/Default/TokenValidator.cs index b507b70cc..35dd13b78 100644 --- a/identity-server/src/IdentityServer/Validation/Default/TokenValidator.cs +++ b/identity-server/src/IdentityServer/Validation/Default/TokenValidator.cs @@ -230,7 +230,7 @@ internal class TokenValidator : ITokenValidator SessionId = sid, Client = result.Client, Type = SessionValidationType.AccessToken - }); + }, ct); if (!sessionResult) { diff --git a/identity-server/test/IdentityServer.UnitTests/Common/MockReturnUrlParser.cs b/identity-server/test/IdentityServer.UnitTests/Common/MockReturnUrlParser.cs index 37a42166d..994214573 100644 --- a/identity-server/test/IdentityServer.UnitTests/Common/MockReturnUrlParser.cs +++ b/identity-server/test/IdentityServer.UnitTests/Common/MockReturnUrlParser.cs @@ -16,7 +16,7 @@ public class MockReturnUrlParser : ReturnUrlParser { } - public override Task ParseAsync(string returnUrl) => Task.FromResult(AuthorizationRequestResult); + public override Task ParseAsync(string returnUrl, CT ct) => Task.FromResult(AuthorizationRequestResult); public override bool IsValidReturnUrl(string returnUrl) => IsValidReturnUrlResult; } diff --git a/identity-server/test/IdentityServer.UnitTests/Common/StubSessionCoordinationService.cs b/identity-server/test/IdentityServer.UnitTests/Common/StubSessionCoordinationService.cs index 822d1fdc5..99a74ae2e 100644 --- a/identity-server/test/IdentityServer.UnitTests/Common/StubSessionCoordinationService.cs +++ b/identity-server/test/IdentityServer.UnitTests/Common/StubSessionCoordinationService.cs @@ -13,5 +13,5 @@ internal class StubSessionCoordinationService : ISessionCoordinationService public Task ProcessLogoutAsync(UserSession session, CT _) => Task.CompletedTask; - public Task ValidateSessionAsync(SessionValidationRequest request) => Task.FromResult(true); + public Task ValidateSessionAsync(SessionValidationRequest request, CT _) => Task.FromResult(true); } diff --git a/identity-server/test/IdentityServer.UnitTests/ResponseHandling/UserInfoResponseGeneratorTests.cs b/identity-server/test/IdentityServer.UnitTests/ResponseHandling/UserInfoResponseGeneratorTests.cs index adf64e0f9..d5d462047 100644 --- a/identity-server/test/IdentityServer.UnitTests/ResponseHandling/UserInfoResponseGeneratorTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/ResponseHandling/UserInfoResponseGeneratorTests.cs @@ -15,6 +15,8 @@ namespace UnitTests.ResponseHandling; public class UserInfoResponseGeneratorTests { + private readonly CT _ct = TestContext.Current.CancellationToken; + private UserInfoResponseGenerator _subject; private MockProfileService _mockProfileService = new MockProfileService(); private ClaimsPrincipal _user; @@ -50,7 +52,7 @@ public class UserInfoResponseGeneratorTests [Fact] public async Task GetRequestedClaimTypesAsync_when_no_scopes_requested_should_return_empty_claim_types() { - var resources = await _subject.GetRequestedResourcesAsync(null); + var resources = await _subject.GetRequestedResourcesAsync(null, _ct); var claims = await _subject.GetRequestedClaimTypesAsync(resources); claims.ShouldBe(new string[] { }); } @@ -61,7 +63,7 @@ public class UserInfoResponseGeneratorTests _identityResources.Add(new IdentityResource("id1", new[] { "c1", "c2" })); _identityResources.Add(new IdentityResource("id2", new[] { "c2", "c3" })); - var resources = await _subject.GetRequestedResourcesAsync(new[] { "id1", "id2", "id3" }); + var resources = await _subject.GetRequestedResourcesAsync(new[] { "id1", "id2", "id3" }, _ct); var claims = await _subject.GetRequestedClaimTypesAsync(resources); claims.ShouldBe(["c1", "c2", "c3"]); } @@ -72,7 +74,7 @@ public class UserInfoResponseGeneratorTests _identityResources.Add(new IdentityResource("id1", new[] { "c1", "c2" }) { Enabled = false }); _identityResources.Add(new IdentityResource("id2", new[] { "c2", "c3" })); - var resources = await _subject.GetRequestedResourcesAsync(new[] { "id1", "id2", "id3" }); + var resources = await _subject.GetRequestedResourcesAsync(new[] { "id1", "id2", "id3" }, _ct); var claims = await _subject.GetRequestedClaimTypesAsync(resources); claims.ShouldBe(["c2", "c3"]); } @@ -98,7 +100,7 @@ public class UserInfoResponseGeneratorTests } }; - var claims = await _subject.ProcessAsync(result); + var claims = await _subject.ProcessAsync(result, _ct); _mockProfileService.GetProfileWasCalled.ShouldBeTrue(); _mockProfileService.ProfileContext.RequestedClaimTypes.ShouldBe(["foo", "bar"]); @@ -141,7 +143,7 @@ public class UserInfoResponseGeneratorTests } }; - var claims = await _subject.ProcessAsync(result); + var claims = await _subject.ProcessAsync(result, _ct); claims.ShouldContainKey("email"); claims["email"].ShouldBe("fred@gmail.com"); @@ -178,7 +180,7 @@ public class UserInfoResponseGeneratorTests } }; - var claims = await _subject.ProcessAsync(result); + var claims = await _subject.ProcessAsync(result, _ct); claims.ShouldContainKey("sub"); claims["sub"].ShouldBe("bob"); @@ -209,7 +211,7 @@ public class UserInfoResponseGeneratorTests } }; - Func act = () => _subject.ProcessAsync(result); + Func act = () => _subject.ProcessAsync(result, _ct); var exception = await act.ShouldThrowAsync(); exception.Message.ShouldMatch(".*subject.*"); diff --git a/identity-server/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs b/identity-server/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs index ebf2f6829..1ac1540cb 100644 --- a/identity-server/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Validation/IsLocalUrlTests.cs @@ -13,6 +13,8 @@ namespace UnitTests.Validation; public class IsLocalUrlTests { + private readonly CT _ct = TestContext.Current.CancellationToken; + private const string queryParameters = "?client_id=mvc.code" + "&redirect_uri=https%3A%2F%2Flocalhost%3A44302%2Fsignin-oidc" + "&response_type=code" + @@ -105,7 +107,7 @@ public class IsLocalUrlTests public async Task OidcReturnUrlParser_ParseAsync(string returnUrl, bool expected) { var oidcParser = GetOidcReturnUrlParser(); - var actual = await oidcParser.ParseAsync(returnUrl); + var actual = await oidcParser.ParseAsync(returnUrl, _ct); if (expected) { actual.ShouldNotBeNull(); @@ -138,7 +140,7 @@ public class IsLocalUrlTests public async Task ReturnUrlParser_ParseAsync(string returnUrl, bool expected) { var parser = GetReturnUrlParser(); - var actual = await parser.ParseAsync(returnUrl); + var actual = await parser.ParseAsync(returnUrl, _ct); if (expected) { actual.ShouldNotBeNull();