From 84548d349e3ffde0614305bd5f18e869b0c32968 Mon Sep 17 00:00:00 2001 From: Manuel Guilbault Date: Mon, 10 Dec 2018 11:11:28 +0100 Subject: [PATCH] Refactoring + unit tests for token events. --- .../CachingTokenHandler.cs | 12 +++-- .../ClientCredentialsTokenHandler.cs | 8 ++- .../ClientCredentialsTokenHandlerOptions.cs | 2 +- .../DelegationTokenHandler.cs | 12 ++--- .../DelegationTokenHandlerOptions.cs | 2 +- .../HttpClientBuilderExtensions.cs | 8 +-- .../Infrastructure/AsyncMutex.cs | 7 +-- .../Infrastructure/CachingExtensions.cs | 6 ++- .../PasswordTokenHandler.cs | 12 ++--- .../PasswordTokenHandlerOptions.cs | 2 +- ...ureClientCredentialsTokenHandlerOptions.cs | 15 ------ ...tConfigureDelegationTokenHandlerOptions.cs | 15 ------ ...ostConfigurePasswordTokenHandlerOptions.cs | 15 ------ ...PostConfigureRefreshTokenHandlerOptions.cs | 15 ------ .../RefreshTokenHandler.cs | 12 ++--- .../RefreshTokenHandlerOptions.cs | 2 +- .../TokenHandlerOptionsExtensions.cs | 4 +- .../ClientCredentials.cs | 49 +++++++++++++++-- .../Password.cs | 54 ++++++++++++++++--- .../RefreshToken.cs | 49 +++++++++++++++-- .../TokenDelegation.cs | 53 ++++++++++++++++-- .../Util/TokenEndpointHandler.cs | 2 +- .../Util/TokenEventsMock.cs | 29 ++++++++++ 23 files changed, 253 insertions(+), 132 deletions(-) delete mode 100644 src/AspNetCore.NonInteractiveOidcHandlers/PostConfigureClientCredentialsTokenHandlerOptions.cs delete mode 100644 src/AspNetCore.NonInteractiveOidcHandlers/PostConfigureDelegationTokenHandlerOptions.cs delete mode 100644 src/AspNetCore.NonInteractiveOidcHandlers/PostConfigurePasswordTokenHandlerOptions.cs delete mode 100644 src/AspNetCore.NonInteractiveOidcHandlers/PostConfigureRefreshTokenHandlerOptions.cs create mode 100644 test/AspNetCore.NonInteractiveOidcHandlers.Tests/Util/TokenEventsMock.cs diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/CachingTokenHandler.cs b/src/AspNetCore.NonInteractiveOidcHandlers/CachingTokenHandler.cs index 5459837..c648248 100644 --- a/src/AspNetCore.NonInteractiveOidcHandlers/CachingTokenHandler.cs +++ b/src/AspNetCore.NonInteractiveOidcHandlers/CachingTokenHandler.cs @@ -44,9 +44,13 @@ protected async Task GetTokenAsync(string cacheKey, Func SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var token = await GetTokenAsync(cancellationToken); - if (token != null && token.AccessToken.IsPresent()) + if (token != null && !token.IsError && token.AccessToken.IsPresent()) { request.SetBearerToken(token.AccessToken); } diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/ClientCredentialsTokenHandler.cs b/src/AspNetCore.NonInteractiveOidcHandlers/ClientCredentialsTokenHandler.cs index bc55ad2..893cdd5 100644 --- a/src/AspNetCore.NonInteractiveOidcHandlers/ClientCredentialsTokenHandler.cs +++ b/src/AspNetCore.NonInteractiveOidcHandlers/ClientCredentialsTokenHandler.cs @@ -34,7 +34,7 @@ public override async Task GetTokenAsync(CancellationToken cancel private async Task AcquireTokenAsync(CancellationToken cancellationToken) { - var tokenResponseTask = _options.TokenMutex.AcquireAsync(GetToken); + var tokenResponseTask = _options.TokenMutex.AcquireAsync(RequestTokenAsync); try { var tokenResponse = await tokenResponseTask.ConfigureAwait(false); @@ -42,9 +42,7 @@ private async Task AcquireTokenAsync(CancellationToken cancellati { _logger.LogError($"Error returned from token endpoint: {tokenResponse.Error}"); await _options.Events.OnTokenRequestFailed.Invoke(tokenResponse).ConfigureAwait(false); - throw new InvalidOperationException( - $"Token retrieval failed: {tokenResponse.Error} {tokenResponse.ErrorDescription}", - tokenResponse.Exception); + return tokenResponse; } await _options.Events.OnTokenAcquired(tokenResponse).ConfigureAwait(false); @@ -59,7 +57,7 @@ private async Task AcquireTokenAsync(CancellationToken cancellati } } - private async Task GetToken() + private async Task RequestTokenAsync() { var httpClient = _httpClientFactory.CreateClient(_options.AuthorityHttpClientName); var tokenEndpoint = await _options.GetTokenEndpointAsync(httpClient).ConfigureAwait(false); diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/ClientCredentialsTokenHandlerOptions.cs b/src/AspNetCore.NonInteractiveOidcHandlers/ClientCredentialsTokenHandlerOptions.cs index e825330..38aa324 100644 --- a/src/AspNetCore.NonInteractiveOidcHandlers/ClientCredentialsTokenHandlerOptions.cs +++ b/src/AspNetCore.NonInteractiveOidcHandlers/ClientCredentialsTokenHandlerOptions.cs @@ -22,7 +22,7 @@ public class ClientCredentialsTokenHandlerOptions: TokenHandlerOptions /// public IDictionary ExtraTokenParameters { get; set; } - internal AsyncMutex TokenMutex { get; set; } + internal AsyncMutex TokenMutex { get; } = new AsyncMutex(); public override IEnumerable GetValidationErrors() { diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/DelegationTokenHandler.cs b/src/AspNetCore.NonInteractiveOidcHandlers/DelegationTokenHandler.cs index 41bda8e..409c242 100644 --- a/src/AspNetCore.NonInteractiveOidcHandlers/DelegationTokenHandler.cs +++ b/src/AspNetCore.NonInteractiveOidcHandlers/DelegationTokenHandler.cs @@ -49,11 +49,11 @@ public override async Task GetTokenAsync(CancellationToken cancel return null; } - return await GetTokenAsync($"delegation:{inboundToken}", ct => AcquireToken(inboundToken, ct), cancellationToken) + return await GetTokenAsync($"delegation:{inboundToken}", _ => AcquireTokenAsync(inboundToken), cancellationToken) .ConfigureAwait(false); } - private async Task AcquireToken(string inboundToken, CancellationToken cancellationToken) + private async Task AcquireTokenAsync(string inboundToken) { var lazyToken = _options.LazyTokens.GetOrAdd(inboundToken, CreateLazyDelegatedToken); @@ -64,9 +64,7 @@ private async Task AcquireToken(string inboundToken, Cancellation { _logger.LogError($"Error returned from token endpoint: {tokenResponse.Error}"); await _options.Events.OnTokenRequestFailed.Invoke(tokenResponse).ConfigureAwait(false); - throw new InvalidOperationException( - $"Token retrieval failed: {tokenResponse.Error} {tokenResponse.ErrorDescription}", - tokenResponse.Exception); + return tokenResponse; } await _options.Events.OnTokenAcquired(tokenResponse).ConfigureAwait(false); @@ -82,9 +80,9 @@ private async Task AcquireToken(string inboundToken, Cancellation } private AsyncLazy CreateLazyDelegatedToken(string inboundToken) - => new AsyncLazy(() => RequestToken(inboundToken)); + => new AsyncLazy(() => RequestTokenAsync(inboundToken)); - private async Task RequestToken(string inboundToken) + private async Task RequestTokenAsync(string inboundToken) { var httpClient = _httpClientFactory.CreateClient(_options.AuthorityHttpClientName); var tokenEndpoint = await _options.GetTokenEndpointAsync(httpClient).ConfigureAwait(false); diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/DelegationTokenHandlerOptions.cs b/src/AspNetCore.NonInteractiveOidcHandlers/DelegationTokenHandlerOptions.cs index 489b2bd..6dd6c93 100644 --- a/src/AspNetCore.NonInteractiveOidcHandlers/DelegationTokenHandlerOptions.cs +++ b/src/AspNetCore.NonInteractiveOidcHandlers/DelegationTokenHandlerOptions.cs @@ -30,7 +30,7 @@ public class DelegationTokenHandlerOptions: TokenHandlerOptions /// public Func> TokenRetriever { get; set; } = TokenRetrieval.FromAuthenticationService(); - internal ConcurrentDictionary> LazyTokens { get; set; } + internal ConcurrentDictionary> LazyTokens { get; } = new ConcurrentDictionary>(); public override IEnumerable GetValidationErrors() { diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/HttpClientBuilderExtensions.cs b/src/AspNetCore.NonInteractiveOidcHandlers/HttpClientBuilderExtensions.cs index e2ab19c..0819b2a 100644 --- a/src/AspNetCore.NonInteractiveOidcHandlers/HttpClientBuilderExtensions.cs +++ b/src/AspNetCore.NonInteractiveOidcHandlers/HttpClientBuilderExtensions.cs @@ -47,7 +47,7 @@ public static IHttpClientBuilder AddOidcTokenDelegation(this IHttpClientBuilder .AddHttpContextAccessor() .Configure(builder.Name, configureOptions) .AddPostConfigure>() - .AddPostConfigure(); + ; var instanceName = builder.Name; return builder.AddHttpMessageHandler(sp => @@ -71,7 +71,7 @@ public static IHttpClientBuilder AddOidcClientCredentials(this IHttpClientBuilde builder.Services .Configure(builder.Name, configureOptions) .AddPostConfigure>() - .AddPostConfigure(); + ; var instanceName = builder.Name; return builder.AddHttpMessageHandler(sp => @@ -94,7 +94,7 @@ public static IHttpClientBuilder AddOidcPassword(this IHttpClientBuilder builder builder.Services .Configure(builder.Name, configureOptions) .AddPostConfigure>() - .AddPostConfigure(); + ; var instanceName = builder.Name; return builder.AddHttpMessageHandler(sp => @@ -118,7 +118,7 @@ public static IHttpClientBuilder AddOidcRefreshToken(this IHttpClientBuilder bui builder.Services .Configure(builder.Name, configureOptions) .AddPostConfigure>() - .AddPostConfigure(); + ; var instanceName = builder.Name; return builder.AddHttpMessageHandler(sp => diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/Infrastructure/AsyncMutex.cs b/src/AspNetCore.NonInteractiveOidcHandlers/Infrastructure/AsyncMutex.cs index 0d4c026..6aef77c 100644 --- a/src/AspNetCore.NonInteractiveOidcHandlers/Infrastructure/AsyncMutex.cs +++ b/src/AspNetCore.NonInteractiveOidcHandlers/Infrastructure/AsyncMutex.cs @@ -12,12 +12,7 @@ public Task AcquireAsync(Func> factory) { lock (_taskGuard) { - if (_task != null) - { - return _task; - } - - return _task = factory(); + return _task ?? (_task = factory()); } } diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/Infrastructure/CachingExtensions.cs b/src/AspNetCore.NonInteractiveOidcHandlers/Infrastructure/CachingExtensions.cs index 13752bc..a182b5e 100644 --- a/src/AspNetCore.NonInteractiveOidcHandlers/Infrastructure/CachingExtensions.cs +++ b/src/AspNetCore.NonInteractiveOidcHandlers/Infrastructure/CachingExtensions.cs @@ -9,6 +9,8 @@ namespace AspNetCore.NonInteractiveOidcHandlers.Infrastructure { internal static class CachingExtensions { + private static readonly Encoding CacheEncoding = Encoding.UTF8; + public static async Task GetTokenAsync(this IDistributedCache cache, string key, CancellationToken cancellationToken = default(CancellationToken)) { var bytes = await cache @@ -19,7 +21,7 @@ internal static class CachingExtensions return null; } - var json = Encoding.UTF8.GetString(bytes); + var json = CacheEncoding.GetString(bytes); var tokenResponse = new TokenResponse(json); return tokenResponse; } @@ -35,7 +37,7 @@ internal static class CachingExtensions var absoluteExpiration = DateTimeOffset.UtcNow.Add(expiresIn < options.CacheDuration ? expiresIn : options.CacheDuration); var json = tokenResponse.Raw; - var bytes = Encoding.UTF8.GetBytes(json); + var bytes = CacheEncoding.GetBytes(json); await cache .SetAsync(key, bytes, new DistributedCacheEntryOptions { AbsoluteExpiration = absoluteExpiration }, cancellationToken) .ConfigureAwait(false); diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/PasswordTokenHandler.cs b/src/AspNetCore.NonInteractiveOidcHandlers/PasswordTokenHandler.cs index 64e1085..5109097 100644 --- a/src/AspNetCore.NonInteractiveOidcHandlers/PasswordTokenHandler.cs +++ b/src/AspNetCore.NonInteractiveOidcHandlers/PasswordTokenHandler.cs @@ -41,13 +41,13 @@ public override async Task GetTokenAsync(CancellationToken cancel } var (userName, password) = userCredentials.Value; - return await GetTokenAsync($"password:{userName}", ct => AcquireToken(userName, password, ct), cancellationToken) + return await GetTokenAsync($"password:{userName}", _ => AcquireTokenAsync(userName, password), cancellationToken) .ConfigureAwait(false); } - private async Task AcquireToken(string userName, string password, CancellationToken cancellationToken) + private async Task AcquireTokenAsync(string userName, string password) { - var lazyToken = _options.LazyTokens.GetOrAdd(userName, _ => new AsyncLazy(() => RequestToken(userName, password))); + var lazyToken = _options.LazyTokens.GetOrAdd(userName, _ => new AsyncLazy(() => RequestTokenAsync(userName, password))); try { var tokenResponse = await lazyToken.Value.ConfigureAwait(false); @@ -55,9 +55,7 @@ private async Task AcquireToken(string userName, string password, { _logger.LogError($"Error returned from token endpoint: {tokenResponse.Error}"); await _options.Events.OnTokenRequestFailed.Invoke(tokenResponse).ConfigureAwait(false); - throw new InvalidOperationException( - $"Token retrieval failed: {tokenResponse.Error} {tokenResponse.ErrorDescription}", - tokenResponse.Exception); + return tokenResponse; } await _options.Events.OnTokenAcquired(tokenResponse).ConfigureAwait(false); @@ -72,7 +70,7 @@ private async Task AcquireToken(string userName, string password, } } - private async Task RequestToken(string userName, string password) + private async Task RequestTokenAsync(string userName, string password) { var httpClient = _httpClientFactory.CreateClient(_options.AuthorityHttpClientName); var tokenEndpoint = await _options.GetTokenEndpointAsync(httpClient).ConfigureAwait(false); diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/PasswordTokenHandlerOptions.cs b/src/AspNetCore.NonInteractiveOidcHandlers/PasswordTokenHandlerOptions.cs index a918004..c6374c6 100644 --- a/src/AspNetCore.NonInteractiveOidcHandlers/PasswordTokenHandlerOptions.cs +++ b/src/AspNetCore.NonInteractiveOidcHandlers/PasswordTokenHandlerOptions.cs @@ -30,7 +30,7 @@ public class PasswordTokenHandlerOptions: TokenHandlerOptions /// public IDictionary ExtraTokenParameters { get; set; } - internal ConcurrentDictionary> LazyTokens { get; set; } + internal ConcurrentDictionary> LazyTokens { get; } = new ConcurrentDictionary>(); public override IEnumerable GetValidationErrors() { diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/PostConfigureClientCredentialsTokenHandlerOptions.cs b/src/AspNetCore.NonInteractiveOidcHandlers/PostConfigureClientCredentialsTokenHandlerOptions.cs deleted file mode 100644 index 5f3ce2c..0000000 --- a/src/AspNetCore.NonInteractiveOidcHandlers/PostConfigureClientCredentialsTokenHandlerOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using AspNetCore.NonInteractiveOidcHandlers.Infrastructure; -using IdentityModel.Client; -using Microsoft.Extensions.Options; - -namespace AspNetCore.NonInteractiveOidcHandlers -{ - internal class PostConfigureClientCredentialsTokenHandlerOptions: IPostConfigureOptions - { - public void PostConfigure(string name, ClientCredentialsTokenHandlerOptions options) - { - options.TokenMutex = new AsyncMutex(); - - } - } -} diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/PostConfigureDelegationTokenHandlerOptions.cs b/src/AspNetCore.NonInteractiveOidcHandlers/PostConfigureDelegationTokenHandlerOptions.cs deleted file mode 100644 index b269656..0000000 --- a/src/AspNetCore.NonInteractiveOidcHandlers/PostConfigureDelegationTokenHandlerOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Concurrent; -using AspNetCore.NonInteractiveOidcHandlers.Infrastructure; -using IdentityModel.Client; -using Microsoft.Extensions.Options; - -namespace AspNetCore.NonInteractiveOidcHandlers -{ - internal class PostConfigureDelegationTokenHandlerOptions: IPostConfigureOptions - { - public void PostConfigure(string name, DelegationTokenHandlerOptions options) - { - options.LazyTokens = new ConcurrentDictionary>(); - } - } -} diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/PostConfigurePasswordTokenHandlerOptions.cs b/src/AspNetCore.NonInteractiveOidcHandlers/PostConfigurePasswordTokenHandlerOptions.cs deleted file mode 100644 index 1eaa4d5..0000000 --- a/src/AspNetCore.NonInteractiveOidcHandlers/PostConfigurePasswordTokenHandlerOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Concurrent; -using AspNetCore.NonInteractiveOidcHandlers.Infrastructure; -using IdentityModel.Client; -using Microsoft.Extensions.Options; - -namespace AspNetCore.NonInteractiveOidcHandlers -{ - public class PostConfigurePasswordTokenHandlerOptions: IPostConfigureOptions - { - public void PostConfigure(string name, PasswordTokenHandlerOptions options) - { - options.LazyTokens = new ConcurrentDictionary>(); - } - } -} diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/PostConfigureRefreshTokenHandlerOptions.cs b/src/AspNetCore.NonInteractiveOidcHandlers/PostConfigureRefreshTokenHandlerOptions.cs deleted file mode 100644 index e54c630..0000000 --- a/src/AspNetCore.NonInteractiveOidcHandlers/PostConfigureRefreshTokenHandlerOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Concurrent; -using AspNetCore.NonInteractiveOidcHandlers.Infrastructure; -using IdentityModel.Client; -using Microsoft.Extensions.Options; - -namespace AspNetCore.NonInteractiveOidcHandlers -{ - public class PostConfigureRefreshTokenHandlerOptions: IPostConfigureOptions - { - public void PostConfigure(string name, RefreshTokenHandlerOptions options) - { - options.LazyTokens = new ConcurrentDictionary>(); - } - } -} diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/RefreshTokenHandler.cs b/src/AspNetCore.NonInteractiveOidcHandlers/RefreshTokenHandler.cs index 489da88..4e935cf 100644 --- a/src/AspNetCore.NonInteractiveOidcHandlers/RefreshTokenHandler.cs +++ b/src/AspNetCore.NonInteractiveOidcHandlers/RefreshTokenHandler.cs @@ -41,13 +41,13 @@ public override async Task GetTokenAsync(CancellationToken cancel return null; } - return await GetTokenAsync($"refresh_token:{refreshToken.ToSha512()}", ct => AcquireToken(refreshToken, ct), cancellationToken) + return await GetTokenAsync($"refresh_token:{refreshToken.ToSha512()}", _ => AcquireTokenAsync(refreshToken), cancellationToken) .ConfigureAwait(false); } - private async Task AcquireToken(string refreshToken, CancellationToken cancellationToken) + private async Task AcquireTokenAsync(string refreshToken) { - var lazyToken = _options.LazyTokens.GetOrAdd(refreshToken, rt => new AsyncLazy(() => RequestToken(rt))); + var lazyToken = _options.LazyTokens.GetOrAdd(refreshToken, rt => new AsyncLazy(() => RequestTokenAsync(rt))); try { var tokenResponse = await lazyToken.Value.ConfigureAwait(false); @@ -55,9 +55,7 @@ private async Task AcquireToken(string refreshToken, Cancellation { _logger.LogError($"Error returned from token endpoint: {tokenResponse.Error}"); await _options.Events.OnTokenRequestFailed.Invoke(tokenResponse).ConfigureAwait(false); - throw new InvalidOperationException( - $"Token retrieval failed: {tokenResponse.Error} {tokenResponse.ErrorDescription}", - tokenResponse.Exception); + return tokenResponse; } await _options.Events.OnTokenAcquired(tokenResponse).ConfigureAwait(false); @@ -72,7 +70,7 @@ private async Task AcquireToken(string refreshToken, Cancellation } } - private async Task RequestToken(string refreshToken) + private async Task RequestTokenAsync(string refreshToken) { var httpClient = _httpClientFactory.CreateClient(_options.AuthorityHttpClientName); var tokenEndpoint = await _options.GetTokenEndpointAsync(httpClient).ConfigureAwait(false); diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/RefreshTokenHandlerOptions.cs b/src/AspNetCore.NonInteractiveOidcHandlers/RefreshTokenHandlerOptions.cs index 2f6e89e..5c89d0c 100644 --- a/src/AspNetCore.NonInteractiveOidcHandlers/RefreshTokenHandlerOptions.cs +++ b/src/AspNetCore.NonInteractiveOidcHandlers/RefreshTokenHandlerOptions.cs @@ -25,7 +25,7 @@ public class RefreshTokenHandlerOptions: TokenHandlerOptions /// public Func RefreshTokenRetriever { get; set; } - internal ConcurrentDictionary> LazyTokens { get; set; } + internal ConcurrentDictionary> LazyTokens { get; } = new ConcurrentDictionary>(); public override IEnumerable GetValidationErrors() { diff --git a/src/AspNetCore.NonInteractiveOidcHandlers/TokenHandlerOptionsExtensions.cs b/src/AspNetCore.NonInteractiveOidcHandlers/TokenHandlerOptionsExtensions.cs index 54bb797..7c30a9a 100644 --- a/src/AspNetCore.NonInteractiveOidcHandlers/TokenHandlerOptionsExtensions.cs +++ b/src/AspNetCore.NonInteractiveOidcHandlers/TokenHandlerOptionsExtensions.cs @@ -15,11 +15,11 @@ public static async Task GetTokenEndpointAsync(this TokenHandlerOptions return options.TokenEndpoint; } - var endpoint = await options.GetTokenEndpointFromDiscoveryDocument(authorityHttpClient).ConfigureAwait(false); + var endpoint = await authorityHttpClient.GetTokenEndpointFromDiscoveryDocument(options).ConfigureAwait(false); return endpoint; } - public static async Task GetTokenEndpointFromDiscoveryDocument(this TokenHandlerOptions options, HttpClient authorityHttpClient) + public static async Task GetTokenEndpointFromDiscoveryDocument(this HttpClient authorityHttpClient, TokenHandlerOptions options) { var discoveryRequest = new DiscoveryDocumentRequest { diff --git a/test/AspNetCore.NonInteractiveOidcHandlers.Tests/ClientCredentials.cs b/test/AspNetCore.NonInteractiveOidcHandlers.Tests/ClientCredentials.cs index 91e3da2..3847aa9 100644 --- a/test/AspNetCore.NonInteractiveOidcHandlers.Tests/ClientCredentials.cs +++ b/test/AspNetCore.NonInteractiveOidcHandlers.Tests/ClientCredentials.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using AspNetCore.NonInteractiveOidcHandlers.Tests.Util; +using IdentityModel.Client; using Microsoft.Extensions.DependencyInjection; using NFluent; using Xunit; @@ -22,16 +23,36 @@ public class ClientCredentials }; [Fact] - public void Token_request_error_should_throw() + public async Task Token_request_error_should_trigger_unauthenticated_request_to_api() { + var api = new DownstreamApiHandler(); var client = HostFactory.CreateClient( b => b.AddOidcClientCredentials(_options), - TokenEndpointHandler.BadRequest("invalid_grant")); + TokenEndpointHandler.OidcProtocolError("invalid_grant"), + api: api); - async Task Act() => await client.GetAsync("https://default"); + await client.GetAsync("https://default"); - Check.ThatAsyncCode(Act) - .Throws().WithMessage("Token retrieval failed: invalid_grant "); + Check.That(api.LastRequestToken).IsNull(); + } + + [Fact] + public async Task Token_request_error_should_trigger_TokenRequestFailed_event() + { + var eventsMock = new TokenEventsMock(); + var client = HostFactory.CreateClient( + b => b.AddOidcClientCredentials(o => + { + _options(o); + o.Events = eventsMock.CreateEvents(); + }), + TokenEndpointHandler.OidcProtocolError("invalid_grant")); + + await client.GetAsync("https://default"); + + Check.That(eventsMock.LatestTokenRequestFailed).IsNotNull(); + Check.That(eventsMock.LatestTokenRequestFailed.ErrorType).IsEqualTo(ResponseErrorType.Protocol); + Check.That(eventsMock.LatestTokenRequestFailed.Error).IsEqualTo("invalid_grant"); } [Fact] @@ -52,6 +73,24 @@ public async Task Successful_token_request_should_trigger_authenticated_request_ Check.That(api.LastRequestToken).IsEqualTo("access-token"); } + [Fact] + public async Task Successful_token_request_should_trigger_TokenAcquired_event() + { + var eventsMock = new TokenEventsMock(); + var client = HostFactory.CreateClient( + b => b.AddOidcClientCredentials(o => + { + _options(o); + o.Events = eventsMock.CreateEvents(); + }), + TokenEndpointHandler.ValidBearerToken("access-token", TimeSpan.MaxValue)); + + await client.GetAsync("https://default"); + + Check.That(eventsMock.LatestTokenAcquired).IsNotNull(); + Check.That(eventsMock.LatestTokenAcquired.AccessToken).IsEqualTo("access-token"); + } + [Fact] public async Task Successful_token_request_should_trigger_authenticated_request_to_proper_api_when_multiple_clients_registered() { diff --git a/test/AspNetCore.NonInteractiveOidcHandlers.Tests/Password.cs b/test/AspNetCore.NonInteractiveOidcHandlers.Tests/Password.cs index 85761d9..774ef7f 100644 --- a/test/AspNetCore.NonInteractiveOidcHandlers.Tests/Password.cs +++ b/test/AspNetCore.NonInteractiveOidcHandlers.Tests/Password.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using AspNetCore.NonInteractiveOidcHandlers.Tests.Util; +using IdentityModel.Client; using Microsoft.Extensions.DependencyInjection; using NFluent; using Xunit; @@ -25,17 +26,36 @@ public class PasswordTests }; [Fact] - public void Token_request_error_should_throw() + public async Task Token_request_error_should_trigger_unauthenticated_request_to_api() { - var client = HostFactory - .CreateClient( - b => b.AddOidcPassword(_options), - TokenEndpointHandler.BadRequest("invalid_grant")); + var api = new DownstreamApiHandler(); + var client = HostFactory.CreateClient( + b => b.AddOidcPassword(_options), + TokenEndpointHandler.OidcProtocolError("invalid_grant"), + api: api); - async Task Act() => await client.GetAsync("https://default"); + await client.GetAsync("https://default"); - Check.ThatAsyncCode(Act) - .Throws().WithMessage("Token retrieval failed: invalid_grant "); + Check.That(api.LastRequestToken).IsNull(); + } + + [Fact] + public async Task Token_request_error_should_trigger_TokenRequestFailed_event() + { + var eventsMock = new TokenEventsMock(); + var client = HostFactory.CreateClient( + b => b.AddOidcPassword(o => + { + _options(o); + o.Events = eventsMock.CreateEvents(); + }), + TokenEndpointHandler.OidcProtocolError("invalid_grant")); + + await client.GetAsync("https://default"); + + Check.That(eventsMock.LatestTokenRequestFailed).IsNotNull(); + Check.That(eventsMock.LatestTokenRequestFailed.ErrorType).IsEqualTo(ResponseErrorType.Protocol); + Check.That(eventsMock.LatestTokenRequestFailed.Error).IsEqualTo("invalid_grant"); } [Fact] @@ -59,6 +79,24 @@ public async Task Successful_token_request_should_trigger_authenticated_request_ Check.That(api.LastRequestToken).IsEqualTo("access-token"); } + [Fact] + public async Task Successful_token_request_should_trigger_TokenAcquired_event() + { + var eventsMock = new TokenEventsMock(); + var client = HostFactory.CreateClient( + b => b.AddOidcPassword(o => + { + _options(o); + o.Events = eventsMock.CreateEvents(); + }), + TokenEndpointHandler.ValidBearerToken("access-token", TimeSpan.MaxValue)); + + await client.GetAsync("https://default"); + + Check.That(eventsMock.LatestTokenAcquired).IsNotNull(); + Check.That(eventsMock.LatestTokenAcquired.AccessToken).IsEqualTo("access-token"); + } + [Fact] public async Task Successful_token_request_should_trigger_authenticated_request_to_proper_api_when_multiple_clients_registered() { diff --git a/test/AspNetCore.NonInteractiveOidcHandlers.Tests/RefreshToken.cs b/test/AspNetCore.NonInteractiveOidcHandlers.Tests/RefreshToken.cs index ced2d80..188951b 100644 --- a/test/AspNetCore.NonInteractiveOidcHandlers.Tests/RefreshToken.cs +++ b/test/AspNetCore.NonInteractiveOidcHandlers.Tests/RefreshToken.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using AspNetCore.NonInteractiveOidcHandlers.Tests.Util; +using IdentityModel.Client; using Microsoft.Extensions.DependencyInjection; using NFluent; using Xunit; @@ -22,16 +23,36 @@ public class RefreshTokenTests }; [Fact] - public void Token_request_error_should_throw() + public async Task Token_request_error_should_trigger_unauthenticated_request_to_api() { + var api = new DownstreamApiHandler(); var client = HostFactory.CreateClient( b => b.AddOidcRefreshToken(_options), - TokenEndpointHandler.BadRequest("invalid_grant")); + TokenEndpointHandler.OidcProtocolError("invalid_grant"), + api: api); - async Task Act() => await client.GetAsync("https://default"); + await client.GetAsync("https://default"); - Check.ThatAsyncCode(Act) - .Throws().WithMessage("Token retrieval failed: invalid_grant "); + Check.That(api.LastRequestToken).IsNull(); + } + + [Fact] + public async Task Token_request_error_should_trigger_TokenRequestFailed_event() + { + var eventsMock = new TokenEventsMock(); + var client = HostFactory.CreateClient( + b => b.AddOidcRefreshToken(o => + { + _options(o); + o.Events = eventsMock.CreateEvents(); + }), + TokenEndpointHandler.OidcProtocolError("invalid_grant")); + + await client.GetAsync("https://default"); + + Check.That(eventsMock.LatestTokenRequestFailed).IsNotNull(); + Check.That(eventsMock.LatestTokenRequestFailed.ErrorType).IsEqualTo(ResponseErrorType.Protocol); + Check.That(eventsMock.LatestTokenRequestFailed.Error).IsEqualTo("invalid_grant"); } [Fact] @@ -52,6 +73,24 @@ public async Task Successful_token_request_should_trigger_authenticated_request_ Check.That(api.LastRequestToken).IsEqualTo("access-token"); } + [Fact] + public async Task Successful_token_request_should_trigger_TokenAcquired_event() + { + var eventsMock = new TokenEventsMock(); + var client = HostFactory.CreateClient( + b => b.AddOidcRefreshToken(o => + { + _options(o); + o.Events = eventsMock.CreateEvents(); + }), + TokenEndpointHandler.ValidBearerToken("access-token", TimeSpan.MaxValue)); + + await client.GetAsync("https://default"); + + Check.That(eventsMock.LatestTokenAcquired).IsNotNull(); + Check.That(eventsMock.LatestTokenAcquired.AccessToken).IsEqualTo("access-token"); + } + [Fact] public async Task Successful_token_request_should_trigger_authenticated_request_to_proper_api_when_multiple_clients_registered() { diff --git a/test/AspNetCore.NonInteractiveOidcHandlers.Tests/TokenDelegation.cs b/test/AspNetCore.NonInteractiveOidcHandlers.Tests/TokenDelegation.cs index 037f2ee..8b0204d 100644 --- a/test/AspNetCore.NonInteractiveOidcHandlers.Tests/TokenDelegation.cs +++ b/test/AspNetCore.NonInteractiveOidcHandlers.Tests/TokenDelegation.cs @@ -2,6 +2,7 @@ using System.Net.Http; using System.Threading.Tasks; using AspNetCore.NonInteractiveOidcHandlers.Tests.Util; +using IdentityModel.Client; using Microsoft.Extensions.DependencyInjection; using NFluent; using Xunit; @@ -32,17 +33,39 @@ public async Task Unauthenticated_request_from_upstream_should_not_trigger_deleg } [Fact] - public void Token_delegation_error_should_throw() + public async Task Token_delegation_error_should_trigger_unauthenticated_request_to_downstream() { + var downstreamApi = new DownstreamApiHandler(); var client = WebHostFactory.CreateClient( b => b.AddOidcTokenDelegation(_options), - TokenEndpointHandler.BadRequest("invalid_grant")); + TokenEndpointHandler.OidcProtocolError("invalid_grant"), + downstreamApi: downstreamApi); + client.SetBearerToken("1234"); + + await client.GetAsync("https://default"); + + Check.That(downstreamApi.LastRequestToken).IsNull(); + } + + [Fact] + public async Task Token_request_error_should_trigger_TokenRequestFailed_event() + { + var eventsMock = new TokenEventsMock(); + var client = WebHostFactory.CreateClient( + b => b.AddOidcTokenDelegation(o => + { + _options(o); + o.Events = eventsMock.CreateEvents(); + }), + TokenEndpointHandler.OidcProtocolError("invalid_grant"), + downstreamApi: new DownstreamApiHandler()); client.SetBearerToken("1234"); - async Task Act() => await client.GetAsync("https://default"); + await client.GetAsync("https://default"); - Check.ThatAsyncCode(Act) - .Throws().WithMessage("Token retrieval failed: invalid_grant "); + Check.That(eventsMock.LatestTokenRequestFailed).IsNotNull(); + Check.That(eventsMock.LatestTokenRequestFailed.ErrorType).IsEqualTo(ResponseErrorType.Protocol); + Check.That(eventsMock.LatestTokenRequestFailed.Error).IsEqualTo("invalid_grant"); } [Fact] @@ -62,6 +85,26 @@ public async Task Authenticated_request_from_upstream_should_trigger_authenticat Check.That(downstreamApi.LastRequestToken).IsEqualTo("downstream-token"); } + [Fact] + public async Task Successful_token_request_should_trigger_TokenAcquired_event() + { + var eventsMock = new TokenEventsMock(); + var client = WebHostFactory.CreateClient( + b => b.AddOidcTokenDelegation(o => + { + _options(o); + o.Events = eventsMock.CreateEvents(); + }), + TokenEndpointHandler.ValidBearerToken("access-token", TimeSpan.MaxValue), + downstreamApi: new DownstreamApiHandler()); + client.SetBearerToken("upstream-token"); + + await client.GetAsync("https://default"); + + Check.That(eventsMock.LatestTokenAcquired).IsNotNull(); + Check.That(eventsMock.LatestTokenAcquired.AccessToken).IsEqualTo("access-token"); + } + [Fact] public async Task Authenticated_request_from_upstream_should_trigger_authenticated_request_to_proper_downstream_when_multiple_clients_registered() { diff --git a/test/AspNetCore.NonInteractiveOidcHandlers.Tests/Util/TokenEndpointHandler.cs b/test/AspNetCore.NonInteractiveOidcHandlers.Tests/Util/TokenEndpointHandler.cs index 8a57cf1..321bab2 100644 --- a/test/AspNetCore.NonInteractiveOidcHandlers.Tests/Util/TokenEndpointHandler.cs +++ b/test/AspNetCore.NonInteractiveOidcHandlers.Tests/Util/TokenEndpointHandler.cs @@ -20,7 +20,7 @@ public static TokenEndpointHandler ValidBearerToken(string accessToken, TimeSpan expires_in = expiresIn.TotalSeconds, }); - public static TokenEndpointHandler BadRequest(string error) + public static TokenEndpointHandler OidcProtocolError(string error) => new TokenEndpointHandler(HttpStatusCode.BadRequest, new { error }); private readonly HttpStatusCode _statusCode; diff --git a/test/AspNetCore.NonInteractiveOidcHandlers.Tests/Util/TokenEventsMock.cs b/test/AspNetCore.NonInteractiveOidcHandlers.Tests/Util/TokenEventsMock.cs new file mode 100644 index 0000000..91c1459 --- /dev/null +++ b/test/AspNetCore.NonInteractiveOidcHandlers.Tests/Util/TokenEventsMock.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using IdentityModel.Client; + +namespace AspNetCore.NonInteractiveOidcHandlers.Tests.Util +{ + public class TokenEventsMock + { + public TokenResponse LatestTokenAcquired { get; private set; } + public TokenResponse LatestTokenRequestFailed { get; private set; } + + public TokenHandlerEvents CreateEvents() + => new TokenHandlerEvents() + { + OnTokenAcquired = tokenResponse => + { + LatestTokenAcquired = tokenResponse; + return Task.CompletedTask; + }, + OnTokenRequestFailed = tokenResponse => + { + LatestTokenRequestFailed = tokenResponse; + return Task.CompletedTask; + }, + }; + } +}