/* * 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.Security.Claims; using System.Text; using System.Text.Json; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using Properties = OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants.Properties; namespace OpenIddict.Client.AspNetCore; [EditorBrowsable(EditorBrowsableState.Never)] public static partial class OpenIddictClientAspNetCoreHandlers { public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( /* * Top-level request processing: */ InferEndpointType.Descriptor, /* * Authentication processing: */ ValidateCorrelationCookie.Descriptor, /* * Challenge processing: */ GenerateCorrelationCookie.Descriptor, ResolveHostChallengeParameters.Descriptor) .AddRange(Authentication.DefaultHandlers); /// /// Contains the logic responsible of inferring the endpoint type from the request address. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class InferEndpointType : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() .SetOrder(int.MinValue + 50_000) .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)); context.EndpointType = Matches(request, context.Options.RedirectionEndpointUris) ? OpenIddictClientEndpointType.Redirection : OpenIddictClientEndpointType.Unknown; return default; static bool Matches(HttpRequest request, IReadOnlyList addresses) { for (var index = 0; index < addresses.Count; index++) { var address = addresses[index]; if (address.IsAbsoluteUri) { // If the request host is not available (e.g because HTTP/1.0 was used), ignore absolute URLs. if (!request.Host.HasValue) { continue; } // Create a Uri instance using the request scheme and raw host and compare the two base addresses. if (!Uri.TryCreate(request.Scheme + Uri.SchemeDelimiter + request.Host, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString() || uri.Port != address.Port || !string.Equals(uri.Scheme, address.Scheme, StringComparison.OrdinalIgnoreCase) || !string.Equals(uri.Host, address.Host, StringComparison.OrdinalIgnoreCase)) { continue; } var path = PathString.FromUriComponent(address); if (AreEquivalent(path, request.PathBase + request.Path)) { return true; } } else if (address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase)) { var path = new PathString(address.OriginalString); if (AreEquivalent(path, request.Path)) { return true; } } } return false; // ASP.NET Core's routing system ignores trailing slashes when determining // whether the request path matches a registered route, which is not the case // with PathString, that treats /connect/token and /connect/token/ as different // addresses. To mitigate this inconsistency, a manual check is used here. static bool AreEquivalent(PathString left, PathString right) => left.Equals(right, StringComparison.OrdinalIgnoreCase) || left.Equals(right + "/", StringComparison.OrdinalIgnoreCase) || right.Equals(left + "/", StringComparison.OrdinalIgnoreCase); } } } /// /// Contains the logic responsible of extracting OpenID Connect requests from GET HTTP requests. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class ExtractGetRequest : 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 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 request = context.Transaction.GetHttpRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); if (HttpMethods.IsGet(request.Method)) { context.Transaction.Request = new OpenIddictRequest(request.Query); } 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 default; } return default; } } /// /// Contains the logic responsible of 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 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(ExtractGetRequest.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public async 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 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 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(request.HttpContext.RequestAborted)); } 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 of extracting OpenID Connect requests from POST HTTP requests. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class ExtractPostRequest : IOpenIddictClientHandler where TContext : BaseValidatingContext { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() .SetOrder(ExtractGetOrPostRequest.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public async 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 request = context.Transaction.GetHttpRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); if (HttpMethods.IsPost(request.Method)) { // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization 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(request.HttpContext.RequestAborted)); } 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 of validating the correlation cookie that serves as a CSRF protection. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class ValidateCorrelationCookie : IOpenIddictClientHandler { private readonly IOptionsMonitor _options; public ValidateCorrelationCookie(IOptionsMonitor options!!) => _options = options; /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() .UseSingletonHandler() .SetOrder(ValidateStateToken.Descriptor.Order + 500) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ProcessAuthenticationContext 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 request forgery protection from the state token principal. // If the claim cannot be found, this means the protection was disabled // using a custom event handler. In this case, bypass the validation. var claim = context.StateTokenPrincipal.GetClaim(Claims.RequestForgeryProtection); if (string.IsNullOrEmpty(claim)) { return default; } // Compute the name of the cookie name based on the prefix set in the options // and the random request forgery protection claim restored from the state. var name = new StringBuilder(_options.CurrentValue.CookieBuilder.Name) .Append(Separators.Dot) .Append(claim) .ToString(); // Try to find the cookie matching the request forgery protection stored in the state. // // If the cookie cannot be found, this may indicate that the authorization response // is unsolicited and potentially malicious. This may also be caused by an unadequate // same-site configuration. In any case, the authentication demand MUST be rejected // as it's impossible to ensure it's not a session fixation attack without the cookie. var value = request.Cookies[name]; if (string.IsNullOrEmpty(value) || !string.Equals(value, "v1", StringComparison.Ordinal)) { context.Reject( error: Errors.InvalidRequest, description: SR.GetResourceString(SR.ID2129), uri: SR.FormatID8000(SR.ID2129)); return default; } // Note: when deleting a cookie, the same options used when creating it MUST be specified. var options = _options.CurrentValue.CookieBuilder.Build(request.HttpContext); // Return a response header asking the browser to delete the state cookie. request.HttpContext.Response.Cookies.Delete(name, options); return default; } } /// /// Contains the logic responsible of resolving the additional challenge 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 class ResolveHostChallengeParameters : 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!!) { var properties = context.Transaction.GetProperty(typeof(AuthenticationProperties).FullName!); if (properties is null) { return default; } // If an issuer was explicitly set, update the challenge context to use it. 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) || !uri.IsWellFormedOriginalString()) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0306)); } context.Issuer = uri; } // If a return URL was specified, use it as the target_link_uri claim. if (!string.IsNullOrEmpty(properties.RedirectUri)) { context.TargetLinkUri = properties.RedirectUri; } 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), _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0115)) }; } return default; } } /// /// Contains the logic responsible of creating a correlation cookie that serves as a CSRF countermeasure. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class GenerateCorrelationCookie : IOpenIddictClientHandler { private readonly IOptionsMonitor _options; public GenerateCorrelationCookie(IOptionsMonitor options!!) => _options = 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(ProcessChallengeContext context!!) { // Note: using a correlation cookie serves as an 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 set the request forgery protection claim to null or // remove this handler from the handlers list and add a custom one using a different approach. if (string.IsNullOrEmpty(context.RequestForgeryProtection)) { return default; } Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); Debug.Assert(!string.IsNullOrEmpty(context.StateToken), SR.GetResourceString(SR.ID4010)); // 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 options = _options.CurrentValue.CookieBuilder.Build(response.HttpContext); // 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. options.Expires ??= context.StateTokenPrincipal.GetExpirationDate(); // Compute a collision-resistant and hard-to-guess cookie name based on the prefix set // in the options and the random request forgery protection claim generated earlier. var name = new StringBuilder(_options.CurrentValue.CookieBuilder.Name) .Append(Separators.Dot) .Append(context.RequestForgeryProtection) .ToString(); // Add the correlation cookie to the response headers. response.Cookies.Append(name, "v1", options); return default; } } /// /// Contains the logic responsible of 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 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!!) { context.SkipRequest(); return default; } } /// /// Contains the logic responsible of 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 : 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!!) { // 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 of 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 : 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!!) { // 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 of 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 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(100_000) .SetType(OpenIddictClientHandlerType.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)); 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 of 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 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!!) { // 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 null || !feature.Enabled) { 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 of 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 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!!) { // 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, response.HttpContext.RequestAborted); context.HandleRequest(); } } /// /// Contains the logic responsible of 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 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!!) { context.Logger.LogInformation(SR.GetResourceString(SR.ID6145)); context.HandleRequest(); return default; } } }