/* * 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.Buffers.Binary; using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Security.Claims; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Microsoft.Net.Http.Headers; using OpenIddict.Extensions; using Properties = OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants.Properties; namespace OpenIddict.Client.AspNetCore; [EditorBrowsable(EditorBrowsableState.Never)] public static partial class OpenIddictClientAspNetCoreHandlers { public static ImmutableArray DefaultHandlers { get; } = [ /* * Top-level request processing: */ ResolveRequestUri.Descriptor, ValidateTransportSecurityRequirement.Descriptor, ValidateHostHeader.Descriptor, /* * Authentication processing: */ ValidateAuthenticationType.Descriptor, ValidateAuthenticationNonce.Descriptor, ResolveRequestForgeryProtection.Descriptor, /* * Challenge processing: */ ValidateChallengeType.Descriptor, ResolveHostChallengeProperties.Descriptor, ValidateTransportSecurityRequirementForChallenge.Descriptor, AttachNonDefaultResponseMode.Descriptor, GenerateLoginCorrelationCookie.Descriptor, /* * Sign-out processing: */ ResolveHostSignOutProperties.Descriptor, ValidateTransportSecurityRequirementForSignOut.Descriptor, GenerateLogoutCorrelationCookie.Descriptor, /* * Error processing: */ // Note: these handler registrations are only used as a last resort for errors that are not // returned by an OpenIddict endpoint (e.g errors returned during a failed challenge demand). // // Errors returned by an OpenIddict endpoint are handled via the Apply*Response events. AttachHttpResponseCode.Descriptor, AttachCacheControlHeader.Descriptor, ProcessStatusCodePagesErrorResponse.Descriptor, ProcessLocalErrorResponse.Descriptor, .. Authentication.DefaultHandlers, .. Session.DefaultHandlers ]; /// /// Contains the logic responsible for resolving the request URI from the ASP.NET Core environment. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ResolveRequestUri : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() .SetOrder(int.MinValue + 50_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessRequestContext context) { if (context is null) { throw new ArgumentNullException(nameof(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)); // OpenIddict supports both absolute and relative URIs for all its endpoints, but only absolute // URIs can be properly canonicalized by the BCL System.Uri class (e.g './path/../' is normalized // to './' once the URI is fully constructed). At this stage of the request processing, rejecting // requests that lack the host information (e.g because HTTP/1.0 was used and no Host header was // sent by the HTTP client) is not desirable as it would affect all requests, including requests // that are not meant to be handled by OpenIddict itself. To avoid that, a fake host is temporarily // used to build an absolute base URI and a request URI that will be used to determine whether the // received request matches one of the URIs assigned to an OpenIddict endpoint. If the request // is later handled by OpenIddict, an additional check will be made to require the Host header. (context.BaseUri, context.RequestUri) = request.Host switch { { HasValue: true } host => ( BaseUri: new Uri(request.Scheme + Uri.SchemeDelimiter + host + request.PathBase, UriKind.Absolute), RequestUri: new Uri(request.GetEncodedUrl(), UriKind.Absolute)), { HasValue: false } => ( BaseUri: new UriBuilder { Scheme = request.Scheme, Path = request.PathBase.ToUriComponent() }.Uri, RequestUri: new UriBuilder { Scheme = request.Scheme, Path = (request.PathBase + request.Path).ToUriComponent(), Query = request.QueryString.ToUriComponent() }.Uri) }; return default; } } /// /// Contains the logic responsible for rejecting OpenID Connect requests that don't use transport security. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ValidateTransportSecurityRequirement : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() .UseSingletonHandler() .SetOrder(InferEndpointType.Descriptor.Order + 250) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessRequestContext context) { if (context is null) { throw new ArgumentNullException(nameof(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)); // Don't require that transport security be used if the request is not handled by OpenIddict. if (context.EndpointType is not OpenIddictClientEndpointType.Unknown && !request.IsHttps) { context.Reject( error: Errors.InvalidRequest, description: SR.GetResourceString(SR.ID2083), uri: SR.FormatID8000(SR.ID2083)); return default; } return default; } } /// /// Contains the logic responsible for validating the Host header extracted from the HTTP header. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ValidateHostHeader : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() .SetOrder(ValidateTransportSecurityRequirement.Descriptor.Order + 250) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessRequestContext context) { if (context is null) { throw new ArgumentNullException(nameof(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)); // Don't require that the request host be present if the request is not handled by OpenIddict. if (context.EndpointType is not OpenIddictClientEndpointType.Unknown && !request.Host.HasValue) { context.Reject( error: Errors.InvalidRequest, description: SR.FormatID2081(HeaderNames.Host), uri: SR.FormatID8000(SR.ID2081)); return default; } return default; } } /// /// Contains the logic responsible for extracting OpenID Connect requests from GET or POST HTTP requests. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ExtractGetOrPostRequest : IOpenIddictClientHandler where TContext : BaseValidatingContext { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() .SetOrder(int.MinValue + 100_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public async ValueTask HandleAsync(TContext context) { if (context is null) { throw new ArgumentNullException(nameof(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)); if (HttpMethods.IsGet(request.Method)) { context.Transaction.Request = new OpenIddictRequest(request.Query); } else if (HttpMethods.IsPost(request.Method)) { // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization for more information. if (string.IsNullOrEmpty(request.ContentType)) { context.Logger.LogInformation(SR.GetResourceString(SR.ID6138), HeaderNames.ContentType); context.Reject( error: Errors.InvalidRequest, description: SR.FormatID2081(HeaderNames.ContentType), uri: SR.FormatID8000(SR.ID2081)); return; } // May have media/type; charset=utf-8, allow partial match. if (!request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { context.Logger.LogInformation(SR.GetResourceString(SR.ID6139), HeaderNames.ContentType, request.ContentType); context.Reject( error: Errors.InvalidRequest, description: SR.FormatID2082(HeaderNames.ContentType), uri: SR.FormatID8000(SR.ID2082)); return; } context.Transaction.Request = new OpenIddictRequest(await request.ReadFormAsync(context.CancellationToken)); } else { context.Logger.LogInformation(SR.GetResourceString(SR.ID6137), request.Method); context.Reject( error: Errors.InvalidRequest, description: SR.GetResourceString(SR.ID2084), uri: SR.FormatID8000(SR.ID2084)); return; } } } /// /// Contains the logic responsible for rejecting authentication demands that specify an unsupported type. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ValidateAuthenticationType : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() .SetOrder(ValidateAuthenticationNonce.Descriptor.Order - 500) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessAuthenticationContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } if (context.GrantType is GrantTypes.DeviceCode) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0402)); } return default; } } /// /// Contains the logic responsible for rejecting authentication demands that specify an explicit nonce property. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ValidateAuthenticationNonce : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() .SetOrder(ValidateAuthenticationDemand.Descriptor.Order - 500) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessAuthenticationContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } if (!string.IsNullOrEmpty(context.Nonce)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0377)); } return default; } } /// /// Contains the logic responsible for resolving the request forgery protection from the correlation cookie. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ResolveRequestForgeryProtection : IOpenIddictClientHandler { private readonly IOptionsMonitor _options; public ResolveRequestForgeryProtection(IOptionsMonitor options) => _options = options ?? throw new ArgumentNullException(nameof(options)); /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() .AddFilter() .UseSingletonHandler() .SetOrder(ValidateRequestForgeryProtection.Descriptor.Order - 500) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessAuthenticationContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); // 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 nonce from the state token principal. var nonce = context.StateTokenPrincipal.GetClaim(Claims.Private.Nonce); if (string.IsNullOrEmpty(nonce)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0354)); } // Resolve the cookie builder from the ASP.NET Core integration options. var builder = _options.CurrentValue.CookieBuilder; // Compute the name of the cookie name based on the prefix and the random nonce. var name = new StringBuilder(builder.Name) .Append(Separators.Dot) .Append(nonce) .ToString(); // Try to find the correlation cookie matching the nonce stored in the state. If the cookie // cannot be found, this may indicate that the authorization response is unsolicited and // potentially malicious or be caused by an invalid or unadequate same-site configuration. // // In any case, the authentication demand MUST be rejected as it's impossible to ensure // it's not an injection or session fixation attack without the correlation cookie. var value = request.Cookies[name]; if (string.IsNullOrEmpty(value)) { context.Reject( error: Errors.InvalidRequest, description: SR.GetResourceString(SR.ID2129), uri: SR.FormatID8000(SR.ID2129)); return default; } try { // Extract the payload and validate the version marker. var payload = Base64UrlEncoder.DecodeBytes(value); if (payload.Length < (1 + sizeof(uint)) || payload[0] is not 0x01) { context.Reject( error: Errors.InvalidRequest, description: SR.GetResourceString(SR.ID2163), uri: SR.FormatID8000(SR.ID2163)); return default; } // Extract the length of the request forgery protection. var length = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.AsSpan(1, sizeof(uint))); if (length is 0 || length != (payload.Length - (1 + sizeof(uint)))) { context.Reject( error: Errors.InvalidRequest, description: SR.GetResourceString(SR.ID2163), uri: SR.FormatID8000(SR.ID2163)); return default; } // Note: since the correlation cookie is not protected against tampering, an unexpected // value may be present in the cookie payload and this call may return a string whose // length doesn't match the expected value. In any case, any tampering attempt will be // detected when comparing the resolved value with the expected value stored in the state. context.RequestForgeryProtection = Encoding.UTF8.GetString(payload, index: 5, length); } catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) { context.Reject( error: Errors.InvalidRequest, description: SR.GetResourceString(SR.ID2163), uri: SR.FormatID8000(SR.ID2163)); return default; } // Return a response header asking the browser to delete the state cookie. // // Note: when deleting a cookie, the same options used when creating it MUST be specified. request.HttpContext.Response.Cookies.Delete(name, builder.Build(request.HttpContext)); return default; } } /// /// Contains the logic responsible for rejecting challenge demands that specify an unsupported type. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ValidateChallengeType : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() .SetOrder(ResolveHostChallengeProperties.Descriptor.Order - 500) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessChallengeContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } if (context.GrantType is GrantTypes.DeviceCode) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0402)); } return default; } } /// /// Contains the logic responsible for resolving the context-specific properties and parameters stored in the /// ASP.NET Core authentication properties specified by the application that triggered the challenge operation. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ResolveHostChallengeProperties : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() .SetOrder(ValidateChallengeDemand.Descriptor.Order - 500) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessChallengeContext context) { if (context is null) { throw new ArgumentNullException(nameof(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)); var properties = context.Transaction.GetProperty(typeof(AuthenticationProperties).FullName!); if (properties is { Items.Count: > 0 }) { context.CodeChallengeMethod = GetProperty(properties, Properties.CodeChallengeMethod); context.GrantType = GetProperty(properties, Properties.GrantType); context.IdentityTokenHint = GetProperty(properties, Properties.IdentityTokenHint); context.LoginHint = GetProperty(properties, Properties.LoginHint); context.ProviderName = GetProperty(properties, Properties.ProviderName); context.RegistrationId = GetProperty(properties, Properties.RegistrationId); context.ResponseMode = GetProperty(properties, Properties.ResponseMode); context.ResponseType = GetProperty(properties, Properties.ResponseType); context.TargetLinkUri = properties.RedirectUri switch { // If a return URL - local or not - was explicitly set in the authentication properties, always honor it. { Length: > 0 } uri => uri, // If no return URL was explicitly set in the authentication properties (e.g because // the challenge was triggered automatically by ASP.NET Core or because no return URL // was specified by the user), use the current location as the default target link URI. _ => (request.HttpContext.Features.Get()?.OriginalPathBase ?? request.PathBase) + (request.HttpContext.Features.Get()?.OriginalPath ?? request.Path) + request.QueryString }; if (properties.Items.TryGetValue(Properties.Issuer, out string? issuer) && !string.IsNullOrEmpty(issuer)) { // Ensure the issuer set by the application is a valid absolute URI. if (!Uri.TryCreate(issuer, UriKind.Absolute, out Uri? uri) || OpenIddictHelpers.IsImplicitFileUri(uri)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0306)); } context.Issuer = uri; } if (properties.Items.TryGetValue(Properties.Scope, out string? scope) && !string.IsNullOrEmpty(scope)) { context.Scopes.UnionWith(scope.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)); } foreach (var property in properties.Items) { context.Properties[property.Key] = property.Value; } } if (properties is { Parameters.Count: > 0 }) { foreach (var parameter in properties.Parameters) { context.Parameters[parameter.Key] = parameter.Value switch { OpenIddictParameter value => value, JsonElement value => new OpenIddictParameter(value), JsonNode value => new OpenIddictParameter(value), bool value => new OpenIddictParameter(value), int value => new OpenIddictParameter(value), long value => new OpenIddictParameter(value), string value => new OpenIddictParameter(value), ImmutableArray value => new OpenIddictParameter(value), string?[] value => new(ImmutableCollectionsMarshal.AsImmutableArray(value)), IEnumerable value => new OpenIddictParameter([.. value]), _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0115)) }; } } return default; static string? GetProperty(AuthenticationProperties properties, string name) => properties.Items.TryGetValue(name, out string? value) ? value : null; } } /// /// Contains the logic responsible for preventing challenge operations from being triggered from non-HTTPS endpoints. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ValidateTransportSecurityRequirementForChallenge : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() .UseSingletonHandler() .SetOrder(ValidateChallengeDemand.Descriptor.Order + 500) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessChallengeContext context) { if (context is null) { throw new ArgumentNullException(nameof(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)); if (!request.IsHttps) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0364)); } return default; } } /// /// Contains the logic responsible for attaching a non-default response mode to the challenge request. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class AttachNonDefaultResponseMode : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() .UseSingletonHandler() .SetOrder(AttachResponseMode.Descriptor.Order - 500) .Build(); /// public ValueTask HandleAsync(ProcessChallengeContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } // If an explicit response type was specified, don't overwrite it. if (!string.IsNullOrEmpty(context.ResponseMode)) { return default; } // Note: in most cases, the query response mode will be used as it offers the best compatibility and, // unlike the form_post response mode, is compatible with SameSite=Lax cookies (as it uses GET requests // for the callback stage). However, some specific response_type/response_mode combinations are not // allowed (e.g query can never be used with a type containing id_token or token, as required by the // OAuth 2.0 multiple response types specification). To prevent invalid combinations from being sent to // the remote server, the response types are taken into account when selecting the best response mode. if (context.ResponseType?.Split(Separators.Space) is not IList { Count: > 0 } types) { return default; } context.ResponseMode = ( // Note: if response modes are explicitly listed in the client registration, only use // the response modes that are both listed and enabled in the global client options. // Otherwise, always default to the response modes that have been enabled globally. SupportedClientResponseModes: context.Registration.ResponseModes.Count switch { 0 => context.Options.ResponseModes as ICollection, _ => context.Options.ResponseModes.Intersect(context.Registration.ResponseModes, StringComparer.Ordinal).ToList() }, SupportedServerResponseModes: context.Configuration.ResponseModesSupported) switch { // If both the client and the server support response_mode=form_post, use it if the response // types contain a value that prevents response_mode=query from being used (token/id_token). ({ Count: > 0 } client, { Count: > 0 } server) when client.Contains(ResponseModes.FormPost) && server.Contains(ResponseModes.FormPost) && (types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token)) => ResponseModes.FormPost, // If the client supports response_mode=form_post and the server doesn't specify a list // of response modes, assume it is supported and use it if the response types contain // a value that prevents response_mode=query from being used (token/id_token). ({ Count: > 0 } client, { Count: 0 }) when client.Contains(ResponseModes.FormPost) && (types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token)) => ResponseModes.FormPost, // If both the client and the server support response_mode=query, use it. ({ Count: > 0 } client, { Count: > 0 } server) when client.Contains(ResponseModes.Query) && server.Contains(ResponseModes.Query) => ResponseModes.Query, // If the client supports response_mode=query and the server doesn't // specify a list of response modes, assume it is supported. ({ Count: > 0 } client, { Count: 0 }) when client.Contains(ResponseModes.Query) => ResponseModes.Query, // If both the client and the server support response_mode=form_post, use it. ({ Count: > 0 } client, { Count: > 0 } server) when client.Contains(ResponseModes.FormPost) && server.Contains(ResponseModes.FormPost) => ResponseModes.FormPost, // Assign a null value to allow the generic handler present in // the base client package to negotiate other response modes. _ => null }; return default; } } /// /// Contains the logic responsible for creating a correlation cookie that serves as a /// protection against state token injection, forged requests and session fixation attacks. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class GenerateLoginCorrelationCookie : IOpenIddictClientHandler { private readonly IOptionsMonitor _options; public GenerateLoginCorrelationCookie(IOptionsMonitor options) => _options = options ?? throw new ArgumentNullException(nameof(options)); /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() .AddFilter() .UseSingletonHandler() .SetOrder(AttachChallengeParameters.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessChallengeContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } // Note: using a correlation cookie serves as an injection/antiforgery protection as the request // will always be rejected if a cookie corresponding to the request forgery protection claim // persisted in the state token cannot be found. This protection is considered essential // in OpenIddict and cannot be disabled via the options. Applications that prefer implementing // a different protection strategy can remove this handler from the handlers list and add // a custom one using a different approach (e.g by storing the value in the session state). if (string.IsNullOrEmpty(context.Nonce)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0352)); } if (string.IsNullOrEmpty(context.RequestForgeryProtection)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0343)); } Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); // 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)); // Resolve the cookie builder from the ASP.NET Core integration options. var builder = _options.CurrentValue.CookieBuilder; // Unless a value was explicitly set in the options, use the expiration date // of the state token principal as the expiration date of the correlation cookie. var options = builder.Build(response.HttpContext); options.Expires ??= context.StateTokenPrincipal.GetExpirationDate(); // Compute a collision-resistant and hard-to-guess cookie name using the nonce. var name = new StringBuilder(builder.Name) .Append(Separators.Dot) .Append(context.Nonce) .ToString(); // Create the cookie payload containing... var count = Encoding.UTF8.GetByteCount(context.RequestForgeryProtection); var payload = new byte[1 + sizeof(uint) + count]; // ... the version marker identifying the binary format used to create the payload (1 byte). payload[0] = 0x01; // ... the length of the request forgery protection (4 bytes). BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, sizeof(uint)), (uint) count); // ... the request forgery protection (variable length). var written = Encoding.UTF8.GetBytes(s: context.RequestForgeryProtection, charIndex: 0, charCount: context.RequestForgeryProtection.Length, bytes: payload, byteIndex: 5); Debug.Assert(written == count, SR.FormatID4016(written, count)); // Add the correlation cookie to the response headers. response.Cookies.Append(name, Base64UrlEncoder.Encode(payload), options); return default; } } /// /// Contains the logic responsible for resolving the context-specific properties and parameters stored in the /// ASP.NET Core authentication properties specified by the application that triggered the sign-out operation. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ResolveHostSignOutProperties : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() .SetOrder(ValidateSignOutDemand.Descriptor.Order - 500) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessSignOutContext context) { if (context is null) { throw new ArgumentNullException(nameof(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)); var properties = context.Transaction.GetProperty(typeof(AuthenticationProperties).FullName!); if (properties is { Items.Count: > 0 }) { context.IdentityTokenHint = GetProperty(properties, Properties.IdentityTokenHint); context.LoginHint = GetProperty(properties, Properties.LoginHint); context.ProviderName = GetProperty(properties, Properties.ProviderName); context.RegistrationId = GetProperty(properties, Properties.RegistrationId); context.TargetLinkUri = properties.RedirectUri switch { // If a return URL - local or not - was explicitly set in the authentication properties, always honor it. { Length: > 0 } uri => uri, // If no return URL was explicitly set in the authentication properties (e.g because // the challenge was triggered automatically by ASP.NET Core or because no return URL // was specified by the user), use the current location as the default target link URI. _ => (request.HttpContext.Features.Get()?.OriginalPathBase ?? request.PathBase) + (request.HttpContext.Features.Get()?.OriginalPath ?? request.Path) + request.QueryString }; if (properties.Items.TryGetValue(Properties.Issuer, out string? issuer) && !string.IsNullOrEmpty(issuer)) { // Ensure the issuer set by the application is a valid absolute URI. if (!Uri.TryCreate(issuer, UriKind.Absolute, out Uri? uri) || OpenIddictHelpers.IsImplicitFileUri(uri)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0306)); } context.Issuer = uri; } foreach (var property in properties.Items) { context.Properties[property.Key] = property.Value; } } if (properties is { Parameters.Count: > 0 }) { foreach (var parameter in properties.Parameters) { context.Parameters[parameter.Key] = parameter.Value switch { OpenIddictParameter value => value, JsonElement value => new OpenIddictParameter(value), JsonNode value => new OpenIddictParameter(value), bool value => new OpenIddictParameter(value), int value => new OpenIddictParameter(value), long value => new OpenIddictParameter(value), string value => new OpenIddictParameter(value), ImmutableArray value => new OpenIddictParameter(value), string?[] value => new(ImmutableCollectionsMarshal.AsImmutableArray(value)), IEnumerable value => new OpenIddictParameter([.. value]), _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0115)) }; } } return default; static string? GetProperty(AuthenticationProperties properties, string name) => properties.Items.TryGetValue(name, out string? value) ? value : null; } } /// /// Contains the logic responsible for preventing sign-out operations from being triggered from non-HTTPS endpoints. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ValidateTransportSecurityRequirementForSignOut : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() .UseSingletonHandler() .SetOrder(ValidateSignOutDemand.Descriptor.Order + 500) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessSignOutContext context) { if (context is null) { throw new ArgumentNullException(nameof(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)); if (!request.IsHttps) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0365)); } return default; } } /// /// Contains the logic responsible for creating a correlation cookie that serves as a /// protection against state token injection, forged requests and denial of service attacks. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class GenerateLogoutCorrelationCookie : IOpenIddictClientHandler { private readonly IOptionsMonitor _options; public GenerateLogoutCorrelationCookie(IOptionsMonitor options) => _options = options ?? throw new ArgumentNullException(nameof(options)); /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() .UseSingletonHandler() .SetOrder(AttachChallengeParameters.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessSignOutContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } // Note: using a correlation cookie serves as an injection/antiforgery protection as the request // will always be rejected if a cookie corresponding to the request forgery protection claim // persisted in the state token cannot be found. This protection is considered essential // in OpenIddict and cannot be disabled via the options. Applications that prefer implementing // a different protection strategy can remove this handler from the handlers list and add // a custom one using a different approach (e.g by storing the value in the session state). if (string.IsNullOrEmpty(context.Nonce)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0352)); } if (string.IsNullOrEmpty(context.RequestForgeryProtection)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0343)); } Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); // 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)); // Resolve the cookie builder from the ASP.NET Core integration options. var builder = _options.CurrentValue.CookieBuilder; // Unless a value was explicitly set in the options, use the expiration date // of the state token principal as the expiration date of the correlation cookie. var options = builder.Build(response.HttpContext); options.Expires ??= context.StateTokenPrincipal.GetExpirationDate(); // Compute a collision-resistant and hard-to-guess cookie name using the nonce. var name = new StringBuilder(builder.Name) .Append(Separators.Dot) .Append(context.Nonce) .ToString(); // Create the cookie payload containing... var count = Encoding.UTF8.GetByteCount(context.RequestForgeryProtection); var payload = new byte[1 + sizeof(uint) + count]; // ... the version marker identifying the binary format used to create the payload (1 byte). payload[0] = 0x01; // ... the length of the request forgery protection (4 bytes). BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, sizeof(uint)), (uint) count); // ... the request forgery protection (variable length). var written = Encoding.UTF8.GetBytes(s: context.RequestForgeryProtection, charIndex: 0, charCount: context.RequestForgeryProtection.Length, bytes: payload, byteIndex: 5); Debug.Assert(written == count, SR.FormatID4016(written, count)); // Add the correlation cookie to the response headers. response.Cookies.Append(name, Base64UrlEncoder.Encode(payload), options); return default; } } /// /// Contains the logic responsible for enabling the pass-through mode for the received request. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class EnablePassthroughMode : IOpenIddictClientHandler where TContext : BaseRequestContext where TFilter : IOpenIddictClientHandlerFilter { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() .UseSingletonHandler>() .SetOrder(int.MaxValue - 100_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(TContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } context.SkipRequest(); 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 sealed class AttachHttpResponseCode : IOpenIddictClientHandler where TContext : BaseRequestContext { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() .SetOrder(100_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(TContext context) { if (context is null) { throw new ArgumentNullException(nameof(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)); Debug.Assert(context.Transaction.Response is not null, SR.GetResourceString(SR.ID4007)); response.StatusCode = context.Transaction.Response.Error switch { null => 200, // Note: the default code may be replaced by another handler (e.g when doing redirects). _ => 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 sealed class AttachCacheControlHeader : IOpenIddictClientHandler where TContext : BaseRequestContext { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() .SetOrder(AttachHttpResponseCode.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(TContext context) { if (context is null) { throw new ArgumentNullException(nameof(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 processing OpenID Connect responses that must be handled by another /// middleware in the pipeline at a later stage (e.g an ASP.NET Core MVC action or a NancyFX module). /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ProcessPassthroughErrorResponse : IOpenIddictClientHandler where TContext : BaseRequestContext where TFilter : IOpenIddictClientHandlerFilter { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() .AddFilter() .UseSingletonHandler>() .SetOrder(500_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(TContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } Debug.Assert(context.Transaction.Response is not null, SR.GetResourceString(SR.ID4007)); if (string.IsNullOrEmpty(context.Transaction.Response.Error)) { return default; } context.SkipRequest(); return default; } } /// /// Contains the logic responsible for processing OpenID Connect responses handled by the status code pages middleware. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ProcessStatusCodePagesErrorResponse : IOpenIddictClientHandler where TContext : BaseRequestContext { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() .UseSingletonHandler>() .SetOrder(ProcessPassthroughErrorResponse>.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(TContext context) { if (context is null) { throw new ArgumentNullException(nameof(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)); Debug.Assert(context.Transaction.Response is not null, SR.GetResourceString(SR.ID4007)); if (string.IsNullOrEmpty(context.Transaction.Response.Error)) { return default; } // Determine if the status code pages middleware has been enabled for this request. // If it was not registered or enabled, let the default OpenIddict client handlers render // a default error page instead of delegating the rendering to the status code middleware. var feature = response.HttpContext.Features.Get(); if (feature is not { Enabled: true }) { return default; } // Mark the request as fully handled to prevent the other OpenIddict client handlers // from displaying the default error page and to allow the status code pages middleware // to rewrite the response using the logic defined by the developer when registering it. context.HandleRequest(); return default; } } /// /// Contains the logic responsible for processing context responses that must be returned as plain-text. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ProcessLocalErrorResponse : IOpenIddictClientHandler where TContext : BaseRequestContext { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() .SetOrder(ProcessStatusCodePagesErrorResponse.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public async ValueTask HandleAsync(TContext context) { if (context is null) { throw new ArgumentNullException(nameof(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)); Debug.Assert(context.Transaction.Response is not null, SR.GetResourceString(SR.ID4007)); if (string.IsNullOrEmpty(context.Transaction.Response.Error)) { return; } // Don't return the state originally sent by the client application. context.Transaction.Response.State = null; context.Logger.LogInformation(SR.GetResourceString(SR.ID6143), context.Transaction.Response); using var stream = new MemoryStream(); using var writer = new StreamWriter(stream); foreach (var parameter in context.Transaction.Response.GetParameters()) { // Ignore null or empty parameters, including JSON // objects that can't be represented as strings. var value = (string?) parameter.Value; if (string.IsNullOrEmpty(value)) { continue; } writer.Write(parameter.Key); writer.Write(':'); writer.Write(value); writer.WriteLine(); } writer.Flush(); response.ContentLength = stream.Length; response.ContentType = "text/plain;charset=UTF-8"; stream.Seek(offset: 0, loc: SeekOrigin.Begin); await stream.CopyToAsync(response.Body, 4096, context.CancellationToken); context.HandleRequest(); } } /// /// Contains the logic responsible for processing OpenID Connect responses that don't specify any parameter. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ProcessEmptyResponse : IOpenIddictClientHandler where TContext : BaseRequestContext { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() .SetOrder(int.MaxValue - 100_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(TContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } context.Logger.LogInformation(SR.GetResourceString(SR.ID6145)); context.HandleRequest(); return default; } } }