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.
 
 
 
 
 
 

469 lines
22 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.Security.Claims;
using System.Text.Json;
using Microsoft.IdentityModel.JsonWebTokens;
namespace OpenIddict.Validation;
public static partial class OpenIddictValidationHandlers
{
public static class Introspection
{
public static ImmutableArray<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Introspection response handling:
*/
AttachCredentials.Descriptor,
AttachToken.Descriptor,
/*
* Introspection response handling:
*/
HandleErrorResponse<HandleIntrospectionResponseContext>.Descriptor,
ValidateWellKnownParameters.Descriptor,
HandleInactiveResponse.Descriptor,
ValidateIssuer.Descriptor,
ValidateTokenUsage.Descriptor,
PopulateClaims.Descriptor);
/// <summary>
/// Contains the logic responsible for attaching the client credentials to the introspection request.
/// </summary>
public class AttachCredentials : IOpenIddictValidationHandler<PrepareIntrospectionRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<PrepareIntrospectionRequestContext>()
.UseSingletonHandler<AttachCredentials>()
.SetOrder(int.MinValue + 100_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(PrepareIntrospectionRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.Request.ClientId = context.Options.ClientId;
context.Request.ClientSecret = context.Options.ClientSecret;
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching the token to the introspection request.
/// </summary>
public class AttachToken : IOpenIddictValidationHandler<PrepareIntrospectionRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<PrepareIntrospectionRequestContext>()
.UseSingletonHandler<AttachToken>()
.SetOrder(AttachCredentials.Descriptor.Order + 100_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(PrepareIntrospectionRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.Request.Token = context.Token;
context.Request.TokenTypeHint = context.TokenTypeHint;
return default;
}
}
/// <summary>
/// Contains the logic responsible for validating the well-known parameters contained in the introspection response.
/// </summary>
public class ValidateWellKnownParameters : IOpenIddictValidationHandler<HandleIntrospectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<ValidateWellKnownParameters>()
.SetOrder(HandleErrorResponse<HandleIntrospectionResponseContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleIntrospectionResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
foreach (var parameter in context.Response.GetParameters())
{
if (!ValidateParameterType(parameter.Key, parameter.Value))
{
context.Reject(
error: Errors.ServerError,
description: SR.FormatID2107(parameter.Key),
uri: SR.FormatID8000(SR.ID2107));
return default;
}
}
return default;
// Note: in the typical case, the response parameters should be deserialized from a
// JSON response and thus natively stored as System.Text.Json.JsonElement instances.
//
// In the rare cases where the underlying value wouldn't be a JsonElement instance
// (e.g when custom parameters are manually added to the response), the static
// conversion operator would take care of converting the underlying value to a
// JsonElement instance using the same value type as the original parameter value.
static bool ValidateParameterType(string name, OpenIddictParameter value) => name switch
{
// The following parameters MUST be formatted as booleans:
Claims.Active => ((JsonElement) value).ValueKind is JsonValueKind.True or JsonValueKind.False,
// The following parameters MUST be formatted as unique strings:
Claims.JwtId or Claims.Issuer or Claims.Scope or Claims.TokenUsage
=> ((JsonElement) value).ValueKind is JsonValueKind.String,
// The following parameters MUST be formatted as strings or arrays of strings:
//
// Note: empty arrays and arrays that contain a single value are also considered valid.
Claims.Audience => ((JsonElement) value) is JsonElement element &&
element.ValueKind is JsonValueKind.String ||
(element.ValueKind is JsonValueKind.Array && ValidateStringArray(element)),
// The following parameters MUST be formatted as numeric dates:
Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore
=> ((JsonElement) value).ValueKind is JsonValueKind.Number,
// Parameters that are not in the well-known list can be of any type.
_ => true
};
static bool ValidateStringArray(JsonElement element)
{
foreach (var item in element.EnumerateArray())
{
if (item.ValueKind is not JsonValueKind.String)
{
return false;
}
}
return true;
}
}
}
/// <summary>
/// Contains the logic responsible for extracting the active: false marker from the response.
/// </summary>
public class HandleInactiveResponse : IOpenIddictValidationHandler<HandleIntrospectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<HandleInactiveResponse>()
.SetOrder(ValidateWellKnownParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleIntrospectionResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Note: the introspection specification requires that server return "active: false" instead of a proper
// OAuth 2.0 error when the token is invalid, expired, revoked or invalid for any other reason.
// While OpenIddict's server can be tweaked to return a proper error (by removing NormalizeErrorResponse)
// from the enabled handlers, supporting "active: false" is required to ensure total compatibility.
var active = (bool?) context.Response[Parameters.Active];
if (active is null)
{
context.Reject(
error: Errors.ServerError,
description: SR.FormatID2105(Parameters.Active),
uri: SR.FormatID8000(SR.ID2105));
return default;
}
if (active is not true)
{
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2106),
uri: SR.FormatID8000(SR.ID2106));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting the issuer from the introspection response.
/// </summary>
public class ValidateIssuer : IOpenIddictValidationHandler<HandleIntrospectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<ValidateIssuer>()
.SetOrder(ValidateWellKnownParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleIntrospectionResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// The issuer claim is optional. If it's not null or empty, validate it to
// ensure it matches the issuer registered in the server configuration.
var issuer = (string?) context.Response[Claims.Issuer];
if (!string.IsNullOrEmpty(issuer))
{
if (!Uri.TryCreate(issuer, UriKind.Absolute, out Uri? uri))
{
context.Reject(
error: Errors.ServerError,
description: SR.GetResourceString(SR.ID2108),
uri: SR.FormatID8000(SR.ID2108));
return default;
}
if (context.Issuer is not null && context.Issuer != uri)
{
context.Reject(
error: Errors.ServerError,
description: SR.GetResourceString(SR.ID2109),
uri: SR.FormatID8000(SR.ID2109));
return default;
}
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting and validating the token usage from the introspection response.
/// </summary>
public class ValidateTokenUsage : IOpenIddictValidationHandler<HandleIntrospectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<ValidateTokenUsage>()
.SetOrder(ValidateIssuer.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleIntrospectionResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// OpenIddict-based authorization servers always return the actual token type using
// the special "token_usage" claim, that helps resource servers determine whether the
// introspected token is of the expected type and prevent token substitution attacks.
// In this handler, the "token_usage" is verified to ensure it corresponds to a supported
// value so that the component that triggered the introspection request can determine
// whether the returned token has an acceptable type depending on the context.
var usage = (string?) context.Response[Claims.TokenUsage];
if (string.IsNullOrEmpty(usage))
{
return default;
}
if (!(usage switch
{
// Note: by default, OpenIddict only allows access/refresh tokens to be
// introspected but additional types can be added using the events model.
TokenTypeHints.AccessToken or TokenTypeHints.AuthorizationCode or
TokenTypeHints.IdToken or TokenTypeHints.RefreshToken or
TokenTypeHints.UserCode
=> true,
_ => false // Other token usages are not supported.
}))
{
context.Reject(
error: Errors.ServerError,
description: SR.GetResourceString(SR.ID2118),
uri: SR.FormatID8000(SR.ID2118));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting the claims from the introspection response.
/// </summary>
public class PopulateClaims : IOpenIddictValidationHandler<HandleIntrospectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<PopulateClaims>()
.SetOrder(ValidateTokenUsage.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(HandleIntrospectionResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
// Ensure the issuer resolved from the configuration matches the expected value.
if (configuration is not null && configuration.Issuer != context.Issuer)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0307));
}
// Create a new claims-based identity using the same authentication type
// and the name/role claims as the one used by IdentityModel for JWT tokens.
var identity = new ClaimsIdentity(
context.Options.TokenValidationParameters.AuthenticationType,
context.Options.TokenValidationParameters.NameClaimType,
context.Options.TokenValidationParameters.RoleClaimType);
// Resolve the issuer that will be attached to the claims created by this handler.
// Note: at this stage, the optional issuer extracted from the response is assumed
// to be valid, as it is guarded against unknown values by the ValidateIssuer handler.
var issuer = (string?) context.Response[Claims.Issuer] ??
configuration?.Issuer?.AbsoluteUri ??
context.Issuer?.AbsoluteUri ?? ClaimsIdentity.DefaultIssuer;
foreach (var parameter in context.Response.GetParameters())
{
// Always exclude null keys as they can't be represented as valid claims.
if (string.IsNullOrEmpty(parameter.Key))
{
continue;
}
// Exclude OpenIddict-specific private claims, that MUST NOT be set based on data returned
// by the remote authorization server (that may or may not be an OpenIddict server).
if (parameter.Key.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase))
{
continue;
}
// Ignore all protocol claims that shouldn't be mapped to CLR claims.
if (parameter.Key is Claims.Active or Claims.Issuer or Claims.NotBefore or Claims.TokenType)
{
continue;
}
// Note: in the typical case, the response parameters should be deserialized from a
// JSON response and thus natively stored as System.Text.Json.JsonElement instances.
//
// In the rare cases where the underlying value wouldn't be a JsonElement instance
// (e.g when custom parameters are manually added to the response), the static
// conversion operator would take care of converting the underlying value to a
// JsonElement instance using the same value type as the original parameter value.
switch ((JsonElement) parameter.Value)
{
// Top-level claims represented as arrays are split and mapped to multiple CLR claims
// to match the logic implemented by IdentityModel for JWT token deserialization.
case { ValueKind: JsonValueKind.Array } value:
foreach (var item in value.EnumerateArray())
{
identity.AddClaim(new Claim(
type : parameter.Key,
value : item.ToString()!,
valueType : GetClaimValueType(item),
issuer : issuer,
originalIssuer: issuer,
subject : identity));
}
break;
// Note: JsonElement.ToString() returns string.Empty for JsonValueKind.Null and
// JsonValueKind.Undefined, which, unlike null strings, is a valid claim value.
case { ValueKind: _ } value:
identity.AddClaim(new Claim(
type : parameter.Key,
value : value.ToString()!,
valueType : GetClaimValueType(value),
issuer : issuer,
originalIssuer: issuer,
subject : identity));
break;
}
}
context.Principal = new ClaimsPrincipal(identity);
static string GetClaimValueType(JsonElement element) => element.ValueKind switch
{
JsonValueKind.String => ClaimValueTypes.String,
JsonValueKind.True or JsonValueKind.False => ClaimValueTypes.Boolean,
JsonValueKind.Number when element.TryGetInt32(out _) => ClaimValueTypes.Integer32,
JsonValueKind.Number when element.TryGetInt64(out _) => ClaimValueTypes.Integer64,
JsonValueKind.Number when element.TryGetUInt32(out _) => ClaimValueTypes.UInteger32,
JsonValueKind.Number when element.TryGetUInt64(out _) => ClaimValueTypes.UInteger64,
JsonValueKind.Number when element.TryGetDouble(out _) => ClaimValueTypes.Double,
JsonValueKind.Null or JsonValueKind.Undefined => JsonClaimValueTypes.JsonNull,
JsonValueKind.Array => JsonClaimValueTypes.JsonArray,
JsonValueKind.Object or _ => JsonClaimValueTypes.Json
};
}
}
}
}