diff --git a/src/Http.Authentication.JwtBearer/ClaimsPrincipalConverter.cs b/src/Http.Authentication.JwtBearer/ClaimsPrincipalConverter.cs new file mode 100644 index 0000000..321b987 --- /dev/null +++ b/src/Http.Authentication.JwtBearer/ClaimsPrincipalConverter.cs @@ -0,0 +1,28 @@ +using Microsoft.Azure.Functions.Worker.Converters; +using System.Security.Claims; + +namespace Microsoft.Azure.Functions.Worker; + +internal class ClaimsPrincipalConverter : IInputConverter +{ + public ValueTask ConvertAsync(ConverterContext context) + { + if (context.TargetType != typeof(ClaimsPrincipal)) + { + return ValueTask.FromResult(ConversionResult.Unhandled()); + } + + try + { + var principal = context.FunctionContext.GetClaimsPrincipal(); + + return principal is null + ? ValueTask.FromResult(ConversionResult.Unhandled()) + : ValueTask.FromResult(ConversionResult.Success(principal)); + } + catch (Exception ex) + { + return ValueTask.FromResult(ConversionResult.Failed(ex)); + } + } +} \ No newline at end of file diff --git a/src/Http.Authentication.JwtBearer/FunctionContextExtensions.cs b/src/Http.Authentication.JwtBearer/FunctionContextExtensions.cs deleted file mode 100644 index 7b00988..0000000 --- a/src/Http.Authentication.JwtBearer/FunctionContextExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Microsoft.Azure.Functions.Worker.Http; - -internal static class FunctionContextExtensions -{ - // https://github.com/Azure/azure-functions-dotnet-worker/issues/414 - public static HttpRequestData? GetHttpRequestData(this FunctionContext context) - { - var feature = context.Features.SingleOrDefault(f => f.Key.Name == "IFunctionBindingsFeature").Value; - Type type = feature.GetType(); - var inputData = type.GetProperties().Single(p => p.Name == "InputData").GetValue(feature) as IReadOnlyDictionary; - return inputData?.Values.SingleOrDefault(o => o is HttpRequestData) as HttpRequestData; - } - - public static void SetHttpResponseData(this FunctionContext context, HttpResponseData response) - { - var feature = context.Features.SingleOrDefault(f => f.Key.Name == "IFunctionBindingsFeature").Value; - Type type = feature.GetType(); - var result = type.GetProperties().Single(p => p.Name == "InvocationResult"); - result.SetValue(feature, response); - } -} diff --git a/src/Http.Authentication.JwtBearer/Http.Authentication.JwtBearer.csproj b/src/Http.Authentication.JwtBearer/Http.Authentication.JwtBearer.csproj index c57260e..36fde39 100644 --- a/src/Http.Authentication.JwtBearer/Http.Authentication.JwtBearer.csproj +++ b/src/Http.Authentication.JwtBearer/Http.Authentication.JwtBearer.csproj @@ -9,9 +9,9 @@ - + - + diff --git a/src/Http.Authentication.JwtBearer/JwtBearerAuthenticationMiddleware.cs b/src/Http.Authentication.JwtBearer/JwtBearerAuthenticationMiddleware.cs index 4332a78..92ee488 100644 --- a/src/Http.Authentication.JwtBearer/JwtBearerAuthenticationMiddleware.cs +++ b/src/Http.Authentication.JwtBearer/JwtBearerAuthenticationMiddleware.cs @@ -22,7 +22,7 @@ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next if (authentication is not null) { - var request = context.GetHttpRequestData(); + var request = await context.GetHttpRequestDataAsync().ConfigureAwait(false); if (request is not null) { @@ -30,18 +30,18 @@ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next if (principal is null) { - context.SetHttpResponseData(request.CreateResponse(HttpStatusCode.Unauthorized)); + var response = request.CreateResponse(HttpStatusCode.Unauthorized); + context.GetInvocationResult().Value = response; return; } - else - { - context.Items[nameof(ClaimsPrincipal)] = principal; - if (!authentication.IsAuthorized(principal)) - { - context.SetHttpResponseData(request.CreateResponse(HttpStatusCode.Forbidden)); - return; - } + context.Items[nameof(ClaimsPrincipal)] = principal; + + if (!authentication.IsAuthorized(principal)) + { + var response = request.CreateResponse(HttpStatusCode.Forbidden); + context.GetInvocationResult().Value = response; + return; } } } @@ -58,6 +58,21 @@ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next try { + if (!request.Headers.TryGetValues("Authorization", out var values) || !values.Any()) + { + logger.LogTrace("No Authorization header present on the request."); + return null; + } + + var authorization = values.Single(); + var value = AuthenticationHeaderValue.Parse(authorization); + + if (!string.Equals(JwtBearerDefaults.AuthenticationScheme, value.Scheme, StringComparison.InvariantCultureIgnoreCase)) + { + logger.LogTrace($"Authorizatrion header scheme {JwtBearerDefaults.AuthenticationScheme} is not supported."); + return null; + } + var options = context.InstanceServices.GetRequiredService>().CurrentValue; if (_configuration is null && options.ConfigurationManager != null) @@ -65,77 +80,65 @@ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next _configuration = await options.ConfigurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); } - if (request.Headers.TryGetValues("Authorization", out var values)) - { - var authorization = values.SingleOrDefault(); - if (authorization is not null) - { - var value = AuthenticationHeaderValue.Parse(authorization); + var token = value.Parameter; + var validationParameters = options.TokenValidationParameters.Clone(); - if (string.Equals(JwtBearerDefaults.AuthenticationScheme, value.Scheme, StringComparison.InvariantCultureIgnoreCase)) - { - var token = value.Parameter; - var validationParameters = options.TokenValidationParameters.Clone(); + if (_configuration is not null) + { + var issuers = new[] { _configuration.Issuer }; + validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers; - if (_configuration is not null) - { - var issuers = new[] { _configuration.Issuer }; - validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers; + validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) + ?? _configuration.SigningKeys; + } - validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) - ?? _configuration.SigningKeys; - } + List? validationFailures = null; + SecurityToken? validatedToken = null; + foreach (var validator in options.SecurityTokenValidators) + { + if (validator.CanReadToken(token)) + { + ClaimsPrincipal principal; + try + { + principal = validator.ValidateToken(token, validationParameters, out validatedToken); + } + catch (Exception ex) + { + logger.LogTrace(ex, "Token validation failed: {Message}", ex.Message); - List? validationFailures = null; - SecurityToken? validatedToken = null; - foreach (var validator in options.SecurityTokenValidators) + // 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) { - if (validator.CanReadToken(token)) - { - ClaimsPrincipal principal; - try - { - principal = validator.ValidateToken(token, validationParameters, out validatedToken); - } - catch (Exception ex) - { - logger.LogInformation(ex, "Token validation failed: {Message}", ex.Message); - - // 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.LogTrace("Token validation succeeded for principal: {Identity}", principal.Identity?.Name ?? "[null]"); - - return principal; - } + options.ConfigurationManager.RequestRefresh(); } - if (validationFailures is not null) + if (validationFailures == null) { - var exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures); - logger.LogTrace(exception, "Token validation failed: {Message}", exception.Message); - } - else - { - logger.LogTrace("No SecurityTokenValidator available for token: {Token}", token ?? "[null]"); + validationFailures = new List(1); } + validationFailures.Add(ex); + continue; } + + logger.LogTrace("Token validation succeeded for principal: {Identity}", principal.Identity?.Name ?? "[null]"); + + return principal; } } + if (validationFailures is not null) + { + var exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures); + logger.LogTrace(exception, "Token validation failed: {Message}", exception.Message); + } + else + { + logger.LogTrace("No SecurityTokenValidator available for token: {Token}", token ?? "[null]"); + } + return null; } catch (Exception ex) diff --git a/src/Http.Authentication.JwtBearer/JwtExtensions.cs b/src/Http.Authentication.JwtBearer/JwtExtensions.cs index d00fb42..551fb5e 100644 --- a/src/Http.Authentication.JwtBearer/JwtExtensions.cs +++ b/src/Http.Authentication.JwtBearer/JwtExtensions.cs @@ -25,6 +25,8 @@ public static IFunctionsWorkerApplicationBuilder AddJwtBearerAuthentication(this builder.UseMiddleware(); + builder.Services.Configure(options => options.InputConverters.Register()); + return builder; } diff --git a/src/Http/Http.csproj b/src/Http/Http.csproj index 7f6708d..3456f53 100644 --- a/src/Http/Http.csproj +++ b/src/Http/Http.csproj @@ -10,7 +10,7 @@ - +