From d06df142e8c4c1c1f6a4d61ba9ba69bedb9c83c8 Mon Sep 17 00:00:00 2001 From: David Lebee Date: Thu, 23 Jul 2020 12:50:46 -0400 Subject: [PATCH] Add project files. --- PoweredSoft.DynamicJwtBearer.sln | 25 +++ .../DynamicJwtBearerExtensions.cs | 22 +++ .../DynamicJwtBearerHandler.cs | 174 ++++++++++++++++++ ...micJwtBearerHanderConfigurationResolver.cs | 11 ++ .../LoggingExtensions.cs | 38 ++++ .../PoweredSoft.DynamicJwtBearer.csproj | 12 ++ 6 files changed, 282 insertions(+) create mode 100644 PoweredSoft.DynamicJwtBearer.sln create mode 100644 PoweredSoft.DynamicJwtBearer/DynamicJwtBearerExtensions.cs create mode 100644 PoweredSoft.DynamicJwtBearer/DynamicJwtBearerHandler.cs create mode 100644 PoweredSoft.DynamicJwtBearer/IDynamicJwtBearerHanderConfigurationResolver.cs create mode 100644 PoweredSoft.DynamicJwtBearer/LoggingExtensions.cs create mode 100644 PoweredSoft.DynamicJwtBearer/PoweredSoft.DynamicJwtBearer.csproj diff --git a/PoweredSoft.DynamicJwtBearer.sln b/PoweredSoft.DynamicJwtBearer.sln new file mode 100644 index 0000000..cef476e --- /dev/null +++ b/PoweredSoft.DynamicJwtBearer.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29503.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.DynamicJwtBearer", "PoweredSoft.DynamicJwtBearer\PoweredSoft.DynamicJwtBearer.csproj", "{0A15F002-66C4-44D1-8162-563F860C49E4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0A15F002-66C4-44D1-8162-563F860C49E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A15F002-66C4-44D1-8162-563F860C49E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A15F002-66C4-44D1-8162-563F860C49E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A15F002-66C4-44D1-8162-563F860C49E4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9D366086-7F19-4F1E-A5B5-DAEF1EA56D90} + EndGlobalSection +EndGlobal diff --git a/PoweredSoft.DynamicJwtBearer/DynamicJwtBearerExtensions.cs b/PoweredSoft.DynamicJwtBearer/DynamicJwtBearerExtensions.cs new file mode 100644 index 0000000..dba83e3 --- /dev/null +++ b/PoweredSoft.DynamicJwtBearer/DynamicJwtBearerExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using System; + +namespace PoweredSoft.DynamicJwtBearer +{ + public static class DynamicJwtBearerExtensions + { + public static AuthenticationBuilder AddDynamicJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, Action action = null) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, JwtBearerPostConfigureOptions>()); + + if (action != null) + return builder.AddScheme(authenticationScheme, null, action); + + return builder.AddScheme(authenticationScheme, null, _ => { }); + } + } +} diff --git a/PoweredSoft.DynamicJwtBearer/DynamicJwtBearerHandler.cs b/PoweredSoft.DynamicJwtBearer/DynamicJwtBearerHandler.cs new file mode 100644 index 0000000..5a6a417 --- /dev/null +++ b/PoweredSoft.DynamicJwtBearer/DynamicJwtBearerHandler.cs @@ -0,0 +1,174 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; + +namespace PoweredSoft.DynamicJwtBearer +{ + public class DynamicJwtBearerHandler : JwtBearerHandler + { + private readonly IDynamicJwtBearerHanderConfigurationResolver dynamicJwtBearerHanderConfigurationResolver; + + public DynamicJwtBearerHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IDynamicJwtBearerHanderConfigurationResolver dynamicJwtBearerHanderConfigurationResolver) : base(options, logger, encoder, clock) + { + this.dynamicJwtBearerHanderConfigurationResolver = dynamicJwtBearerHanderConfigurationResolver; + } + + /// + /// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using set in the options. + /// + /// + protected override async Task HandleAuthenticateAsync() + { + string token = null; + try + { + // Give application opportunity to find from a different location, adjust, or reject token + var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options); + + // event can set the token + await Events.MessageReceived(messageReceivedContext); + if (messageReceivedContext.Result != null) + { + return messageReceivedContext.Result; + } + + // If application retrieved token from somewhere else, use that. + token = messageReceivedContext.Token; + + if (string.IsNullOrEmpty(token)) + { + string authorization = Request.Headers[HeaderNames.Authorization]; + + // If no authorization header found, nothing to process further + if (string.IsNullOrEmpty(authorization)) + { + return AuthenticateResult.NoResult(); + } + + if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + token = authorization.Substring("Bearer ".Length).Trim(); + } + + // If no token found, no further work possible + if (string.IsNullOrEmpty(token)) + { + return AuthenticateResult.NoResult(); + } + } + + var currentConfiguration = await this.dynamicJwtBearerHanderConfigurationResolver.ResolveCurrentOpenIdConfiguration(Context); + var validationParameters = Options.TokenValidationParameters.Clone(); + if (currentConfiguration != null) + { + var issuers = new[] { currentConfiguration.Issuer }; + validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers; + + validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(currentConfiguration.SigningKeys) + ?? currentConfiguration.SigningKeys; + } + + List validationFailures = null; + SecurityToken validatedToken; + foreach (var validator in Options.SecurityTokenValidators) + { + if (validator.CanReadToken(token)) + { + ClaimsPrincipal principal; + try + { + principal = validator.ValidateToken(token, validationParameters, out validatedToken); + } + catch (Exception ex) + { + Logger.TokenValidationFailed(ex); + + // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event. + if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null + && ex is SecurityTokenSignatureKeyNotFoundException) + { + Options.ConfigurationManager.RequestRefresh(); + } + + if (validationFailures == null) + { + validationFailures = new List(1); + } + validationFailures.Add(ex); + continue; + } + + Logger.TokenValidationSucceeded(); + + var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options) + { + Principal = principal, + SecurityToken = validatedToken + }; + + await Events.TokenValidated(tokenValidatedContext); + if (tokenValidatedContext.Result != null) + { + return tokenValidatedContext.Result; + } + + if (Options.SaveToken) + { + tokenValidatedContext.Properties.StoreTokens(new[] + { + new AuthenticationToken { Name = "access_token", Value = token } + }); + } + + tokenValidatedContext.Success(); + return tokenValidatedContext.Result; + } + } + + if (validationFailures != null) + { + var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options) + { + Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures) + }; + + await Events.AuthenticationFailed(authenticationFailedContext); + if (authenticationFailedContext.Result != null) + { + return authenticationFailedContext.Result; + } + + return AuthenticateResult.Fail(authenticationFailedContext.Exception); + } + + return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]"); + } + catch (Exception ex) + { + Logger.ErrorProcessingMessage(ex); + + var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options) + { + Exception = ex + }; + + await Events.AuthenticationFailed(authenticationFailedContext); + if (authenticationFailedContext.Result != null) + { + return authenticationFailedContext.Result; + } + + throw; + } + } + } +} diff --git a/PoweredSoft.DynamicJwtBearer/IDynamicJwtBearerHanderConfigurationResolver.cs b/PoweredSoft.DynamicJwtBearer/IDynamicJwtBearerHanderConfigurationResolver.cs new file mode 100644 index 0000000..de14d31 --- /dev/null +++ b/PoweredSoft.DynamicJwtBearer/IDynamicJwtBearerHanderConfigurationResolver.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using System.Threading.Tasks; + +namespace PoweredSoft.DynamicJwtBearer +{ + public interface IDynamicJwtBearerHanderConfigurationResolver + { + Task ResolveCurrentOpenIdConfiguration(HttpContext context); + } +} diff --git a/PoweredSoft.DynamicJwtBearer/LoggingExtensions.cs b/PoweredSoft.DynamicJwtBearer/LoggingExtensions.cs new file mode 100644 index 0000000..c2dcaa6 --- /dev/null +++ b/PoweredSoft.DynamicJwtBearer/LoggingExtensions.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Logging; +using System; + +namespace PoweredSoft.DynamicJwtBearer +{ + + internal static class LoggingExtensions + { + private static Action _tokenValidationFailed; + private static Action _tokenValidationSucceeded; + private static Action _errorProcessingMessage; + + static LoggingExtensions() + { + _tokenValidationFailed = LoggerMessage.Define( + eventId: new EventId(1, "TokenValidationFailed"), + logLevel: LogLevel.Information, + formatString: "Failed to validate the token."); + _tokenValidationSucceeded = LoggerMessage.Define( + eventId: new EventId(2, "TokenValidationSucceeded"), + logLevel: LogLevel.Information, + formatString: "Successfully validated the token."); + _errorProcessingMessage = LoggerMessage.Define( + eventId: new EventId(3, "ProcessingMessageFailed"), + logLevel: LogLevel.Error, + formatString: "Exception occurred while processing message."); + } + + public static void TokenValidationFailed(this ILogger logger, Exception ex) + => _tokenValidationFailed(logger, ex); + + public static void TokenValidationSucceeded(this ILogger logger) + => _tokenValidationSucceeded(logger, null); + + public static void ErrorProcessingMessage(this ILogger logger, Exception ex) + => _errorProcessingMessage(logger, ex); + } +} diff --git a/PoweredSoft.DynamicJwtBearer/PoweredSoft.DynamicJwtBearer.csproj b/PoweredSoft.DynamicJwtBearer/PoweredSoft.DynamicJwtBearer.csproj new file mode 100644 index 0000000..90b6520 --- /dev/null +++ b/PoweredSoft.DynamicJwtBearer/PoweredSoft.DynamicJwtBearer.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.0 + + + + + + + +