/* * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) * See https://github.com/openiddict/openiddict-core for more information concerning * the license and the contributors participating to this project. */ using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using Microsoft.AspNetCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Properties = OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreConstants.Properties; #if SUPPORTS_JSON_NODES using System.Text.Json.Nodes; #endif namespace OpenIddict.Validation.AspNetCore; [EditorBrowsable(EditorBrowsableState.Never)] public static partial class OpenIddictValidationAspNetCoreHandlers { public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( /* * Request top-level processing: */ InferIssuerFromHost.Descriptor, /* * Authentication processing: */ ExtractAccessTokenFromAuthorizationHeader.Descriptor, ExtractAccessTokenFromBodyForm.Descriptor, ExtractAccessTokenFromQueryString.Descriptor, /* * Challenge processing: */ AttachHostChallengeError.Descriptor, /* * Response processing: */ AttachHttpResponseCode.Descriptor, AttachCacheControlHeader.Descriptor, AttachWwwAuthenticateHeader.Descriptor, ProcessChallengeErrorResponse.Descriptor, ProcessJsonResponse.Descriptor, AttachHttpResponseCode.Descriptor, AttachCacheControlHeader.Descriptor, AttachWwwAuthenticateHeader.Descriptor, ProcessChallengeErrorResponse.Descriptor, ProcessJsonResponse.Descriptor); /// /// Contains the logic responsible for infering the default issuer from the HTTP request host and validating it. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class InferIssuerFromHost : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() .SetOrder(int.MinValue + 100_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessRequestContext context!!) { // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, // this may indicate that the request was incorrectly processed by another server stack. var request = context.Transaction.GetHttpRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); // Only use the current host as the issuer if the // issuer was not explicitly set in the options. if (context.Issuer is not null) { return default; } if (!request.Host.HasValue) { context.Reject( error: Errors.InvalidRequest, description: SR.FormatID2081(HeaderNames.Host), uri: SR.FormatID8000(SR.ID2081)); return default; } if (!Uri.TryCreate(request.Scheme + Uri.SchemeDelimiter + request.Host + request.PathBase, UriKind.Absolute, out Uri? issuer) || !issuer.IsWellFormedOriginalString()) { context.Reject( error: Errors.InvalidRequest, description: SR.FormatID2082(HeaderNames.Host), uri: SR.FormatID8000(SR.ID2082)); return default; } context.Issuer = issuer; return default; } } /// /// Contains the logic responsible for extracting the access token from the standard HTTP Authorization header. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class ExtractAccessTokenFromAuthorizationHeader : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() .UseSingletonHandler() .SetOrder(EvaluateValidatedTokens.Descriptor.Order + 500) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessAuthenticationContext context!!) { // If a token was already resolved, don't overwrite it. if (!string.IsNullOrEmpty(context.AccessToken)) { return default; } // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, // this may indicate that the request was incorrectly processed by another server stack. var request = context.Transaction.GetHttpRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); // Resolve the access token from the standard Authorization header. // See https://tools.ietf.org/html/rfc6750#section-2.1 for more information. string header = request.Headers[HeaderNames.Authorization]; if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { context.AccessToken = header.Substring("Bearer ".Length); return default; } return default; } } /// /// Contains the logic responsible for extracting the access token from the standard access_token form parameter. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class ExtractAccessTokenFromBodyForm : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() .UseSingletonHandler() .SetOrder(ExtractAccessTokenFromAuthorizationHeader.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); /// public async ValueTask HandleAsync(ProcessAuthenticationContext context!!) { // If a token was already resolved, don't overwrite it. if (!string.IsNullOrEmpty(context.AccessToken)) { return; } // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, // this may indicate that the request was incorrectly processed by another server stack. var request = context.Transaction.GetHttpRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); if (string.IsNullOrEmpty(request.ContentType) || !request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { return; } // Resolve the access token from the standard access_token form parameter. // See https://tools.ietf.org/html/rfc6750#section-2.2 for more information. var form = await request.ReadFormAsync(request.HttpContext.RequestAborted); if (form.TryGetValue(Parameters.AccessToken, out StringValues token)) { context.AccessToken = token; return; } } } /// /// Contains the logic responsible for extracting the access token from the standard access_token query parameter. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class ExtractAccessTokenFromQueryString : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() .UseSingletonHandler() .SetOrder(ExtractAccessTokenFromBodyForm.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessAuthenticationContext context!!) { // If a token was already resolved, don't overwrite it. if (!string.IsNullOrEmpty(context.AccessToken)) { return default; } // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, // this may indicate that the request was incorrectly processed by another server stack. var request = context.Transaction.GetHttpRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); // Resolve the access token from the standard access_token query parameter. // See https://tools.ietf.org/html/rfc6750#section-2.3 for more information. if (request.Query.TryGetValue(Parameters.AccessToken, out StringValues token)) { context.AccessToken = token; return default; } return default; } } /// /// Contains the logic responsible for attaching the error details using the ASP.NET Core authentication properties. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class AttachHostChallengeError : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() .SetOrder(int.MinValue + 50_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessChallengeContext context!!) { var properties = context.Transaction.GetProperty(typeof(AuthenticationProperties).FullName!); if (properties is null) { return default; } if (properties.Items.TryGetValue(Properties.Error, out string? error) && !string.IsNullOrEmpty(error)) { context.Parameters[Parameters.Error] = error; } if (properties.Items.TryGetValue(Properties.ErrorDescription, out string? description) && !string.IsNullOrEmpty(description)) { context.Parameters[Parameters.ErrorDescription] = description; } if (properties.Items.TryGetValue(Properties.ErrorUri, out string? uri) && !string.IsNullOrEmpty(uri)) { context.Parameters[Parameters.ErrorUri] = uri; } if (properties.Items.TryGetValue(Properties.Scope, out string? scope) && !string.IsNullOrEmpty(scope)) { context.Parameters[Parameters.Scope] = scope; } foreach (var parameter in properties.Parameters) { context.Parameters[parameter.Key] = parameter.Value switch { OpenIddictParameter value => value, JsonElement value => new OpenIddictParameter(value), bool value => new OpenIddictParameter(value), int value => new OpenIddictParameter(value), long value => new OpenIddictParameter(value), string value => new OpenIddictParameter(value), string[] value => new OpenIddictParameter(value), #if SUPPORTS_JSON_NODES JsonNode value => new OpenIddictParameter(value), #endif _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0115)) }; } return default; } } /// /// Contains the logic responsible for attaching an appropriate HTTP status code. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class AttachHttpResponseCode : IOpenIddictValidationHandler where TContext : BaseRequestContext { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() .SetOrder(100_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(TContext context!!) { Debug.Assert(context.Transaction.Response is not null, SR.GetResourceString(SR.ID4007)); // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, // this may indicate that the request was incorrectly processed by another server stack. var response = context.Transaction.GetHttpRequest()?.HttpContext.Response ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); response.StatusCode = context.Transaction.Response.Error switch { null => 200, Errors.InvalidToken or Errors.MissingToken => 401, Errors.InsufficientAccess or Errors.InsufficientScope => 403, _ => 400 }; return default; } } /// /// Contains the logic responsible for attaching the appropriate HTTP response cache headers. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class AttachCacheControlHeader : IOpenIddictValidationHandler where TContext : BaseRequestContext { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() .SetOrder(AttachHttpResponseCode.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(TContext context!!) { // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, // this may indicate that the request was incorrectly processed by another server stack. var response = context.Transaction.GetHttpRequest()?.HttpContext.Response ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); // Prevent the response from being cached. response.Headers[HeaderNames.CacheControl] = "no-store"; response.Headers[HeaderNames.Pragma] = "no-cache"; response.Headers[HeaderNames.Expires] = "Thu, 01 Jan 1970 00:00:00 GMT"; return default; } } /// /// Contains the logic responsible for attaching errors details to the WWW-Authenticate header. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class AttachWwwAuthenticateHeader : IOpenIddictValidationHandler where TContext : BaseRequestContext { private readonly IOptionsMonitor _options; public AttachWwwAuthenticateHeader(IOptionsMonitor options!!) => _options = options; /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() .SetOrder(AttachCacheControlHeader.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(TContext context!!) { Debug.Assert(context.Transaction.Response is not null, SR.GetResourceString(SR.ID4007)); // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, // this may indicate that the request was incorrectly processed by another server stack. var response = context.Transaction.GetHttpRequest()?.HttpContext.Response ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); var scheme = context.Transaction.Response.Error switch { Errors.InvalidToken or Errors.MissingToken or Errors.InsufficientAccess or Errors.InsufficientScope => Schemes.Bearer, _ => null }; if (string.IsNullOrEmpty(scheme)) { return default; } var parameters = new Dictionary(StringComparer.Ordinal); // If a realm was configured in the options, attach it to the parameters. if (!string.IsNullOrEmpty(_options.CurrentValue.Realm)) { parameters[Parameters.Realm] = _options.CurrentValue.Realm; } foreach (var parameter in context.Transaction.Response.GetParameters()) { // Note: the error details are only included if the error was not caused by a missing token, as recommended // by the OAuth 2.0 bearer specification: https://tools.ietf.org/html/rfc6750#section-3.1. if (string.Equals(context.Transaction.Response.Error, Errors.MissingToken, StringComparison.Ordinal) && (string.Equals(parameter.Key, Parameters.Error, StringComparison.Ordinal) || string.Equals(parameter.Key, Parameters.ErrorDescription, StringComparison.Ordinal) || string.Equals(parameter.Key, Parameters.ErrorUri, StringComparison.Ordinal))) { continue; } // Ignore values that can't be represented as unique strings. var value = (string?) parameter.Value; if (string.IsNullOrEmpty(value)) { continue; } parameters[parameter.Key] = value; } var builder = new StringBuilder(scheme); foreach (var parameter in parameters) { builder.Append(' '); builder.Append(parameter.Key); builder.Append('='); builder.Append('"'); builder.Append(parameter.Value.Replace("\"", "\\\"")); builder.Append('"'); builder.Append(','); } // If the WWW-Authenticate header ends with a comma, remove it. if (builder[builder.Length - 1] == ',') { builder.Remove(builder.Length - 1, 1); } response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString()); return default; } } /// /// Contains the logic responsible for processing challenge responses that contain a WWW-Authenticate header. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class ProcessChallengeErrorResponse : IOpenIddictValidationHandler where TContext : BaseRequestContext { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() .SetOrder(AttachWwwAuthenticateHeader.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(TContext context!!) { // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, // this may indicate that the request was incorrectly processed by another server stack. var response = context.Transaction.GetHttpRequest()?.HttpContext.Response ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); // If the response doesn't contain a WWW-Authenticate header, don't return an empty response. if (!response.Headers.ContainsKey(HeaderNames.WWWAuthenticate)) { return default; } context.Logger.LogInformation(SR.GetResourceString(SR.ID6141), context.Transaction.Response); context.HandleRequest(); return default; } } /// /// Contains the logic responsible for processing OpenID Connect responses that must be returned as JSON. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class ProcessJsonResponse : IOpenIddictValidationHandler where TContext : BaseRequestContext { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() .SetOrder(ProcessChallengeErrorResponse.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); /// public async ValueTask HandleAsync(TContext context!!) { Debug.Assert(context.Transaction.Response is not null, SR.GetResourceString(SR.ID4007)); // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, // this may indicate that the request was incorrectly processed by another server stack. var response = context.Transaction.GetHttpRequest()?.HttpContext.Response ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); context.Logger.LogInformation(SR.GetResourceString(SR.ID6142), context.Transaction.Response); using var stream = new MemoryStream(); using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Indented = true }); context.Transaction.Response.WriteTo(writer); writer.Flush(); response.ContentLength = stream.Length; response.ContentType = "application/json;charset=UTF-8"; stream.Seek(offset: 0, loc: SeekOrigin.Begin); await stream.CopyToAsync(response.Body, 4096, response.HttpContext.RequestAborted); context.HandleRequest(); } } }