/* * 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.Text; using System.Text.Encodings.Web; using Microsoft.AspNetCore; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; namespace OpenIddict.Server.AspNetCore; public static partial class OpenIddictServerAspNetCoreHandlers { public static class Authentication { public static ImmutableArray DefaultHandlers { get; } = [ /* * Authorization request extraction: */ ExtractGetOrPostRequest.Descriptor, /* * Authorization request handling: */ EnablePassthroughMode.Descriptor, /* * Authorization response processing: */ AttachHttpResponseCode.Descriptor, AttachCacheControlHeader.Descriptor, ProcessSelfRedirection.Descriptor, ProcessFormPostResponse.Descriptor, ProcessQueryResponse.Descriptor, ProcessFragmentResponse.Descriptor, ProcessPassthroughErrorResponse.Descriptor, ProcessStatusCodePagesErrorResponse.Descriptor, ProcessLocalErrorResponse.Descriptor, /* * Pushed authorization request extraction: */ ExtractPostRequest.Descriptor, ValidateClientAuthenticationMethod.Descriptor, ExtractClientCertificate.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* * Pushed authorization response processing: */ AttachHttpResponseCode.Descriptor, AttachCacheControlHeader.Descriptor, AttachWwwAuthenticateHeader.Descriptor, ProcessJsonResponse.Descriptor ]; /// /// Contains the logic responsible for processing authorization responses requiring a self-redirection. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ProcessSelfRedirection : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() .SetOrder(250_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ApplyAuthorizationResponseContext context) { ArgumentNullException.ThrowIfNull(context); if (context is not { BaseUri.IsAbsoluteUri: true, RequestUri.IsAbsoluteUri: true }) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0127)); } if (string.IsNullOrEmpty(context.Response.RequestUri)) { return ValueTask.CompletedTask; } // 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 location = QueryHelpers.AddQueryString(context.RequestUri.GetLeftPart(UriPartial.Path), from parameter in context.Response.GetParameters() let values = (ImmutableArray?) parameter.Value where values is not null from value in values.GetValueOrDefault() where !string.IsNullOrEmpty(value) select KeyValuePair.Create(parameter.Key, value)); response.Redirect(location); context.HandleRequest(); return ValueTask.CompletedTask; } } /// /// Contains the logic responsible for processing authorization responses using the form_post response mode. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ProcessFormPostResponse : IOpenIddictServerHandler { private readonly HtmlEncoder _encoder; public ProcessFormPostResponse(HtmlEncoder encoder) => _encoder = encoder; /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() .SetOrder(ProcessSelfRedirection.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public async ValueTask HandleAsync(ApplyAuthorizationResponseContext context) { ArgumentNullException.ThrowIfNull(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 (string.IsNullOrEmpty(context.RedirectUri) || !string.Equals(context.ResponseMode, ResponseModes.FormPost, StringComparison.Ordinal)) { return; } context.Logger.LogInformation(6147, SR.GetResourceString(SR.ID6147), context.RedirectUri, context.Response); using var buffer = new MemoryStream(); using var writer = new StreamWriter(buffer); writer.WriteLine(""); writer.WriteLine(""); writer.WriteLine(""); // While the redirect_uri parameter should be guarded against unknown values, // it's still safer to encode it to avoid cross-site scripting attacks // if the authorization server has a relaxed policy concerning redirect URIs. writer.WriteLine($@"
"); // Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters // with the same name are used by derived drafts like the OAuth 2.0 token exchange specification. // For consistency, multiple parameters with the same name are also supported by this endpoint. foreach (var (key, value) in from parameter in context.Response.GetParameters() let values = (ImmutableArray?) parameter.Value where values is not null from value in values.GetValueOrDefault() where !string.IsNullOrEmpty(value) select (parameter.Key, Value: value)) { writer.WriteLine($@""); } writer.WriteLine(@""); writer.WriteLine(""); writer.WriteLine(""); writer.WriteLine(""); writer.WriteLine(""); writer.Flush(); response.StatusCode = 200; response.ContentLength = buffer.Length; response.ContentType = "text/html;charset=UTF-8"; response.Headers[HeaderNames.CacheControl] = "no-store"; response.Headers[HeaderNames.Pragma] = "no-cache"; response.Headers[HeaderNames.Expires] = "-1"; buffer.Seek(offset: 0, loc: SeekOrigin.Begin); await buffer.CopyToAsync(response.Body, 4096); context.HandleRequest(); } } /// /// Contains the logic responsible for processing authorization responses using the query response mode. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ProcessQueryResponse : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() .SetOrder(ProcessFormPostResponse.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ApplyAuthorizationResponseContext context) { ArgumentNullException.ThrowIfNull(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 (string.IsNullOrEmpty(context.RedirectUri) || !string.Equals(context.ResponseMode, ResponseModes.Query, StringComparison.Ordinal)) { return ValueTask.CompletedTask; } context.Logger.LogInformation(6148, SR.GetResourceString(SR.ID6148), context.RedirectUri, context.Response); // Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters // with the same name are used by derived drafts like the OAuth 2.0 token exchange specification. // For consistency, multiple parameters with the same name are also supported by this endpoint. var location = QueryHelpers.AddQueryString(context.RedirectUri, from parameter in context.Response.GetParameters() let values = (ImmutableArray?) parameter.Value where values is not null from value in values.GetValueOrDefault() where !string.IsNullOrEmpty(value) select KeyValuePair.Create(parameter.Key, value)); response.Redirect(location); context.HandleRequest(); return ValueTask.CompletedTask; } } /// /// Contains the logic responsible for processing authorization responses using the fragment response mode. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public sealed class ProcessFragmentResponse : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() .SetOrder(ProcessQueryResponse.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ApplyAuthorizationResponseContext context) { ArgumentNullException.ThrowIfNull(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 (string.IsNullOrEmpty(context.RedirectUri) || !string.Equals(context.ResponseMode, ResponseModes.Fragment, StringComparison.Ordinal)) { return ValueTask.CompletedTask; } context.Logger.LogInformation(6149, SR.GetResourceString(SR.ID6149), context.RedirectUri, context.Response); var builder = new StringBuilder(context.RedirectUri); // Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters // with the same name are used by derived drafts like the OAuth 2.0 token exchange specification. // For consistency, multiple parameters with the same name are also supported by this endpoint. foreach (var (key, value) in from parameter in context.Response.GetParameters() let values = (ImmutableArray?) parameter.Value where values is not null from value in values.GetValueOrDefault() where !string.IsNullOrEmpty(value) select (parameter.Key, Value: value)) { builder.Append(Contains(builder, '#') ? '&' : '#') .Append(Uri.EscapeDataString(key)) .Append('=') .Append(Uri.EscapeDataString(value)); } response.Redirect(builder.ToString()); context.HandleRequest(); return ValueTask.CompletedTask; static bool Contains(StringBuilder builder, char delimiter) { for (var index = 0; index < builder.Length; index++) { if (builder[index] == delimiter) { return true; } } return false; } } } } }