Versatile OpenID Connect stack for ASP.NET Core and Microsoft.Owin (compatible with ASP.NET 4.6.1)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

817 lines
38 KiB

/*
* 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<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Top-level request processing:
*/
InferEndpointType.Descriptor,
/*
* Authentication processing:
*/
ValidateCorrelationCookie.Descriptor,
/*
* Challenge processing:
*/
GenerateCorrelationCookie.Descriptor,
ResolveHostChallengeParameters.Descriptor)
.AddRange(Authentication.DefaultHandlers);
/// <summary>
/// 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.
/// </summary>
public class InferEndpointType : IOpenIddictClientHandler<ProcessRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<InferEndpointType>()
.SetOrder(int.MinValue + 50_000)
.Build();
/// <inheritdoc/>
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<Uri> 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);
}
}
}
/// <summary>
/// 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.
/// </summary>
public class ExtractGetRequest<TContext> : IOpenIddictClientHandler<TContext> where TContext : BaseValidatingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ExtractGetRequest<TContext>>()
.SetOrder(int.MinValue + 100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ExtractGetOrPostRequest<TContext> : IOpenIddictClientHandler<TContext> where TContext : BaseValidatingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ExtractGetOrPostRequest<TContext>>()
.SetOrder(ExtractGetRequest<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
}
/// <summary>
/// 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.
/// </summary>
public class ExtractPostRequest<TContext> : IOpenIddictClientHandler<TContext> where TContext : BaseValidatingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ExtractPostRequest<TContext>>()
.SetOrder(ExtractGetOrPostRequest<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
}
/// <summary>
/// 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.
/// </summary>
public class ValidateCorrelationCookie : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
private readonly IOptionsMonitor<OpenIddictClientAspNetCoreOptions> _options;
public ValidateCorrelationCookie(IOptionsMonitor<OpenIddictClientAspNetCoreOptions> options!!)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateCorrelationCookie>()
.SetOrder(ValidateStateToken.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ResolveHostChallengeParameters : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ResolveHostChallengeParameters>()
.SetOrder(ValidateChallengeDemand.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context!!)
{
var properties = context.Transaction.GetProperty<AuthenticationProperties>(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;
}
}
/// <summary>
/// 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.
/// </summary>
public class GenerateCorrelationCookie : IOpenIddictClientHandler<ProcessChallengeContext>
{
private readonly IOptionsMonitor<OpenIddictClientAspNetCoreOptions> _options;
public GenerateCorrelationCookie(IOptionsMonitor<OpenIddictClientAspNetCoreOptions> options!!)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireStateTokenGenerated>()
.UseSingletonHandler<GenerateCorrelationCookie>()
.SetOrder(AttachChallengeParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class EnablePassthroughMode<TContext, TFilter> : IOpenIddictClientHandler<TContext>
where TContext : BaseRequestContext
where TFilter : IOpenIddictClientHandlerFilter<TContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<TFilter>()
.UseSingletonHandler<EnablePassthroughMode<TContext, TFilter>>()
.SetOrder(int.MaxValue - 100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(TContext context!!)
{
context.SkipRequest();
return default;
}
}
/// <summary>
/// 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.
/// </summary>
public class AttachHttpResponseCode<TContext> : IOpenIddictClientHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<AttachHttpResponseCode<TContext>>()
.SetOrder(100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class AttachCacheControlHeader<TContext> : IOpenIddictClientHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<AttachCacheControlHeader<TContext>>()
.SetOrder(AttachHttpResponseCode<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessPassthroughErrorResponse<TContext, TFilter> : IOpenIddictClientHandler<TContext>
where TContext : BaseRequestContext
where TFilter : IOpenIddictClientHandlerFilter<TContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireErrorPassthroughEnabled>()
.AddFilter<TFilter>()
.UseSingletonHandler<ProcessPassthroughErrorResponse<TContext, TFilter>>()
.SetOrder(100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessStatusCodePagesErrorResponse<TContext> : IOpenIddictClientHandler<TContext>
where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireStatusCodePagesIntegrationEnabled>()
.UseSingletonHandler<ProcessStatusCodePagesErrorResponse<TContext>>()
.SetOrder(ProcessPassthroughErrorResponse<TContext, IOpenIddictClientHandlerFilter<TContext>>.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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<IStatusCodePagesFeature>();
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessLocalErrorResponse<TContext> : IOpenIddictClientHandler<TContext>
where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessLocalErrorResponse<TContext>>()
.SetOrder(ProcessStatusCodePagesErrorResponse<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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();
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessEmptyResponse<TContext> : IOpenIddictClientHandler<TContext>
where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessEmptyResponse<TContext>>()
.SetOrder(int.MaxValue - 100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(TContext context!!)
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6145));
context.HandleRequest();
return default;
}
}
}