diff --git a/eng/Versions.props b/eng/Versions.props index 6e1a40b8..8583ed33 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -17,7 +17,7 @@ 2019.1.3 12.0.2 1.0.1 - 6.2.0-preview-60806030202 + 6.2.0-preview-60906195846 1.5.0 4.0.0-preview.6.build.801 2.9.0 diff --git a/samples/Mvc.Client/Startup.cs b/samples/Mvc.Client/Startup.cs index 73e225ef..482cba07 100644 --- a/samples/Mvc.Client/Startup.cs +++ b/samples/Mvc.Client/Startup.cs @@ -56,6 +56,8 @@ namespace Mvc.Client options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; + + options.AccessDeniedPath = "/"; }); services.AddMvc(); diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index 64b22e1c..08fa8207 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs @@ -75,6 +75,7 @@ namespace OpenIddict.Abstractions public const string StreetAddress = "street_address"; public const string Subject = "sub"; public const string TokenType = "token_type"; + public const string TokenUsage = "token_usage"; public const string UpdatedAt = "updated_at"; public const string Username = "username"; public const string Website = "website"; @@ -93,7 +94,8 @@ namespace OpenIddict.Abstractions public const string CodeChallenge = "oi_cd_chlg"; public const string CodeChallengeMethod = "oi_cd_chlg_meth"; public const string IdentityTokenLifetime = "oi_idt_lft"; - public const string OriginalRedirectUri = "oi_reduri"; + public const string Nonce = "oi_nce"; + public const string RedirectUri = "oi_reduri"; public const string RefreshTokenLifetime = "oi_reft_lft"; public const string TokenUsage = "oi_tkn_use"; } @@ -310,29 +312,7 @@ namespace OpenIddict.Abstractions public static class Properties { - public const string AccessTokenLifetime = ".access_token_lifetime"; - public const string AuthorizationCodeLifetime = ".authorization_code_lifetime"; - public const string Audiences = ".audiences"; - public const string CodeChallenge = ".code_challenge"; - public const string CodeChallengeMethod = ".code_challenge_method"; public const string Destinations = ".destinations"; - public const string Error = ".error"; - public const string ErrorDescription = ".error_description"; - public const string ErrorUri = ".error_uri"; - public const string Expires = ".expires"; - public const string IdentityTokenLifetime = ".identity_token_lifetime"; - public const string Issued = ".issued"; - public const string Nonce = ".nonce"; - public const string OriginalPrincipal = ".original_principal"; - public const string OriginalRedirectUri = ".original_redirect_uri"; - public const string PostLogoutRedirectUri = ".post_logout_redirect_uri"; - public const string Presenters = ".presenters"; - public const string RefreshTokenLifetime = ".refresh_token_lifetime"; - public const string Resources = ".resources"; - public const string Scopes = ".scopes"; - public const string TokenId = ".token_id"; - public const string TokenUsage = ".token_usage"; - public const string ValidatedRedirectUri = ".validated_redirect_uri"; } public static class ResponseModes diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs index b9ef9553..1a9e6261 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs @@ -1260,6 +1260,85 @@ namespace OpenIddict.Abstractions return principal.GetClaim(Claims.JwtId); } + /// + /// Gets the token usage associated with the claims principal. + /// + /// The claims principal. + /// The token usage or null if the claim cannot be found. + public static string GetTokenUsage([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return principal.GetClaim(Claims.Private.TokenUsage); + } + + /// + /// Gets a boolean value indicating whether the + /// claims principal corresponds to an access token. + /// + /// The claims principal. + /// true if the principal corresponds to an access token. + public static bool IsAccessToken([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return string.Equals(principal.GetTokenUsage(), TokenUsages.AccessToken, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Gets a boolean value indicating whether the + /// claims principal corresponds to an access token. + /// + /// The claims principal. + /// true if the principal corresponds to an authorization code. + public static bool IsAuthorizationCode([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return string.Equals(principal.GetTokenUsage(), TokenUsages.AuthorizationCode, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Gets a boolean value indicating whether the + /// claims principal corresponds to an identity token. + /// + /// The claims principal. + /// true if the principal corresponds to an identity token. + public static bool IsIdentityToken([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return string.Equals(principal.GetTokenUsage(), TokenUsages.IdToken, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Gets a boolean value indicating whether the + /// claims principal corresponds to a refresh token. + /// + /// The claims principal. + /// true if the principal corresponds to a refresh token. + public static bool IsRefreshToken([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return string.Equals(principal.GetTokenUsage(), TokenUsages.RefreshToken, StringComparison.OrdinalIgnoreCase); + } + /// /// Determines whether the claims principal contains at least one audience. /// diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs index cd5ff4be..c38f46bd 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs @@ -123,12 +123,13 @@ namespace OpenIddict.Abstractions // not be present more than once but derived specifications like the // token exchange RFC deliberately allow specifying multiple resource // parameters with the same name to represent a multi-valued parameter. - switch (parameter.Value?.Length ?? 0) + AddParameter(parameter.Key, parameter.Value?.Length switch { - case 0: AddParameter(parameter.Key, default); break; - case 1: AddParameter(parameter.Key, parameter.Value[0]); break; - default: AddParameter(parameter.Key, parameter.Value); break; - } + null => default, + 0 => default, + 1 => new OpenIddictParameter(parameter.Value[0]), + _ => new OpenIddictParameter(parameter.Value) + }); } } @@ -154,12 +155,12 @@ namespace OpenIddict.Abstractions // not be present more than once but derived specifications like the // token exchange RFC deliberately allow specifying multiple resource // parameters with the same name to represent a multi-valued parameter. - switch (parameter.Value.Count) + AddParameter(parameter.Key, parameter.Value.Count switch { - case 0: AddParameter(parameter.Key, default); break; - case 1: AddParameter(parameter.Key, parameter.Value[0]); break; - default: AddParameter(parameter.Key, parameter.Value.ToArray()); break; - } + 0 => default, + 1 => new OpenIddictParameter(parameter.Value[0]), + _ => new OpenIddictParameter(parameter.Value.ToArray()) + }); } } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs new file mode 100644 index 00000000..8135e8e0 --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs @@ -0,0 +1,97 @@ +/* + * 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; +using System.Collections.Immutable; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore; +using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server.AspNetCore +{ + public static partial class OpenIddictServerAspNetCoreHandlers + { + public static class Introspection + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Introspection request extraction: + */ + ExtractGetOrPostRequest.Descriptor, + + /* + * Introspection request handling: + */ + InferIssuerFromHost.Descriptor, + + /* + * Introspection response processing: + */ + ProcessJsonResponse.Descriptor); + + /// + /// Contains the logic responsible of infering the issuer URL from the HTTP request host. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class InferIssuerFromHost : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(OpenIddictServerHandlers.Introspection.AttachMetadataClaims.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] HandleIntrospectionRequestContext context) + { + if (context == 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(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + // If the issuer was not populated by another handler (e.g from the server options), + // try to infer it from the request scheme/host/path base (which requires HTTP/1.1). + if (context.Issuer == null) + { + if (!request.Host.HasValue) + { + throw new InvalidOperationException("No host was attached to the HTTP request."); + } + + if (!Uri.TryCreate(request.Scheme + "://" + request.Host + request.PathBase, UriKind.Absolute, out Uri issuer)) + { + throw new InvalidOperationException("The issuer address cannot be inferred from the current request."); + } + + context.Issuer = issuer.AbsoluteUri; + } + + return default; + } + } + } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs index ed5ed5bf..2988e8ab 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs @@ -37,6 +37,7 @@ namespace OpenIddict.Server.AspNetCore .AddRange(Authentication.DefaultHandlers) .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) + .AddRange(Introspection.DefaultHandlers) .AddRange(Serialization.DefaultHandlers) .AddRange(Session.DefaultHandlers) .AddRange(Userinfo.DefaultHandlers); diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs new file mode 100644 index 00000000..0713e1ed --- /dev/null +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs @@ -0,0 +1,32 @@ +/* + * 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. + */ + +namespace OpenIddict.Server.DataProtection +{ + public static class OpenIddictServerDataProtectionConstants + { + public static class Properties + { + public const string AccessTokenLifetime = ".access_token_lifetime"; + public const string AuthorizationCodeLifetime = ".authorization_code_lifetime"; + public const string Audiences = ".audiences"; + public const string CodeChallenge = ".code_challenge"; + public const string CodeChallengeMethod = ".code_challenge_method"; + public const string DataProtector = ".data_protector"; + public const string Expires = ".expires"; + public const string IdentityTokenLifetime = ".identity_token_lifetime"; + public const string Issued = ".issued"; + public const string Nonce = ".nonce"; + public const string OriginalRedirectUri = ".original_redirect_uri"; + public const string Presenters = ".presenters"; + public const string RefreshTokenLifetime = ".refresh_token_lifetime"; + public const string Resources = ".resources"; + public const string Scopes = ".scopes"; + public const string TokenId = ".token_id"; + public const string TokenUsage = ".token_usage"; + } + } +} diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Serialization.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Serialization.cs index 8d5ea736..a8963fd2 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Serialization.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Serialization.cs @@ -25,6 +25,7 @@ using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlers.Serialization; +using Properties = OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Properties; namespace OpenIddict.Server.DataProtection { @@ -98,7 +99,7 @@ namespace OpenIddict.Server.DataProtection throw new ArgumentNullException(nameof(context)); } - if (!context.Properties.TryGetValue(typeof(IDataProtector).FullName, out var property) || + if (!context.Properties.TryGetValue(Properties.DataProtector, out var property) || !(property is IDataProtector protector)) { throw new InvalidOperationException(new StringBuilder() @@ -128,7 +129,7 @@ namespace OpenIddict.Server.DataProtection SetProperty(properties, Properties.Issued, context.Principal.GetCreationDate()?.ToString("r", CultureInfo.InvariantCulture)); SetProperty(properties, Properties.OriginalRedirectUri, - context.Principal.GetClaim(Claims.Private.OriginalRedirectUri)); + context.Principal.GetClaim(Claims.Private.RedirectUri)); SetProperty(properties, Properties.RefreshTokenLifetime, context.Principal.GetClaim(Claims.Private.RefreshTokenLifetime)); @@ -306,7 +307,7 @@ namespace OpenIddict.Server.DataProtection throw new ArgumentNullException(nameof(context)); } - if (!context.Properties.TryGetValue(typeof(IDataProtector).FullName, out var property) || + if (!context.Properties.TryGetValue(Properties.DataProtector, out var property) || !(property is IDataProtector protector)) { throw new InvalidOperationException(new StringBuilder() @@ -344,8 +345,12 @@ namespace OpenIddict.Server.DataProtection .SetClaim(Claims.Private.CodeChallenge, GetProperty(properties, Properties.CodeChallenge)) .SetClaim(Claims.Private.CodeChallengeMethod, GetProperty(properties, Properties.CodeChallengeMethod)) .SetClaim(Claims.Private.IdentityTokenLifetime, GetProperty(properties, Properties.IdentityTokenLifetime)) - .SetClaim(Claims.Private.OriginalRedirectUri, GetProperty(properties, Properties.OriginalRedirectUri)) - .SetClaim(Claims.Private.RefreshTokenLifetime, GetProperty(properties, Properties.RefreshTokenLifetime)); + .SetClaim(Claims.Private.RedirectUri, GetProperty(properties, Properties.OriginalRedirectUri)) + .SetClaim(Claims.Private.RefreshTokenLifetime, GetProperty(properties, Properties.RefreshTokenLifetime)) + + // Note: since the data format relies on a data protector using different "purposes" strings + // per token type, the token processed at this stage is guaranteed to be of the expected type. + .SetClaim(Claims.Private.TokenUsage, (string) context.Properties[Properties.TokenUsage]); context.HandleDeserialization(); @@ -532,7 +537,8 @@ namespace OpenIddict.Server.DataProtection } var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); - context.Properties[typeof(IDataProtector).FullName] = protector; + context.Properties[Properties.DataProtector] = protector; + context.Properties[Properties.TokenUsage] = TokenUsages.AccessToken; return default; } @@ -586,7 +592,8 @@ namespace OpenIddict.Server.DataProtection } var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); - context.Properties[typeof(IDataProtector).FullName] = protector; + context.Properties[Properties.DataProtector] = protector; + context.Properties[Properties.TokenUsage] = TokenUsages.AuthorizationCode; return default; } @@ -640,7 +647,8 @@ namespace OpenIddict.Server.DataProtection } var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); - context.Properties[typeof(IDataProtector).FullName] = protector; + context.Properties[Properties.DataProtector] = protector; + context.Properties[Properties.TokenUsage] = TokenUsages.RefreshToken; return default; } @@ -694,7 +702,8 @@ namespace OpenIddict.Server.DataProtection } var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); - context.Properties[typeof(IDataProtector).FullName] = protector; + context.Properties[Properties.DataProtector] = protector; + context.Properties[Properties.TokenUsage] = TokenUsages.AccessToken; return default; } @@ -748,7 +757,8 @@ namespace OpenIddict.Server.DataProtection } var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); - context.Properties[typeof(IDataProtector).FullName] = protector; + context.Properties[Properties.DataProtector] = protector; + context.Properties[Properties.TokenUsage] = TokenUsages.AuthorizationCode; return default; } @@ -802,7 +812,8 @@ namespace OpenIddict.Server.DataProtection } var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); - context.Properties[typeof(IDataProtector).FullName] = protector; + context.Properties[Properties.DataProtector] = protector; + context.Properties[Properties.TokenUsage] = TokenUsages.RefreshToken; return default; } diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs new file mode 100644 index 00000000..603b1c95 --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs @@ -0,0 +1,97 @@ +/* + * 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; +using System.Collections.Immutable; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Owin; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlerFilters; + +namespace OpenIddict.Server.Owin +{ + public static partial class OpenIddictServerOwinHandlers + { + public static class Introspection + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Introspection request extraction: + */ + ExtractGetOrPostRequest.Descriptor, + + /* + * Introspection request handling: + */ + InferIssuerFromHost.Descriptor, + + /* + * Introspection response processing: + */ + ProcessJsonResponse.Descriptor); + + /// + /// Contains the logic responsible of infering the issuer URL from the HTTP request host. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class InferIssuerFromHost : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(OpenIddictServerHandlers.Introspection.AttachMetadataClaims.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] HandleIntrospectionRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + // If the issuer was not populated by another handler (e.g from the server options), + // try to infer it from the request scheme/host/path base (which requires HTTP/1.1). + if (context.Issuer == null) + { + if (string.IsNullOrEmpty(request.Host.Value)) + { + throw new InvalidOperationException("No host was attached to the HTTP request."); + } + + if (!Uri.TryCreate(request.Scheme + "://" + request.Host + request.PathBase, UriKind.Absolute, out Uri issuer)) + { + throw new InvalidOperationException("The issuer address cannot be inferred from the current request."); + } + + context.Issuer = issuer.AbsoluteUri; + } + + return default; + } + } + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs index a03b6e73..f9539399 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs @@ -36,6 +36,7 @@ namespace OpenIddict.Server.Owin .AddRange(Authentication.DefaultHandlers) .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) + .AddRange(Introspection.DefaultHandlers) .AddRange(Serialization.DefaultHandlers) .AddRange(Session.DefaultHandlers) .AddRange(Userinfo.DefaultHandlers); diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index a67e35b4..8c262b92 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -136,8 +136,9 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Makes client identification optional so that token and revocation + /// Makes client identification optional so that token, introspection and revocation /// requests that don't specify a client_id are not automatically rejected. + /// Enabling this option is NOT recommended. /// /// The . public OpenIddictServerBuilder AcceptAnonymousClients() diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs index 37c2619e..a744936a 100644 --- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs +++ b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs @@ -116,6 +116,17 @@ namespace OpenIddict.Server .ToString()); } + if (options.IntrospectionEndpointUris.Count != 0 && !options.CustomHandlers.Any( + descriptor => descriptor.ContextType == typeof(ValidateIntrospectionRequestContext) && + descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) + { + throw new InvalidOperationException(new StringBuilder() + .Append("No custom introspection request validation handler was found. When enabling the degraded mode, ") + .Append("a custom 'IOpenIddictServerHandler' must be implemented ") + .Append("to validate introspection requests (e.g to ensure the client_id and client_secret are valid).") + .ToString()); + } + if (options.LogoutEndpointUris.Count != 0 && !options.CustomHandlers.Any( descriptor => descriptor.ContextType == typeof(ValidateLogoutRequestContext) && descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) diff --git a/src/OpenIddict.Server/OpenIddictServerConstants.cs b/src/OpenIddict.Server/OpenIddictServerConstants.cs new file mode 100644 index 00000000..4f261a2e --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerConstants.cs @@ -0,0 +1,18 @@ +/* + * 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. + */ + +namespace OpenIddict.Server +{ + public static class OpenIddictServerConstants + { + public static class Properties + { + public const string Principal = ".principal"; + public const string ValidatedPostLogoutRedirectUri = ".validated_post_logout_redirect_uri"; + public const string ValidatedRedirectUri = ".validated_redirect_uri"; + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs index 37ebe685..fd6d9d25 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; +using System.Security.Claims; using JetBrains.Annotations; using OpenIddict.Abstractions; @@ -47,6 +48,11 @@ namespace OpenIddict.Server /// introspection request, or null if it cannot be found. /// public string TokenTypeHint => Request.TokenTypeHint; + + /// + /// Gets or sets the security principal extracted from the introspected token, if available. + /// + public ClaimsPrincipal Principal { get; set; } } /// @@ -64,16 +70,15 @@ namespace OpenIddict.Server } /// - /// Gets the additional claims returned to the caller. + /// Gets or sets the security principal extracted from the introspected token. /// - public IDictionary Claims { get; } = - new Dictionary(StringComparer.Ordinal); + public ClaimsPrincipal Principal { get; set; } /// - /// Gets or sets the flag indicating - /// whether the token is active or inactive. + /// Gets the additional claims returned to the caller. /// - public bool Active { get; set; } + public IDictionary Claims { get; } = + new Dictionary(StringComparer.Ordinal); /// /// Gets the list of audiences returned to the caller diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs index 32004ed0..ce56f5ad 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -47,7 +47,6 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs index bf97ea81..b3033b35 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs @@ -79,22 +79,6 @@ namespace OpenIddict.Server } } - /// - /// Represents a filter that excludes the associated handlers if the degraded mode was enabled. - /// - public class RequireDegradedModeEnabled : IOpenIddictServerHandlerFilter - { - public ValueTask IsActiveAsync([NotNull] BaseContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - return new ValueTask(context.Options.EnableDegradedMode); - } - } - /// /// Represents a filter that excludes the associated handlers if endpoint permissions were disabled. /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs index 19b77319..a4d34a37 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs @@ -14,6 +14,7 @@ using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlerFilters; +using Properties = OpenIddict.Server.OpenIddictServerConstants.Properties; namespace OpenIddict.Server { diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs index 1669d443..bf444e06 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs @@ -18,6 +18,7 @@ using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlerFilters; +using Properties = OpenIddict.Server.OpenIddictServerConstants.Properties; namespace OpenIddict.Server { @@ -203,7 +204,7 @@ namespace OpenIddict.Server } // Store the security principal extracted from the authorization code/refresh token as an environment property. - context.Transaction.Properties[Properties.OriginalPrincipal] = notification.Principal; + context.Transaction.Properties[Properties.Principal] = notification.Principal; context.Logger.LogInformation("The token request was successfully validated."); } @@ -427,8 +428,7 @@ namespace OpenIddict.Server } /// - /// Contains the logic responsible of rejecting token requests that don't - /// specify a client identifier for the authorization code grant type. + /// Contains the logic responsible of rejecting token requests that don't specify a client identifier. /// public class ValidateClientIdParameter : IOpenIddictServerHandler { @@ -1478,7 +1478,7 @@ namespace OpenIddict.Server // if the authorization request didn't contain an explicit redirect_uri. // See https://tools.ietf.org/html/rfc6749#section-4.1.3 // and http://openid.net/specs/openid-connect-core-1_0.html#TokenRequestValidation. - var address = context.Principal.GetClaim(Claims.Private.OriginalRedirectUri); + var address = context.Principal.GetClaim(Claims.Private.RedirectUri); if (string.IsNullOrEmpty(address)) { return default; @@ -1739,7 +1739,7 @@ namespace OpenIddict.Server return default; } - if (context.Transaction.Properties.TryGetValue(Properties.OriginalPrincipal, out var principal)) + if (context.Transaction.Properties.TryGetValue(Properties.Principal, out var principal)) { context.Principal ??= (ClaimsPrincipal) principal; } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs new file mode 100644 index 00000000..4c366dd7 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs @@ -0,0 +1,1231 @@ +/* + * 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; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlerFilters; +using Properties = OpenIddict.Server.OpenIddictServerConstants.Properties; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerHandlers + { + public static class Introspection + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Introspection request top-level processing: + */ + ExtractIntrospectionRequest.Descriptor, + ValidateIntrospectionRequest.Descriptor, + HandleIntrospectionRequest.Descriptor, + ApplyIntrospectionResponse.Descriptor, + ApplyIntrospectionResponse.Descriptor, + + /* + * Introspection request validation: + */ + ValidateTokenParameter.Descriptor, + ValidateClientIdParameter.Descriptor, + ValidateClientId.Descriptor, + ValidateClientSecret.Descriptor, + ValidateEndpointPermissions.Descriptor, + ValidateToken.Descriptor, + ValidateAuthorizedParty.Descriptor, + + /* + * Introspection request handling: + */ + AttachPrincipal.Descriptor, + AttachMetadataClaims.Descriptor, + AttachApplicationClaims.Descriptor, + + /* + * Introspection response handling: + */ + NormalizeErrorResponse.Descriptor); + + /// + /// Contains the logic responsible of extracting introspection requests and invoking the corresponding event handlers. + /// + public class ExtractIntrospectionRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ExtractIntrospectionRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Introspection) + { + return; + } + + var notification = new ExtractIntrospectionRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + if (notification.Request == null) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The introspection request was not correctly extracted. To extract introspection requests, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .ToString()); + } + + context.Logger.LogInformation("The introspection request was successfully extracted: {Request}.", notification.Request); + } + } + + /// + /// Contains the logic responsible of validating introspection requests and invoking the corresponding event handlers. + /// + public class ValidateIntrospectionRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ValidateIntrospectionRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ExtractIntrospectionRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Introspection) + { + return; + } + + var notification = new ValidateIntrospectionRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + // Store the security principal extracted from the introspected token as an environment property. + context.Transaction.Properties[Properties.Principal] = notification.Principal; + + context.Logger.LogInformation("The introspection request was successfully validated."); + } + } + + /// + /// Contains the logic responsible of handling introspection requests and invoking the corresponding event handlers. + /// + public class HandleIntrospectionRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public HandleIntrospectionRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ValidateIntrospectionRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Introspection) + { + return; + } + + var notification = new HandleIntrospectionRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + var response = new OpenIddictResponse + { + [Claims.Active] = true, + [Claims.Issuer] = notification.Issuer, + [Claims.Username] = notification.Username, + [Claims.Subject] = notification.Subject, + [Claims.Scope] = string.Join(" ", notification.Scopes), + [Claims.JwtId] = notification.TokenId, + [Claims.TokenType] = notification.TokenType, + [Claims.TokenUsage] = notification.TokenUsage, + [Claims.ClientId] = notification.ClientId + }; + + if (notification.IssuedAt != null) + { + response[Claims.IssuedAt] = EpochTime.GetIntDate(notification.IssuedAt.Value.UtcDateTime); + } + + if (notification.NotBefore != null) + { + response[Claims.NotBefore] = EpochTime.GetIntDate(notification.NotBefore.Value.UtcDateTime); + } + + if (notification.ExpiresAt != null) + { + response[Claims.ExpiresAt] = EpochTime.GetIntDate(notification.ExpiresAt.Value.UtcDateTime); + } + + switch (notification.Audiences.Count) + { + case 0: break; + + case 1: + response[Claims.Audience] = notification.Audiences.ElementAt(0); + break; + + default: + response[Claims.Audience] = new JArray(notification.Audiences); + break; + } + + foreach (var claim in notification.Claims) + { + response.SetParameter(claim.Key, claim.Value); + } + + context.Response = response; + } + } + + /// + /// Contains the logic responsible of processing sign-in responses and invoking the corresponding event handlers. + /// + public class ApplyIntrospectionResponse : IOpenIddictServerHandler where TContext : BaseRequestContext + { + private readonly IOpenIddictServerProvider _provider; + + public ApplyIntrospectionResponse([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler>() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Introspection) + { + return; + } + + var notification = new ApplyIntrospectionResponseContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting introspection requests that don't specify a token. + /// + public class ValidateTokenParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ValidateIntrospectionRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Reject introspection requests missing the mandatory token parameter. + if (string.IsNullOrEmpty(context.Request.Token)) + { + context.Logger.LogError("The introspection request was rejected because the token was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'token' parameter is missing."); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting introspection requests that don't specify a client identifier. + /// + public class ValidateClientIdParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateTokenParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ValidateIntrospectionRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // At this stage, reject the introspection request unless the client identification requirement was disabled. + if (!context.Options.AcceptAnonymousClients && string.IsNullOrEmpty(context.ClientId)) + { + context.Logger.LogError("The introspection request was rejected because the mandatory 'client_id' was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'client_id' parameter is missing."); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting introspection requests that use an invalid client_id. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateClientId : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientId() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateClientId([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientIdParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ValidateIntrospectionRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Retrieve the application details corresponding to the requested client_id. + // If no entity can be found, this likely indicates that the client_id is invalid. + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + context.Logger.LogError("The introspection request was rejected because the client " + + "application was not found: '{ClientId}'.", context.ClientId); + + context.Reject( + error: Errors.InvalidClient, + description: "The specified 'client_id' parameter is invalid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting introspection requests specifying an invalid client secret. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateClientSecret : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientSecret() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateClientSecret([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientId.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ValidateIntrospectionRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + // If the application is not a public client, validate the client secret. + if (!await _applicationManager.IsPublicAsync(application) && + !await _applicationManager.ValidateClientSecretAsync(application, context.ClientSecret)) + { + context.Logger.LogError("The introspection request was rejected because the confidential or hybrid application " + + "'{ClientId}' didn't specify valid client credentials.", context.ClientId); + + context.Reject( + error: Errors.InvalidClient, + description: "The specified client credentials are invalid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting introspection requests made by + /// applications that haven't been granted the introspection endpoint permission. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateEndpointPermissions : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateEndpointPermissions() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateEndpointPermissions([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientSecret.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ValidateIntrospectionRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + // Reject the request if the application is not allowed to use the introspection endpoint. + if (!await _applicationManager.HasPermissionAsync(application, Permissions.Endpoints.Introspection)) + { + context.Logger.LogError("The introspection request was rejected because the application '{ClientId}' " + + "was not allowed to use the introspection endpoint.", context.ClientId); + + context.Reject( + error: Errors.UnauthorizedClient, + description: "This client application is not allowed to use the introspection endpoint."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting introspection requests that specify an invalid token. + /// + public class ValidateToken : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ValidateToken([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + // This handler is deliberately registered with a high order to ensure it runs + // after custom handlers registered with the default order and prevent the token + // endpoint from disclosing whether the introspected token is valid before + // the caller's identity can first be fully verified by the other handlers. + .SetOrder(100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ValidateIntrospectionRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: use the "token_type_hint" parameter specified by the client application + // to try to determine the type of the token sent by the client application. + // See https://tools.ietf.org/html/rfc7662#section-2.1 for more information. + var principal = context.Request.TokenTypeHint switch + { + TokenTypeHints.AccessToken => await DeserializeAccessTokenAsync(), + TokenTypeHints.AuthorizationCode => await DeserializeAuthorizationCodeAsync(), + TokenTypeHints.IdToken => await DeserializeIdentityTokenAsync(), + TokenTypeHints.RefreshToken => await DeserializeRefreshTokenAsync(), + + _ => null + }; + + // Note: if the introspected token can't be found using "token_type_hint", + // the search must be extended to all supported token types. + // See https://tools.ietf.org/html/rfc7662#section-2.1 for more information. + // To avoid calling the same deserialization methods twice, an additional check + // is made to exclude the corresponding call when a token_type_hint was specified. + principal ??= context.Request.TokenTypeHint switch + { + TokenTypeHints.AccessToken => await DeserializeAuthorizationCodeAsync() ?? + await DeserializeIdentityTokenAsync() ?? + await DeserializeRefreshTokenAsync(), + + TokenTypeHints.AuthorizationCode => await DeserializeAccessTokenAsync() ?? + await DeserializeIdentityTokenAsync() ?? + await DeserializeRefreshTokenAsync(), + + TokenTypeHints.IdToken => await DeserializeAccessTokenAsync() ?? + await DeserializeAuthorizationCodeAsync() ?? + await DeserializeRefreshTokenAsync(), + + TokenTypeHints.RefreshToken => await DeserializeAccessTokenAsync() ?? + await DeserializeAuthorizationCodeAsync() ?? + await DeserializeIdentityTokenAsync(), + + _ => await DeserializeAccessTokenAsync() ?? + await DeserializeAuthorizationCodeAsync() ?? + await DeserializeIdentityTokenAsync() ?? + await DeserializeRefreshTokenAsync() + }; + + if (principal == null) + { + context.Logger.LogError("The introspection request was rejected because the token was invalid."); + + context.Reject( + error: Errors.InvalidToken, + description: "The specified token is invalid."); + + return; + } + + var date = principal.GetExpirationDate(); + if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + { + context.Logger.LogError("The introspection request was rejected because the token was expired."); + + context.Reject( + error: Errors.InvalidToken, + description: "The specified token is no longer valid."); + + return; + } + + // Attach the principal extracted from the token to the parent event context. + context.Principal = principal; + + async ValueTask DeserializeAccessTokenAsync() + { + var notification = new DeserializeAccessTokenContext(context.Transaction) + { + Token = context.Request.Token + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The access token was not correctly processed. This may indicate ") + .Append("that the event handler responsible of validating access tokens ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + return notification.Principal; + } + + async ValueTask DeserializeAuthorizationCodeAsync() + { + var notification = new DeserializeAuthorizationCodeContext(context.Transaction) + { + Token = context.Request.Token + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The authorization code was not correctly processed. This may indicate ") + .Append("that the event handler responsible of validating authorization codes ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + return notification.Principal; + } + + async ValueTask DeserializeIdentityTokenAsync() + { + var notification = new DeserializeIdentityTokenContext(context.Transaction) + { + Token = context.Request.Token + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The identity token was not correctly processed. This may indicate ") + .Append("that the event handler responsible of validating identity token ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + return notification.Principal; + } + + async ValueTask DeserializeRefreshTokenAsync() + { + var notification = new DeserializeRefreshTokenContext(context.Transaction) + { + Token = context.Request.Token + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The refresh token was not correctly processed. This may indicate ") + .Append("that the event handler responsible of validating refresh tokens ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + return notification.Principal; + } + } + } + + /// + /// Contains the logic responsible of rejecting introspection requests that specify a token + /// that cannot be introspected by the client application sending the introspection requests. + /// + public class ValidateAuthorizedParty : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + // Note: when client identification is not enforced, this handler cannot validate + // the audiences/presenters if the client_id of the calling application is not known. + // In this case, the returned claims are limited by AttachApplicationClaims to limit exposure. + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateToken.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ValidateIntrospectionRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // When the introspected token is an authorization code, the caller must be + // listed as a presenter (i.e the party the authorization code was issued to). + if (context.Principal.IsAuthorizationCode()) + { + if (!context.Principal.HasPresenter()) + { + throw new InvalidOperationException("The presenters list cannot be extracted from the authorization code."); + } + + if (!context.Principal.HasPresenter(context.ClientId)) + { + context.Logger.LogError("The introspection request was rejected because the " + + "authorization code was issued to a different client."); + + context.Reject( + error: Errors.InvalidToken, + description: "The client application is not allowed to introspect the specified token."); + + return default; + } + + return default; + } + + // When the introspected token is an access token, the caller must be listed either as a presenter + // (i.e the party the token was issued to) or as an audience (i.e a resource server/API). + // If the access token doesn't contain any explicit presenter/audience, the token is assumed + // to be not specific to any resource server/client application and the check is bypassed. + if (context.Principal.IsAccessToken() && + context.Principal.HasAudience() && !context.Principal.HasAudience(context.ClientId) && + context.Principal.HasPresenter() && !context.Principal.HasPresenter(context.ClientId)) + { + context.Logger.LogError("The introspection request was rejected because the access token " + + "was issued to a different client or for another resource server."); + + context.Reject( + error: Errors.InvalidToken, + description: "The client application is not allowed to introspect the specified token."); + + return default; + } + + // When the introspected token is an identity token, the caller must be listed as an audience + // (i.e the client application the identity token was initially issued to). + // If the identity token doesn't contain any explicit audience, the token is + // assumed to be not specific to any client application and the check is bypassed. + if (context.Principal.IsIdentityToken() && context.Principal.HasAudience() && + !context.Principal.HasAudience(context.ClientId)) + { + context.Logger.LogError("The introspection request was rejected because the " + + "identity token was issued to a different client."); + + context.Reject( + error: Errors.InvalidToken, + description: "The client application is not allowed to introspect the specified token."); + + return default; + } + + // When the introspected token is a refresh token, the caller must be + // listed as a presenter (i.e the party the token was issued to). + // If the refresh token doesn't contain any explicit presenter, the token is + // assumed to be not specific to any client application and the check is bypassed. + if (context.Principal.IsRefreshToken() && context.Principal.HasPresenter() && + !context.Principal.HasPresenter(context.ClientId)) + { + context.Logger.LogError("The introspection request was rejected because the " + + "refresh token was issued to a different client."); + + context.Reject( + error: Errors.InvalidToken, + description: "The client application is not allowed to introspect the specified token."); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the principal + /// extracted from the introspected token to the event context. + /// + public class AttachPrincipal : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] HandleIntrospectionRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Transaction.Properties.TryGetValue(Properties.Principal, out var principal)) + { + context.Principal ??= (ClaimsPrincipal) principal; + } + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the metadata claims extracted from the token the event context. + /// + public class AttachMetadataClaims : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachPrincipal.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] HandleIntrospectionRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.Issuer = context.Options.Issuer?.AbsoluteUri; + context.TokenId = context.Principal.GetTokenId(); + context.TokenUsage = context.Principal.GetTokenUsage(); + context.Subject = context.Principal.GetClaim(Claims.Subject); + + context.IssuedAt = context.NotBefore = context.Principal.GetCreationDate(); + context.ExpiresAt = context.Principal.GetExpirationDate(); + + // Infer the audiences/client_id claims from the properties stored in the security principal. + // Note: the client_id claim must be a unique string so multiple presenters cannot be returned. + // To work around this limitation, only the first one is returned if multiple values are listed. + context.Audiences.UnionWith(context.Principal.GetAudiences()); + context.ClientId = context.Principal.GetPresenters().FirstOrDefault(); + + // Note: only set "token_type" when the received token is an access token. + // See https://tools.ietf.org/html/rfc7662#section-2.2 + // and https://tools.ietf.org/html/rfc6749#section-5.1 for more information. + if (context.Principal.IsAccessToken()) + { + context.TokenType = TokenTypes.Bearer; + } + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the application-specific claims extracted from the token the event context. + /// + public class AttachApplicationClaims : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public AttachApplicationClaims() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public AttachApplicationClaims([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(AttachMetadataClaims.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] HandleIntrospectionRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Don't return application-specific claims if the token is not an access or identity token. + if (!context.Principal.IsAccessToken() && !context.Principal.IsIdentityToken()) + { + return; + } + + // Only the specified audience (i.e the resource server for an access token + // and the client application for an identity token) can access the sensitive + // application-specific claims contained in the introspected access/identity token. + if (!context.Principal.HasAudience(context.Request.ClientId)) + { + return; + } + + var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + // Public clients are not allowed to access sensitive claims as authentication cannot be enforced. + if (await _applicationManager.IsPublicAsync(application)) + { + return; + } + + context.Username = context.Principal.Identity.Name; + context.Scopes.UnionWith(context.Principal.GetScopes()); + + foreach (var grouping in context.Principal.Claims.GroupBy(claim => claim.Type)) + { + // Exclude standard claims, that are already handled via strongly-typed properties. + // Make sure to always update this list when adding new built-in claim properties. + var type = grouping.Key; + switch (type) + { + case Claims.Audience: + case Claims.ExpiresAt: + case Claims.IssuedAt: + case Claims.Issuer: + case Claims.NotBefore: + case Claims.Scope: + case Claims.Subject: + case Claims.TokenType: + case Claims.TokenUsage: + continue; + } + + // Exclude OpenIddict-specific metadata claims, that are always considered private. + if (type.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var claims = grouping.ToArray(); + context.Claims[type] = claims.Length switch + { + // When there's only one claim with the same type, directly + // convert the claim using the specified claim value type. + 1 => ConvertToParameter(claims[0]), + + // When multiple claims share the same type, retrieve the underlying + // JSON values and add everything to a new unique JSON array. + _ => new JArray(claims.Select(claim => ConvertToParameter(claim).Value)) + }; + } + + static OpenIddictParameter ConvertToParameter(Claim claim) => claim.ValueType switch + { + ClaimValueTypes.Boolean => bool.Parse(claim.Value), + + ClaimValueTypes.Integer => int.Parse(claim.Value, CultureInfo.InvariantCulture), + ClaimValueTypes.Integer32 => int.Parse(claim.Value, CultureInfo.InvariantCulture), + ClaimValueTypes.Integer64 => long.Parse(claim.Value, CultureInfo.InvariantCulture), + + JsonClaimValueTypes.Json => JToken.Parse(claim.Value), + JsonClaimValueTypes.JsonArray => JToken.Parse(claim.Value), + + _ => new OpenIddictParameter(claim.Value) + }; + } + } + + /// + /// Contains the logic responsible of converting introspection errors to standard active: false responses. + /// + public class NormalizeErrorResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ApplyIntrospectionResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(context.Error)) + { + return default; + } + + // If the error indicates an invalid token, remove the error details and only return active: false, + // as required by the introspection specification: https://tools.ietf.org/html/rfc7662#section-2.2. + // While this prevent the resource server from determining the root cause of the introspection failure, + // this is required to keep OpenIddict fully standard and compatible with all introspection clients. + + if (string.Equals(context.Error, Errors.InvalidToken, StringComparison.Ordinal)) + { + context.Response.Error = null; + context.Response.ErrorDescription = null; + context.Response.ErrorUri = null; + + context.Response[Claims.Active] = false; + } + + return default; + } + } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Serialization.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Serialization.cs index 547bea7f..837b7777 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Serialization.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Serialization.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; -using System.Globalization; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -16,7 +15,6 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json.Linq; using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; @@ -109,37 +107,15 @@ namespace OpenIddict.Server throw new InvalidOperationException("The token usage cannot be null or empty."); } - var destinations = new Dictionary(StringComparer.Ordinal); var claims = new Dictionary(StringComparer.Ordinal) { [Claims.Private.TokenUsage] = context.TokenUsage }; + var destinations = new Dictionary(StringComparer.Ordinal); foreach (var group in context.Principal.Claims.GroupBy(claim => claim.Type)) { var collection = group.ToList(); - switch (collection.Count) - { - case 1: - claims[group.Key] = collection[0].ValueType switch - { - ClaimValueTypes.Boolean => bool.Parse(collection[0].Value), - ClaimValueTypes.Double => double.Parse(collection[0].Value, NumberStyles.Number, CultureInfo.InvariantCulture), - ClaimValueTypes.Integer => int.Parse(collection[0].Value, NumberStyles.Integer, CultureInfo.InvariantCulture), - ClaimValueTypes.Integer32 => int.Parse(collection[0].Value, NumberStyles.Integer, CultureInfo.InvariantCulture), - ClaimValueTypes.Integer64 => long.Parse(collection[0].Value, NumberStyles.Integer, CultureInfo.InvariantCulture), - - "JSON" => JObject.Parse(collection[0].Value), - "JSON_ARRAY" => JArray.Parse(collection[0].Value), - - _ => (object) collection[0].Value - }; - break; - - default: - claims[group.Key] = collection.Select(claim => claim.Value).ToArray(); - break; - } // Note: destinations are attached to claims as special CLR properties. Such properties can't be serialized // as part of classic JWT tokens. To work around this limitation, claim destinations are added to a special @@ -170,6 +146,7 @@ namespace OpenIddict.Server context.Token = context.SecurityTokenHandler.CreateToken(new SecurityTokenDescriptor { + Subject = (ClaimsIdentity) context.Principal.Identity, Claims = new ReadOnlyDictionary(claims), EncryptingCredentials = context.EncryptingCredentials, Issuer = context.Issuer?.AbsoluteUri, diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs index 4a8a8b3e..11cd8d94 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs @@ -14,6 +14,7 @@ using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlerFilters; +using Properties = OpenIddict.Server.OpenIddictServerConstants.Properties; namespace OpenIddict.Server { @@ -184,7 +185,7 @@ namespace OpenIddict.Server if (!string.IsNullOrEmpty(notification.PostLogoutRedirectUri)) { // Store the validated post_logout_redirect_uri as an environment property. - context.Transaction.Properties[Properties.PostLogoutRedirectUri] = notification.PostLogoutRedirectUri; + context.Transaction.Properties[Properties.ValidatedPostLogoutRedirectUri] = notification.PostLogoutRedirectUri; } context.Logger.LogInformation("The logout request was successfully validated."); @@ -522,7 +523,7 @@ namespace OpenIddict.Server // Note: at this stage, the validated redirect URI property may be null (e.g if an error // is returned from the ExtractLogoutRequest/ValidateLogoutRequest events). - if (context.Transaction.Properties.TryGetValue(Properties.PostLogoutRedirectUri, out var property)) + if (context.Transaction.Properties.TryGetValue(Properties.ValidatedPostLogoutRedirectUri, out var property)) { context.PostLogoutRedirectUri = (string) property; } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs index c1e6cf5e..aa74914a 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs @@ -16,6 +16,7 @@ using Newtonsoft.Json.Linq; using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; +using Properties = OpenIddict.Server.OpenIddictServerConstants.Properties; namespace OpenIddict.Server { @@ -186,7 +187,7 @@ namespace OpenIddict.Server } // Store the security principal extracted from the authorization code/refresh token as an environment property. - context.Transaction.Properties[Properties.OriginalPrincipal] = notification.Principal; + context.Transaction.Properties[Properties.Principal] = notification.Principal; context.Logger.LogInformation("The userinfo request was successfully validated."); } @@ -497,7 +498,7 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - if (context.Transaction.Properties.TryGetValue(Properties.OriginalPrincipal, out var principal)) + if (context.Transaction.Properties.TryGetValue(Properties.Principal, out var principal)) { context.Principal ??= (ClaimsPrincipal) principal; } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 262547c5..9e584ddf 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -45,6 +45,7 @@ namespace OpenIddict.Server .AddRange(Authentication.DefaultHandlers) .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) + .AddRange(Introspection.DefaultHandlers) .AddRange(Serialization.DefaultHandlers) .AddRange(Session.DefaultHandlers) .AddRange(Userinfo.DefaultHandlers); @@ -502,7 +503,7 @@ namespace OpenIddict.Server // receiving a grant_type=authorization_code token request. if (!string.IsNullOrEmpty(context.Request.RedirectUri)) { - principal.SetClaim(Claims.Private.OriginalRedirectUri, context.Request.RedirectUri); + principal.SetClaim(Claims.Private.RedirectUri, context.Request.RedirectUri); } // Attach the code challenge and the code challenge methods to allow the ValidateCodeVerifier @@ -521,7 +522,7 @@ namespace OpenIddict.Server // the token endpoint as part of the JWT identity token. if (!string.IsNullOrEmpty(context.Request.Nonce)) { - principal.SetClaim(Claims.Nonce, context.Request.Nonce); + principal.SetClaim(Claims.Private.Nonce, context.Request.Nonce); } var notification = new SerializeAuthorizationCodeContext(context.Transaction) @@ -701,7 +702,7 @@ namespace OpenIddict.Server else if (context.EndpointType == OpenIddictServerEndpointType.Token) { - var nonce = context.Principal.GetClaim(Claims.Nonce); + var nonce = context.Principal.GetClaim(Claims.Private.Nonce); if (!string.IsNullOrEmpty(nonce)) { principal.SetClaim(Claims.Nonce, nonce); diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index 777ffeb6..666e9744 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -160,8 +160,8 @@ namespace OpenIddict.Server /// /// Gets or sets a boolean determining whether client identification is optional. - /// Enabling this option allows client applications to communicate with the token - /// and revocation endpoints without having to send their client identifier. + /// Enabling this option allows client applications to communicate with the token, + /// introspection and revocation endpoints without having to send their client identifier. /// public bool AcceptAnonymousClients { get; set; }