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; }