diff --git a/identity-server/src/IdentityServer/Endpoints/TokenEndpoint.cs b/identity-server/src/IdentityServer/Endpoints/TokenEndpoint.cs index de5d44f27..95bb060ad 100644 --- a/identity-server/src/IdentityServer/Endpoints/TokenEndpoint.cs +++ b/identity-server/src/IdentityServer/Endpoints/TokenEndpoint.cs @@ -120,7 +120,17 @@ private async Task ProcessTokenRequestAsync(HttpContext context var requestResult = await _requestValidator.ValidateRequestAsync(requestContext); if (requestResult.IsError) { - await _events.RaiseAsync(new TokenIssuedFailureEvent(requestResult)); + //INFO: this is expected case in the normal DPoP flow and is not a real failure event. + //Keeping a debug log to help with troubleshooting in the case of a buggy client. + if (requestResult.Error == OidcConstants.TokenErrors.UseDPoPNonce) + { + _logger.LogDebug("Token request validation failed with: {tokenFailureReason}", OidcConstants.TokenErrors.UseDPoPNonce); + } + else + { + await _events.RaiseAsync(new TokenIssuedFailureEvent(requestResult)); + } + Telemetry.Metrics.TokenIssuedFailure( clientResult.Client.ClientId, requestResult.ValidatedRequest?.GrantType, null, requestResult.Error); var err = Error(requestResult.Error, requestResult.ErrorDescription, requestResult.CustomResponse); diff --git a/identity-server/test/IdentityServer.UnitTests/Common/TestEventService.cs b/identity-server/test/IdentityServer.UnitTests/Common/TestEventService.cs index 0e6eb0ee8..e57f70246 100644 --- a/identity-server/test/IdentityServer.UnitTests/Common/TestEventService.cs +++ b/identity-server/test/IdentityServer.UnitTests/Common/TestEventService.cs @@ -29,6 +29,12 @@ public T AssertEventWasRaised() return (T)_events.Where(x => x.Key == typeof(T)).Select(x=>x.Value).First(); } + public void AssertEventWasNotRaised() + where T : class + { + _events.ContainsKey(typeof(T)).Should().BeFalse(); + } + public bool CanRaiseEventType(EventTypes evtType) { return true; diff --git a/identity-server/test/IdentityServer.UnitTests/Endpoints/Token/StubClientSecretValidator.cs b/identity-server/test/IdentityServer.UnitTests/Endpoints/Token/StubClientSecretValidator.cs new file mode 100644 index 000000000..6b7191900 --- /dev/null +++ b/identity-server/test/IdentityServer.UnitTests/Endpoints/Token/StubClientSecretValidator.cs @@ -0,0 +1,19 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using System.Threading.Tasks; +using Duende.IdentityServer.Validation; +using Microsoft.AspNetCore.Http; + +namespace IdentityServer.Endpoints.Token; + +internal class StubClientSecretValidator : IClientSecretValidator +{ + public ClientSecretValidationResult Result { get; set; } + + public Task ValidateAsync(HttpContext context) + { + return Task.FromResult(Result); + } +} \ No newline at end of file diff --git a/identity-server/test/IdentityServer.UnitTests/Endpoints/Token/StubTokenRequestValidator.cs b/identity-server/test/IdentityServer.UnitTests/Endpoints/Token/StubTokenRequestValidator.cs new file mode 100644 index 000000000..bcc2a53bc --- /dev/null +++ b/identity-server/test/IdentityServer.UnitTests/Endpoints/Token/StubTokenRequestValidator.cs @@ -0,0 +1,18 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using System.Threading.Tasks; +using Duende.IdentityServer.Validation; + +namespace IdentityServer.Endpoints.Token; + +internal class StubTokenRequestValidator : ITokenRequestValidator +{ + public TokenRequestValidationResult Result { get; set; } + + public Task ValidateRequestAsync(TokenRequestValidationContext context) + { + return Task.FromResult(Result); + } +} \ No newline at end of file diff --git a/identity-server/test/IdentityServer.UnitTests/Endpoints/Token/StubTokenResponseGenerator.cs b/identity-server/test/IdentityServer.UnitTests/Endpoints/Token/StubTokenResponseGenerator.cs new file mode 100644 index 000000000..e7430ca85 --- /dev/null +++ b/identity-server/test/IdentityServer.UnitTests/Endpoints/Token/StubTokenResponseGenerator.cs @@ -0,0 +1,19 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using System.Threading.Tasks; +using Duende.IdentityServer.ResponseHandling; +using Duende.IdentityServer.Validation; + +namespace IdentityServer.Endpoints.Token; + +internal class StubTokenResponseGenerator : ITokenResponseGenerator +{ + public TokenResponse Response { get; set; } = new TokenResponse(); + + public Task ProcessAsync(TokenRequestValidationResult validationResult) + { + return Task.FromResult(Response); + } +} \ No newline at end of file diff --git a/identity-server/test/IdentityServer.UnitTests/Endpoints/Token/TokenEndpointTests.cs b/identity-server/test/IdentityServer.UnitTests/Endpoints/Token/TokenEndpointTests.cs new file mode 100644 index 000000000..68e979d75 --- /dev/null +++ b/identity-server/test/IdentityServer.UnitTests/Endpoints/Token/TokenEndpointTests.cs @@ -0,0 +1,85 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + + +using System.Threading.Tasks; +using Duende.IdentityModel; +using Duende.IdentityServer.Configuration; +using Duende.IdentityServer.Endpoints; +using Duende.IdentityServer.Events; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Validation; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using UnitTests.Common; +using Xunit; + +namespace IdentityServer.Endpoints.Token; + +public class TokenEndpointTests +{ + private HttpContext _context; + + private readonly IdentityServerOptions _identityServerOptions = new IdentityServerOptions(); + + private readonly StubClientSecretValidator _stubClientSecretValidator = new StubClientSecretValidator(); + + private readonly StubTokenRequestValidator _stubTokenRequestValidator = new StubTokenRequestValidator(); + + private readonly StubTokenResponseGenerator _stubTokenResponseGenerator = new StubTokenResponseGenerator(); + + private readonly TestEventService _fakeEventService = new TestEventService(); + + private readonly ILogger _fakeLogger = TestLogger.Create(); + + private TokenEndpoint _subject; + + public TokenEndpointTests() + { + Init(); + } + + [Fact] + public async Task ProcessAsync_should_not_raise_event_on_use_dpop_nonce_token_validation_failure() + { + _context.Request.Method = "POST"; + _context.Request.Headers.ContentType = "application/x-www-form-urlencoded"; + + var client = new Client + { + ClientId = "client", + ClientName = "Test Client", + }; + + _stubClientSecretValidator.Result = new ClientSecretValidationResult + { + IsError = false, + Client = client + }; + + var validatedTokenRequest = new ValidatedTokenRequest + { + Client = client, + GrantType = OidcConstants.GrantTypes.AuthorizationCode + }; + _stubTokenRequestValidator.Result = new TokenRequestValidationResult(validatedTokenRequest, OidcConstants.TokenErrors.UseDPoPNonce); + + await _subject.ProcessAsync(_context); + + _fakeEventService.AssertEventWasNotRaised(); + } + + private void Init() + { + _context = new MockHttpContextAccessor().HttpContext; + + _subject = new TokenEndpoint( + _identityServerOptions, + _stubClientSecretValidator, + _stubTokenRequestValidator, + _stubTokenResponseGenerator, + _fakeEventService, + _fakeLogger); + } +} \ No newline at end of file