diff --git a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs b/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs
index ea14d1c8..58bdf13d 100644
--- a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs
+++ b/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs
@@ -219,14 +219,16 @@ public class InteractiveService : BackgroundService
.LeftAligned()
.AddColumn("Claim type")
.AddColumn("Claim value type")
- .AddColumn("Claim value");
+ .AddColumn("Claim value")
+ .AddColumn("Claim issuer");
foreach (var claim in principal.Claims)
{
table.AddRow(
claim.Type.EscapeMarkup(),
claim.ValueType.EscapeMarkup(),
- claim.Value.EscapeMarkup());
+ claim.Value.EscapeMarkup(),
+ claim.Issuer.EscapeMarkup());
}
return table;
diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx
index 14f8cb3a..6ec10518 100644
--- a/src/OpenIddict.Abstractions/OpenIddictResources.resx
+++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx
@@ -2154,6 +2154,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
The revocation request was rejected by the remote server.
+
+ The introspection response indicates the token is no longer valid.
+
The '{0}' parameter shouldn't be null or empty at this point.
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs
index 633c4169..1728c972 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs
@@ -6,6 +6,7 @@
using System.Collections.Immutable;
using System.Diagnostics;
+using System.Globalization;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.Logging;
@@ -24,8 +25,10 @@ public static partial class OpenIddictClientHandlers
HandleErrorResponse.Descriptor,
HandleInactiveResponse.Descriptor,
ValidateIssuer.Descriptor,
+ ValidateExpirationDate.Descriptor,
ValidateTokenUsage.Descriptor,
- PopulateClaims.Descriptor
+ PopulateClaims.Descriptor,
+ MapInternalClaims.Descriptor
];
///
@@ -217,7 +220,7 @@ public static partial class OpenIddictClientHandlers
}
///
- /// Contains the logic responsible for extracting the issuer from the introspection response.
+ /// Contains the logic responsible for extracting and validating the issuer from the introspection response.
///
public sealed class ValidateIssuer : IOpenIddictClientHandler
{
@@ -270,6 +273,53 @@ public static partial class OpenIddictClientHandlers
}
}
+ ///
+ /// Contains the logic responsible for extracting and validating the expiration date from the introspection response.
+ ///
+ public sealed class ValidateExpirationDate : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidateIssuer.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(HandleIntrospectionResponseContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Note: in most cases, an expired token should lead to an errored or "active=false" response
+ // being returned by the authorization server. Unfortunately, some implementations are known not
+ // to check the expiration date of the introspected token before returning a positive response.
+ //
+ // To ensure expired tokens are rejected, a manual check is performed here if the
+ // expiration date was returned as a dedicated claim by the remote authorization server.
+
+ if (long.TryParse((string?) context.Response[Claims.ExpiresAt],
+ NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) &&
+ DateTimeOffset.FromUnixTimeSeconds(value) is DateTimeOffset date &&
+ date.Add(context.Registration.TokenValidationParameters.ClockSkew) < DateTimeOffset.UtcNow)
+ {
+ context.Reject(
+ error: Errors.ServerError,
+ description: SR.GetResourceString(SR.ID2176),
+ uri: SR.FormatID8000(SR.ID2176));
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
///
/// Contains the logic responsible for extracting and validating the token usage from the introspection response.
///
@@ -281,7 +331,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
.UseSingletonHandler()
- .SetOrder(ValidateIssuer.Descriptor.Order + 1_000)
+ .SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@@ -402,5 +452,72 @@ public static partial class OpenIddictClientHandlers
return default;
}
}
+
+ ///
+ /// Contains the logic responsible for mapping the standard claims to their internal/OpenIddict-specific equivalent.
+ ///
+ public sealed class MapInternalClaims : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(PopulateClaims.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(HandleIntrospectionResponseContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+
+ // Map the internal "oi_crt_dt" claim from the standard "iat" claim, if available.
+ context.Principal.SetCreationDate(context.Principal.GetClaim(Claims.IssuedAt) switch
+ {
+ string date when long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
+ => DateTimeOffset.FromUnixTimeSeconds(value),
+
+ _ => null
+ });
+
+ // Map the internal "oi_exp_dt" claim from the standard "exp" claim, if available.
+ context.Principal.SetExpirationDate(context.Principal.GetClaim(Claims.ExpiresAt) switch
+ {
+ string date when long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
+ => DateTimeOffset.FromUnixTimeSeconds(value),
+
+ _ => null
+ });
+
+ // Map the internal "oi_aud" claims from the standard "aud" claims, if available.
+ context.Principal.SetAudiences(context.Principal.GetClaims(Claims.Audience));
+
+ // Map the internal "oi_prst" claims from the standard "client_id" claim, if available.
+ context.Principal.SetPresenters(context.Principal.GetClaim(Claims.ClientId) switch
+ {
+ string identifier when !string.IsNullOrEmpty(identifier)
+ => ImmutableArray.Create(identifier),
+
+ _ => []
+ });
+
+ // Map the internal "oi_scp" claims from the standard, space-separated "scope" claim, if available.
+ context.Principal.SetScopes(context.Principal.GetClaim(Claims.Scope) switch
+ {
+ string scope => scope.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries).ToImmutableArray(),
+
+ _ => []
+ });
+
+ return default;
+ }
+ }
}
}
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
index 18c4477e..80ea70a6 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
@@ -144,7 +144,7 @@ public static partial class OpenIddictClientHandlers
GenerateIntrospectionClientAssertion.Descriptor,
AttachIntrospectionRequestClientCredentials.Descriptor,
SendIntrospectionRequest.Descriptor,
- MapIntrospectionParametersToWebServicesFederationClaims.Descriptor,
+ MapIntrospectionClaimsToWebServicesFederationClaims.Descriptor,
/*
* Revocation processing:
@@ -656,7 +656,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- Principal = context.StateTokenPrincipal,
Token = context.StateToken,
ValidTokenTypes = { TokenTypeHints.StateToken }
};
@@ -1587,7 +1586,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- Principal = context.FrontchannelIdentityTokenPrincipal,
Token = context.FrontchannelIdentityToken,
ValidTokenTypes = { TokenTypeHints.IdToken }
};
@@ -2102,7 +2100,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- Principal = context.FrontchannelAccessTokenPrincipal,
Token = context.FrontchannelAccessToken,
ValidTokenTypes = { TokenTypeHints.AccessToken }
};
@@ -2177,7 +2174,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- Principal = context.AuthorizationCodePrincipal,
Token = context.AuthorizationCode,
ValidTokenTypes = { TokenTypeHints.AuthorizationCode }
};
@@ -2917,7 +2913,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- Principal = context.BackchannelIdentityTokenPrincipal,
Token = context.BackchannelIdentityToken,
ValidTokenTypes = { TokenTypeHints.IdToken }
};
@@ -3396,7 +3391,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- Principal = context.BackchannelAccessTokenPrincipal,
Token = context.BackchannelAccessToken,
ValidTokenTypes = { TokenTypeHints.AccessToken }
};
@@ -3471,7 +3465,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- Principal = context.RefreshTokenPrincipal,
Token = context.RefreshToken,
ValidTokenTypes = { TokenTypeHints.RefreshToken }
};
@@ -3823,7 +3816,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- Principal = context.UserinfoTokenPrincipal,
Token = context.UserinfoToken,
ValidTokenTypes = { TokenTypeHints.UserinfoToken }
};
@@ -4020,9 +4012,9 @@ public static partial class OpenIddictClientHandlers
// Attach the registration identifier and identity of the authorization server to the returned principal to allow
// resolving it even if no other claim was added (e.g if no id_token was returned/no userinfo endpoint is available).
- context.MergedPrincipal.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
+ context.MergedPrincipal.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
.SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
- .SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName);
+ .SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName);
return default;
@@ -6359,6 +6351,7 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
+ Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
Debug.Assert(context.IntrospectionRequest is not null, SR.GetResourceString(SR.ID4008));
// Ensure the introspection endpoint is present and is a valid absolute URI.
@@ -6385,15 +6378,20 @@ public static partial class OpenIddictClientHandlers
return;
}
+ // Attach the registration identifier and identity of the authorization server to the returned principal.
+ context.Principal.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
+ .SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
+ .SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName);
+
context.Logger.LogTrace(SR.GetResourceString(SR.ID6154), context.Token, context.Principal.Claims);
}
}
///
- /// Contains the logic responsible for mapping the introspection parameters
- /// to their WS-Federation claim equivalent, if applicable.
+ /// Contains the logic responsible for mapping the standard claims resolved from the
+ /// introspection response to their WS-Federation claim equivalent, if applicable.
///
- public sealed class MapIntrospectionParametersToWebServicesFederationClaims : IOpenIddictClientHandler
+ public sealed class MapIntrospectionClaimsToWebServicesFederationClaims : IOpenIddictClientHandler
{
///
/// Gets the default descriptor definition assigned to this handler.
@@ -6401,7 +6399,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
.AddFilter()
- .UseSingletonHandler()
+ .UseSingletonHandler()
.SetOrder(SendIntrospectionRequest.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@@ -6414,11 +6412,6 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- if (context.Options.DisableWebServicesFederationClaimMapping)
- {
- return default;
- }
-
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs
index c442e794..1839ed9b 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs
@@ -340,7 +340,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
return default;
}
- context.Logger.LogInformation(SR.GetResourceString(SR.ID6151), context.PostLogoutRedirectUri, response);
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6151), context.PostLogoutRedirectUri, context.Response);
// Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters
// with the same name are used by derived drafts like the OAuth 2.0 token exchange specification.
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs
index 16897ea1..d08229d1 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs
+++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs
@@ -340,7 +340,7 @@ public static partial class OpenIddictServerOwinHandlers
return default;
}
- context.Logger.LogInformation(SR.GetResourceString(SR.ID6151), context.PostLogoutRedirectUri, response);
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6151), context.PostLogoutRedirectUri, context.Response);
var location = context.PostLogoutRedirectUri;
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs
index 9c589d02..e9f092ba 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs
@@ -554,7 +554,6 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- Principal = context.ClientAssertionPrincipal,
Token = context.ClientAssertion,
TokenFormat = context.ClientAssertionType switch
{
@@ -1255,7 +1254,6 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- Principal = context.AccessTokenPrincipal,
Token = context.AccessToken,
ValidTokenTypes = { TokenTypeHints.AccessToken }
};
@@ -1328,7 +1326,6 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- Principal = context.AuthorizationCodePrincipal,
Token = context.AuthorizationCode,
ValidTokenTypes = { TokenTypeHints.AuthorizationCode }
};
@@ -1401,7 +1398,6 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- Principal = context.DeviceCodePrincipal,
Token = context.DeviceCode,
ValidTokenTypes = { TokenTypeHints.DeviceCode }
};
@@ -1474,7 +1470,6 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- Principal = context.GenericTokenPrincipal,
Token = context.GenericToken,
TokenTypeHint = context.GenericTokenTypeHint,
@@ -1568,7 +1563,6 @@ public static partial class OpenIddictServerHandlers
// Don't validate the lifetime of id_tokens used as id_token_hints.
DisableLifetimeValidation = context.EndpointType is OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.Logout,
- Principal = context.IdentityTokenPrincipal,
Token = context.IdentityToken,
ValidTokenTypes = { TokenTypeHints.IdToken }
};
@@ -1641,7 +1635,6 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- Principal = context.RefreshTokenPrincipal,
Token = context.RefreshToken,
ValidTokenTypes = { TokenTypeHints.RefreshToken }
};
@@ -1714,7 +1707,6 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- Principal = context.UserCodePrincipal,
Token = context.UserCode,
ValidTokenTypes = { TokenTypeHints.UserCode }
};
diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
index e13cd398..f70121e9 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
@@ -5,6 +5,8 @@
*/
using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Globalization;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.Logging;
@@ -23,8 +25,10 @@ public static partial class OpenIddictValidationHandlers
HandleErrorResponse.Descriptor,
HandleInactiveResponse.Descriptor,
ValidateIssuer.Descriptor,
+ ValidateExpirationDate.Descriptor,
ValidateTokenUsage.Descriptor,
- PopulateClaims.Descriptor
+ PopulateClaims.Descriptor,
+ MapInternalClaims.Descriptor
];
///
@@ -216,7 +220,7 @@ public static partial class OpenIddictValidationHandlers
}
///
- /// Contains the logic responsible for extracting the issuer from the introspection response.
+ /// Contains the logic responsible for extracting and validating the issuer from the introspection response.
///
public sealed class ValidateIssuer : IOpenIddictValidationHandler
{
@@ -269,6 +273,53 @@ public static partial class OpenIddictValidationHandlers
}
}
+ ///
+ /// Contains the logic responsible for extracting and validating the expiration date from the introspection response.
+ ///
+ public sealed class ValidateExpirationDate : IOpenIddictValidationHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
+ = OpenIddictValidationHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidateIssuer.Descriptor.Order + 1_000)
+ .SetType(OpenIddictValidationHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(HandleIntrospectionResponseContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Note: in most cases, an expired token should lead to an errored or "active=false" response
+ // being returned by the authorization server. Unfortunately, some implementations are known not
+ // to check the expiration date of the introspected token before returning a positive response.
+ //
+ // To ensure expired tokens are rejected, a manual check is performed here if the
+ // expiration date was returned as a dedicated claim by the remote authorization server.
+
+ if (long.TryParse((string?) context.Response[Claims.ExpiresAt],
+ NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) &&
+ DateTimeOffset.FromUnixTimeSeconds(value) is DateTimeOffset date &&
+ date.Add(context.Options.TokenValidationParameters.ClockSkew) < DateTimeOffset.UtcNow)
+ {
+ context.Reject(
+ error: Errors.InvalidToken,
+ description: SR.GetResourceString(SR.ID2176),
+ uri: SR.FormatID8000(SR.ID2176));
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
///
/// Contains the logic responsible for extracting and validating the token usage from the introspection response.
///
@@ -280,7 +331,7 @@ public static partial class OpenIddictValidationHandlers
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder()
.UseSingletonHandler()
- .SetOrder(ValidateIssuer.Descriptor.Order + 1_000)
+ .SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
@@ -403,5 +454,72 @@ public static partial class OpenIddictValidationHandlers
return default;
}
}
+
+ ///
+ /// Contains the logic responsible for mapping the standard claims to their internal/OpenIddict-specific equivalent.
+ ///
+ public sealed class MapInternalClaims : IOpenIddictValidationHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
+ = OpenIddictValidationHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(PopulateClaims.Descriptor.Order + 1_000)
+ .SetType(OpenIddictValidationHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(HandleIntrospectionResponseContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+
+ // Map the internal "oi_crt_dt" claim from the standard "iat" claim, if available.
+ context.Principal.SetCreationDate(context.Principal.GetClaim(Claims.IssuedAt) switch
+ {
+ string date when long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
+ => DateTimeOffset.FromUnixTimeSeconds(value),
+
+ _ => null
+ });
+
+ // Map the internal "oi_exp_dt" claim from the standard "exp" claim, if available.
+ context.Principal.SetExpirationDate(context.Principal.GetClaim(Claims.ExpiresAt) switch
+ {
+ string date when long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
+ => DateTimeOffset.FromUnixTimeSeconds(value),
+
+ _ => null
+ });
+
+ // Map the internal "oi_aud" claims from the standard "aud" claims, if available.
+ context.Principal.SetAudiences(context.Principal.GetClaims(Claims.Audience));
+
+ // Map the internal "oi_prst" claims from the standard "client_id" claim, if available.
+ context.Principal.SetPresenters(context.Principal.GetClaim(Claims.ClientId) switch
+ {
+ string identifier when !string.IsNullOrEmpty(identifier)
+ => ImmutableArray.Create(identifier),
+
+ _ => []
+ });
+
+ // Map the internal "oi_scp" claims from the standard, space-separated "scope" claim, if available.
+ context.Principal.SetScopes(context.Principal.GetClaim(Claims.Scope) switch
+ {
+ string scope => scope.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries).ToImmutableArray(),
+
+ _ => []
+ });
+
+ return default;
+ }
+ }
}
}
diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
index 59548754..ad6df128 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
@@ -149,7 +149,6 @@ public static partial class OpenIddictValidationHandlers
///
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder()
- .AddFilter()
.AddFilter()
.UseScopedHandler()
.SetOrder(ResolveTokenValidationParameters.Descriptor.Order + 1_000)
@@ -219,7 +218,6 @@ public static partial class OpenIddictValidationHandlers
///
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder()
- .AddFilter()
.UseSingletonHandler()
.SetOrder(ValidateReferenceTokenIdentifier.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
@@ -470,7 +468,6 @@ public static partial class OpenIddictValidationHandlers
///
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder()
- .AddFilter()
.AddFilter()
.UseScopedHandler()
.SetOrder(MapInternalClaims.Descriptor.Order + 1_000)
@@ -699,7 +696,6 @@ public static partial class OpenIddictValidationHandlers
///
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder()
- .AddFilter()
.AddFilter()
.AddFilter()
.UseScopedHandler()
@@ -754,7 +750,6 @@ public static partial class OpenIddictValidationHandlers
///
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder()
- .AddFilter()
.AddFilter()
.AddFilter()
.UseScopedHandler()
diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
index 77d91a43..40677ec3 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
@@ -34,6 +34,7 @@ public static partial class OpenIddictValidationHandlers
AttachIntrospectionRequestClientCredentials.Descriptor,
SendIntrospectionRequest.Descriptor,
ValidateIntrospectedTokenUsage.Descriptor,
+ ValidateIntrospectedTokenAudiences.Descriptor,
ValidateAccessToken.Descriptor,
/*
@@ -81,8 +82,12 @@ public static partial class OpenIddictValidationHandlers
context.ValidateAccessToken,
context.RejectAccessToken) = context.EndpointType switch
{
- // The validation handler is responsible for validating access tokens for endpoints
- // it doesn't manage (typically, API endpoints using token authentication).
+ // When introspection is used, ask the server to validate the token.
+ OpenIddictValidationEndpointType.Unknown
+ when context.Options.ValidationType is OpenIddictValidationType.Introspection
+ => (true, true, false, true),
+
+ // Otherwise, always validate it locally.
OpenIddictValidationEndpointType.Unknown => (true, true, true, true),
_ => (false, false, false, false)
@@ -261,6 +266,7 @@ public static partial class OpenIddictValidationHandlers
.AddFilter()
.UseSingletonHandler()
.SetOrder(EvaluateIntrospectionRequest.Descriptor.Order + 1_000)
+ .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
///
@@ -467,6 +473,7 @@ public static partial class OpenIddictValidationHandlers
.AddFilter()
.UseSingletonHandler()
.SetOrder(GenerateClientAssertion.Descriptor.Order + 1_000)
+ .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
///
@@ -520,6 +527,7 @@ public static partial class OpenIddictValidationHandlers
.AddFilter()
.UseSingletonHandler()
.SetOrder(AttachIntrospectionRequestClientCredentials.Descriptor.Order + 1_000)
+ .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
///
@@ -576,6 +584,7 @@ public static partial class OpenIddictValidationHandlers
.AddFilter()
.UseSingletonHandler()
.SetOrder(SendIntrospectionRequest.Descriptor.Order + 1_000)
+ .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
///
@@ -612,6 +621,75 @@ public static partial class OpenIddictValidationHandlers
}
}
+ ///
+ /// Contains the logic responsible for validating the audiences of the introspected token returned by the server, if applicable.
+ ///
+ public sealed class ValidateIntrospectedTokenAudiences : IOpenIddictValidationHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
+ = OpenIddictValidationHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(ValidateIntrospectedTokenUsage.Descriptor.Order + 1_000)
+ .SetType(OpenIddictValidationHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(context.AccessTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+
+ // In theory, authorization servers are expected to return an error (or an active=false response)
+ // when the caller is not allowed to introspect the token (e.g because it's not a valid audience
+ // or authorized party). Unfortunately, some servers are known to have a relaxed validation policy.
+ //
+ // To ensure the token can be used with this resource server, a second pass is manually performed here.
+
+ // If no explicit audience has been configured, skip the audience validation.
+ if (context.Options.Audiences.Count is 0)
+ {
+ return default;
+ }
+
+ // If the access token doesn't have any audience attached, return an error.
+ var audiences = context.AccessTokenPrincipal.GetAudiences();
+ if (audiences.IsDefaultOrEmpty)
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6157));
+
+ context.Reject(
+ error: Errors.InvalidToken,
+ description: SR.GetResourceString(SR.ID2093),
+ uri: SR.FormatID8000(SR.ID2093));
+
+ return default;
+ }
+
+ // If the access token doesn't include any registered audience, return an error.
+ if (!audiences.Intersect(context.Options.Audiences, StringComparer.Ordinal).Any())
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6158));
+
+ context.Reject(
+ error: Errors.InvalidToken,
+ description: SR.GetResourceString(SR.ID2094),
+ uri: SR.FormatID8000(SR.ID2094));
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
///
/// Contains the logic responsible for ensuring a token was correctly resolved from the context.
///
@@ -648,9 +726,6 @@ public static partial class OpenIddictValidationHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
- // When using introspection, the principal is already available as it is extracted
- // from the introspection response returned by the authorization server.
- Principal = context.AccessTokenPrincipal,
Token = context.AccessToken,
ValidTokenTypes = { TokenTypeHints.AccessToken }
};
diff --git a/src/OpenIddict.Validation/OpenIddictValidationService.cs b/src/OpenIddict.Validation/OpenIddictValidationService.cs
index c3f415df..31fed388 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationService.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationService.cs
@@ -8,9 +8,7 @@ using System.Diagnostics;
using System.Security.Claims;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
-using OpenIddict.Extensions;
using static OpenIddict.Abstractions.OpenIddictExceptions;
namespace OpenIddict.Validation;
@@ -53,21 +51,13 @@ public class OpenIddictValidationService
// can be disposed of asynchronously if it implements IAsyncDisposable.
try
{
- var options = _provider.GetRequiredService>();
- var configuration = await options.CurrentValue.ConfigurationManager
- .GetConfigurationAsync(cancellationToken)
- .WaitAsync(cancellationToken) ??
- throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
-
var dispatcher = scope.ServiceProvider.GetRequiredService();
var factory = scope.ServiceProvider.GetRequiredService();
var transaction = await factory.CreateTransactionAsync();
- var context = new ValidateTokenContext(transaction)
+ var context = new ProcessAuthenticationContext(transaction)
{
- Configuration = configuration,
- Token = token,
- ValidTokenTypes = { TokenTypeHints.AccessToken }
+ AccessToken = token
};
await dispatcher.DispatchAsync(context);
@@ -79,9 +69,9 @@ public class OpenIddictValidationService
context.Error, context.ErrorDescription, context.ErrorUri);
}
- Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+ Debug.Assert(context.AccessTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
- return context.Principal;
+ return context.AccessTokenPrincipal;
}
finally