diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs index 66439e25..46656cc9 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs @@ -304,6 +304,15 @@ namespace OpenIddict.Abstractions /// true if the token has the specified status, false otherwise. ValueTask HasStatusAsync([NotNull] object token, [NotNull] string status, CancellationToken cancellationToken = default); + /// + /// Determines whether a given token has the specified type. + /// + /// The token. + /// The expected type. + /// The that can be used to abort the operation. + /// true if the token has the specified type, false otherwise. + ValueTask HasTypeAsync([NotNull] object token, [NotNull] string type, CancellationToken cancellationToken = default); + /// /// Executes the specified query and returns all the corresponding elements. /// diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs index e53595fb..5b1b731e 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs @@ -1299,8 +1299,7 @@ namespace OpenIddict.Abstractions => principal.GetClaim(Claims.Private.TokenUsage); /// - /// Gets a boolean value indicating whether the - /// claims principal corresponds to an access token. + /// 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. @@ -1315,8 +1314,7 @@ namespace OpenIddict.Abstractions } /// - /// Gets a boolean value indicating whether the - /// claims principal corresponds to an access token. + /// 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. @@ -1331,8 +1329,22 @@ namespace OpenIddict.Abstractions } /// - /// Gets a boolean value indicating whether the - /// claims principal corresponds to an identity token. + /// Gets a boolean value indicating whether the claims principal corresponds to a device code. + /// + /// The claims principal. + /// true if the principal corresponds to a device code. + public static bool IsDeviceCode([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return string.Equals(principal.GetTokenUsage(), TokenUsages.DeviceCode, 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. @@ -1347,8 +1359,7 @@ namespace OpenIddict.Abstractions } /// - /// Gets a boolean value indicating whether the - /// claims principal corresponds to a refresh token. + /// 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. @@ -1362,6 +1373,21 @@ namespace OpenIddict.Abstractions return string.Equals(principal.GetTokenUsage(), TokenUsages.RefreshToken, StringComparison.OrdinalIgnoreCase); } + /// + /// Gets a boolean value indicating whether the claims principal corresponds to a user code. + /// + /// The claims principal. + /// true if the principal corresponds to a user code. + public static bool IsUserCode([NotNull] this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + return string.Equals(principal.GetTokenUsage(), TokenUsages.UserCode, StringComparison.OrdinalIgnoreCase); + } + /// /// Determines whether the claims principal contains at least one audience. /// diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index 60217fa5..2b177700 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -757,6 +757,28 @@ namespace OpenIddict.Core return string.Equals(await Store.GetStatusAsync(token, cancellationToken), status, StringComparison.OrdinalIgnoreCase); } + /// + /// Determines whether a given token has the specified type. + /// + /// The token. + /// The expected type. + /// The that can be used to abort the operation. + /// true if the token has the specified type, false otherwise. + public virtual async ValueTask HasTypeAsync([NotNull] TToken token, [NotNull] string type, CancellationToken cancellationToken = default) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The type cannot be null or empty.", nameof(type)); + } + + return string.Equals(await Store.GetTypeAsync(token, cancellationToken), type, StringComparison.OrdinalIgnoreCase); + } + /// /// Executes the specified query and returns all the corresponding elements. /// @@ -1366,6 +1388,9 @@ namespace OpenIddict.Core ValueTask IOpenIddictTokenManager.HasStatusAsync(object token, string status, CancellationToken cancellationToken) => HasStatusAsync((TToken) token, status, cancellationToken); + ValueTask IOpenIddictTokenManager.HasTypeAsync(object token, string type, CancellationToken cancellationToken) + => HasTypeAsync((TToken) token, type, cancellationToken); + IAsyncEnumerable IOpenIddictTokenManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken) => ListAsync(count, offset, cancellationToken).OfType(); diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs index 2caa348a..cbbf2d30 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs @@ -17,7 +17,6 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; -using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants; using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Purposes; using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters; using static OpenIddict.Server.OpenIddictServerEvents; @@ -87,9 +86,11 @@ namespace OpenIddict.Server.DataProtection // If the token cannot be validated, don't return an error to allow another handle to validate it. var principal = !string.IsNullOrEmpty(context.TokenType) ? ValidateToken(context.Token, context.TokenType) : - ValidateToken(context.Token, TokenUsages.AccessToken) ?? - ValidateToken(context.Token, TokenUsages.RefreshToken) ?? - ValidateToken(context.Token, TokenUsages.AuthorizationCode); + ValidateToken(context.Token, TokenUsages.AccessToken) ?? + ValidateToken(context.Token, TokenUsages.RefreshToken) ?? + ValidateToken(context.Token, TokenUsages.AuthorizationCode) ?? + ValidateToken(context.Token, TokenUsages.DeviceCode) ?? + ValidateToken(context.Token, TokenUsages.UserCode); if (principal == null) { return default; @@ -120,7 +121,7 @@ namespace OpenIddict.Server.DataProtection => new[] { Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server }, TokenUsages.UserCode when !context.Options.DisableTokenStorage - => new[] { Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server, }, + => new[] { Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server }, TokenUsages.AccessToken => new[] { Handlers.Server, Formats.AccessToken, Schemes.Server }, TokenUsages.AuthorizationCode => new[] { Handlers.Server, Formats.AuthorizationCode, Schemes.Server }, diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs index b97b9a09..afa2902f 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs @@ -48,6 +48,7 @@ namespace OpenIddict.Server ValidateClientSecret.Descriptor, ValidateEndpointPermissions.Descriptor, ValidateToken.Descriptor, + ValidateTokenType.Descriptor, ValidateAuthorizedParty.Descriptor, /* @@ -808,6 +809,51 @@ namespace OpenIddict.Server } } + /// + /// Contains the logic responsible of rejecting introspection requests that specify an unsupported token. + /// + public class ValidateTokenType : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .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)); + } + + if (!context.Principal.IsAccessToken() && !context.Principal.IsAuthorizationCode() && + !context.Principal.IsIdentityToken() && !context.Principal.IsRefreshToken()) + { + context.Logger.LogError("The introspection request was rejected because " + + "the received token was of an unsupported type."); + + context.Reject( + error: Errors.UnsupportedTokenType, + description: "The specified token cannot be introspected."); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible of rejecting introspection requests that specify a token /// that cannot be introspected by the client application sending the introspection requests. @@ -824,7 +870,7 @@ namespace OpenIddict.Server // In this case, the returned claims are limited by AttachApplicationClaims to limit exposure. .AddFilter() .UseSingletonHandler() - .SetOrder(ValidateToken.Descriptor.Order + 1_000) + .SetOrder(ValidateTokenType.Descriptor.Order + 1_000) .Build(); /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs index ce0e4e1f..16894b5b 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs @@ -41,6 +41,7 @@ namespace OpenIddict.Server ValidateClientSecret.Descriptor, ValidateEndpointPermissions.Descriptor, ValidateToken.Descriptor, + ValidateTokenType.Descriptor, ValidateAuthorizedParty.Descriptor, /* @@ -754,6 +755,64 @@ namespace OpenIddict.Server } } + /// + /// Contains the logic responsible of rejecting revocation requests that specify an unsupported token. + /// + public class ValidateTokenType : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .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] ValidateRevocationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!context.Principal.IsAccessToken() && + !context.Principal.IsAuthorizationCode() && + !context.Principal.IsRefreshToken()) + { + context.Logger.LogError("The revocation request was rejected because " + + "the received token was of an unsupported type."); + + context.Reject( + error: Errors.UnsupportedTokenType, + description: "This token cannot be revoked."); + + return default; + } + + // If the received token is an access token, return an error if reference tokens are not enabled. + if (context.Principal.IsAccessToken() && !context.Options.UseReferenceAccessTokens) + { + context.Logger.LogError("The revocation request was rejected because the access token was not revocable."); + + context.Reject( + error: Errors.UnsupportedTokenType, + description: "The specified token cannot be revoked."); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible of rejecting revocation requests that specify a token /// that cannot be revoked by the client application sending the revocation requests. @@ -770,7 +829,7 @@ namespace OpenIddict.Server // In this case, the risk is quite limited as claims are never returned by this endpoint. .AddFilter() .UseSingletonHandler() - .SetOrder(ValidateToken.Descriptor.Order + 1_000) + .SetOrder(ValidateTokenType.Descriptor.Order + 1_000) .Build(); /// @@ -949,31 +1008,6 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - // If the received token is not an authorization code or a refresh token, - // return an error to indicate that the token cannot be revoked. - if (context.Principal.IsIdentityToken()) - { - context.Logger.LogError("The revocation request was rejected because identity tokens are not revocable."); - - context.Reject( - error: Errors.UnsupportedTokenType, - description: "The specified token cannot be revoked."); - - return; - } - - // If the received token is an access token, return an error if reference tokens are not enabled. - if (context.Principal.IsAccessToken() && !context.Options.UseReferenceAccessTokens) - { - context.Logger.LogError("The revocation request was rejected because the access token was not revocable."); - - context.Reject( - error: Errors.UnsupportedTokenType, - description: "The specified token cannot be revoked."); - - return; - } - // Extract the token identifier from the authentication principal. var identifier = context.Principal.GetInternalTokenId(); if (string.IsNullOrEmpty(identifier)) diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 50809be8..e0385d36 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -188,6 +188,8 @@ namespace OpenIddict.Server OpenIddictServerEndpointType.Authorization => (context.Request.IdTokenHint, TokenUsages.IdToken), OpenIddictServerEndpointType.Logout => (context.Request.IdTokenHint, TokenUsages.IdToken), + // Generic tokens received by the introspection and revocation can be of any type. + // Additional token type filtering is made by the endpoint themselves, if needed. OpenIddictServerEndpointType.Introspection => (context.Request.Token, null), OpenIddictServerEndpointType.Revocation => (context.Request.Token, null), @@ -334,6 +336,7 @@ namespace OpenIddict.Server return; } + // If the type associated with the token entry doesn't match the expected type, return an error. if (!string.IsNullOrEmpty(context.TokenType) && !string.Equals(context.TokenType, await _tokenManager.GetTypeAsync(token))) { @@ -416,41 +419,18 @@ namespace OpenIddict.Server return; } - // If the token cannot be validated, don't return an error to allow another handle to validate it. - var result = !string.IsNullOrEmpty(context.TokenType) ? - await ValidateTokenAsync(context.Token, context.TokenType) : - await ValidateAnyTokenAsync(context.Token); - if (result.ClaimsIdentity == null) - { - return; - } + var parameters = context.Options.TokenValidationParameters.Clone(); + parameters.ValidIssuer = context.Issuer?.AbsoluteUri; + parameters.IssuerSigningKeys = context.Options.SigningCredentials.Select(credentials => credentials.Key); + parameters.TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key); - // Attach the principal extracted from the token to the parent event context. - context.Principal = new ClaimsPrincipal(result.ClaimsIdentity); - - // Store the token type as a special private claim. - context.Principal.SetClaim(Claims.Private.TokenUsage, ((JsonWebToken) result.SecurityToken).Typ switch + // If a specific token type is expected, override the default valid types to reject + // security tokens whose "typ" header doesn't match the expected token type. + if (!string.IsNullOrEmpty(context.TokenType)) { - JsonWebTokenTypes.AccessToken => TokenUsages.AccessToken, - JsonWebTokenTypes.IdentityToken => TokenUsages.IdToken, - - JsonWebTokenTypes.Private.AuthorizationCode => TokenUsages.AuthorizationCode, - JsonWebTokenTypes.Private.DeviceCode => TokenUsages.DeviceCode, - JsonWebTokenTypes.Private.RefreshToken => TokenUsages.RefreshToken, - JsonWebTokenTypes.Private.UserCode => TokenUsages.UserCode, - - _ => throw new InvalidOperationException("The token type is not supported.") - }); - - context.Logger.LogTrace("The token '{Token}' was successfully validated and the following claims " + - "could be extracted: {Claims}.", context.Token, context.Principal.Claims); - - async ValueTask ValidateTokenAsync(string token, string type) - { - var parameters = context.Options.TokenValidationParameters.Clone(); parameters.ValidTypes = new[] { - type switch + context.TokenType switch { TokenUsages.AccessToken => JsonWebTokenTypes.AccessToken, TokenUsages.IdToken => JsonWebTokenTypes.IdentityToken, @@ -463,74 +443,36 @@ namespace OpenIddict.Server _ => throw new InvalidOperationException("The token type is not supported.") } }; - parameters.ValidIssuer = context.Issuer?.AbsoluteUri; - - parameters.IssuerSigningKeys = type switch - { - TokenUsages.AccessToken => context.Options.SigningCredentials.Select(credentials => credentials.Key), - TokenUsages.AuthorizationCode => context.Options.SigningCredentials.Select(credentials => credentials.Key), - TokenUsages.DeviceCode => context.Options.SigningCredentials.Select(credentials => credentials.Key), - TokenUsages.RefreshToken => context.Options.SigningCredentials.Select(credentials => credentials.Key), - TokenUsages.UserCode => context.Options.SigningCredentials.Select(credentials => credentials.Key), - - TokenUsages.IdToken => context.Options.SigningCredentials - .Select(credentials => credentials.Key) - .OfType(), - - _ => Array.Empty() - }; - - parameters.TokenDecryptionKeys = type switch - { - TokenUsages.AuthorizationCode => context.Options.EncryptionCredentials.Select(credentials => credentials.Key), - TokenUsages.DeviceCode => context.Options.EncryptionCredentials.Select(credentials => credentials.Key), - TokenUsages.RefreshToken => context.Options.EncryptionCredentials.Select(credentials => credentials.Key), - TokenUsages.UserCode => context.Options.EncryptionCredentials.Select(credentials => credentials.Key), - - TokenUsages.AccessToken => context.Options.EncryptionCredentials - .Select(credentials => credentials.Key) - .Where(key => key is SymmetricSecurityKey), - - _ => Array.Empty() - }; + } - var result = await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(token, parameters); - if (!result.IsValid) - { - context.Logger.LogTrace(result.Exception, "An error occurred while validating the token '{Token}'.", token); - } + // If the token cannot be validated, don't return an error to allow another handle to validate it. + var result = await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(context.Token, parameters); + if (result.ClaimsIdentity == null || !result.IsValid) + { + context.Logger.LogTrace(result.Exception, "An error occurred while validating the token '{Token}'.", context.Token); - return result; + return; } - async ValueTask ValidateAnyTokenAsync(string token) - { - var result = await ValidateTokenAsync(token, TokenUsages.AccessToken); - if (result.IsValid) - { - return result; - } + // Attach the principal extracted from the token to the parent event context. + context.Principal = new ClaimsPrincipal(result.ClaimsIdentity); - result = await ValidateTokenAsync(token, TokenUsages.RefreshToken); - if (result.IsValid) - { - return result; - } + // Store the token type as a special private claim. + context.Principal.SetClaim(Claims.Private.TokenUsage, ((JsonWebToken) result.SecurityToken).Typ switch + { + JsonWebTokenTypes.AccessToken => TokenUsages.AccessToken, + JsonWebTokenTypes.IdentityToken => TokenUsages.IdToken, - result = await ValidateTokenAsync(token, TokenUsages.AuthorizationCode); - if (result.IsValid) - { - return result; - } + JsonWebTokenTypes.Private.AuthorizationCode => TokenUsages.AuthorizationCode, + JsonWebTokenTypes.Private.DeviceCode => TokenUsages.DeviceCode, + JsonWebTokenTypes.Private.RefreshToken => TokenUsages.RefreshToken, + JsonWebTokenTypes.Private.UserCode => TokenUsages.UserCode, - result = await ValidateTokenAsync(token, TokenUsages.IdToken); - if (result.IsValid) - { - return result; - } + _ => throw new InvalidOperationException("The token type is not supported.") + }); - return new TokenValidationResult { IsValid = false }; - } + context.Logger.LogTrace("The token '{Token}' was successfully validated and the following claims " + + "could be extracted: {Claims}.", context.Token, context.Principal.Claims); } } diff --git a/src/OpenIddict.Server/OpenIddictServerJsonWebTokenHandler.cs b/src/OpenIddict.Server/OpenIddictServerJsonWebTokenHandler.cs index 7dbeedeb..11efcf82 100644 --- a/src/OpenIddict.Server/OpenIddictServerJsonWebTokenHandler.cs +++ b/src/OpenIddict.Server/OpenIddictServerJsonWebTokenHandler.cs @@ -73,11 +73,6 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(parameters)); } - if (parameters.ValidTypes == null || !parameters.ValidTypes.Any()) - { - throw new InvalidOperationException("The valid token types collection cannot be empty."); - } - if (!CanReadToken(token)) { return new ValueTask(new TokenValidationResult diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index c884f19d..8eebd539 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.ComponentModel; using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; namespace OpenIddict.Server { @@ -111,7 +112,17 @@ namespace OpenIddict.Server RoleClaimType = OpenIddictConstants.Claims.Role, // Note: audience and lifetime are manually validated by OpenIddict itself. ValidateAudience = false, - ValidateLifetime = false + ValidateLifetime = false, + // Note: valid types can be overriden by OpenIddict depending on the received request. + ValidTypes = new[] + { + JsonWebTokenTypes.AccessToken, + JsonWebTokenTypes.IdentityToken, + JsonWebTokenTypes.Private.AuthorizationCode, + JsonWebTokenTypes.Private.DeviceCode, + JsonWebTokenTypes.Private.RefreshToken, + JsonWebTokenTypes.Private.UserCode + } }; /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index 628dfa73..80cc6b9a 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs @@ -212,7 +212,7 @@ namespace OpenIddict.Validation // If the token cannot be validated, don't return an error to allow another handle to validate it. var result = await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(context.Token, parameters); - if (result.ClaimsIdentity == null) + if (result.ClaimsIdentity == null || !result.IsValid) { context.Logger.LogTrace(result.Exception, "An error occurred while validating the token '{Token}'.", context.Token); diff --git a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs index 601fccbd..d0e71701 100644 --- a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs @@ -2106,8 +2106,10 @@ namespace OpenIddict.Abstractions.Tests.Primitives [InlineData("unknown", false)] [InlineData(OpenIddictConstants.TokenUsages.AccessToken, true)] [InlineData(OpenIddictConstants.TokenUsages.AuthorizationCode, false)] + [InlineData(OpenIddictConstants.TokenUsages.DeviceCode, false)] [InlineData(OpenIddictConstants.TokenUsages.IdToken, false)] [InlineData(OpenIddictConstants.TokenUsages.RefreshToken, false)] + [InlineData(OpenIddictConstants.TokenUsages.UserCode, false)] public void IsAccessToken_ReturnsExpectedResult(string usage, bool result) { // Arrange @@ -2137,8 +2139,10 @@ namespace OpenIddict.Abstractions.Tests.Primitives [InlineData("unknown", false)] [InlineData(OpenIddictConstants.TokenUsages.AccessToken, false)] [InlineData(OpenIddictConstants.TokenUsages.AuthorizationCode, true)] + [InlineData(OpenIddictConstants.TokenUsages.DeviceCode, false)] [InlineData(OpenIddictConstants.TokenUsages.IdToken, false)] [InlineData(OpenIddictConstants.TokenUsages.RefreshToken, false)] + [InlineData(OpenIddictConstants.TokenUsages.UserCode, false)] public void IsAuthorizationCode_ReturnsExpectedResult(string usage, bool result) { // Arrange @@ -2151,6 +2155,39 @@ namespace OpenIddict.Abstractions.Tests.Primitives Assert.Equal(result, principal.IsAuthorizationCode()); } + [Fact] + public void IsDeviceCode_ThrowsAnExceptionForNullPrincipal() + { + // Arrange + var principal = (ClaimsPrincipal) null; + + // Act and assert + var exception = Assert.Throws(() => principal.IsDeviceCode()); + + Assert.Equal("principal", exception.ParamName); + } + + [Theory] + [InlineData(null, false)] + [InlineData("unknown", false)] + [InlineData(OpenIddictConstants.TokenUsages.AccessToken, false)] + [InlineData(OpenIddictConstants.TokenUsages.AuthorizationCode, false)] + [InlineData(OpenIddictConstants.TokenUsages.DeviceCode, true)] + [InlineData(OpenIddictConstants.TokenUsages.IdToken, false)] + [InlineData(OpenIddictConstants.TokenUsages.RefreshToken, false)] + [InlineData(OpenIddictConstants.TokenUsages.UserCode, false)] + public void IsDeviceCode_ReturnsExpectedResult(string usage, bool result) + { + // Arrange + var identity = new ClaimsIdentity(); + var principal = new ClaimsPrincipal(identity); + + principal.SetClaim(OpenIddictConstants.Claims.Private.TokenUsage, usage); + + // Act and assert + Assert.Equal(result, principal.IsDeviceCode()); + } + [Fact] public void IsIdentityToken_ThrowsAnExceptionForNullPrincipal() { @@ -2168,8 +2205,10 @@ namespace OpenIddict.Abstractions.Tests.Primitives [InlineData("unknown", false)] [InlineData(OpenIddictConstants.TokenUsages.AccessToken, false)] [InlineData(OpenIddictConstants.TokenUsages.AuthorizationCode, false)] + [InlineData(OpenIddictConstants.TokenUsages.DeviceCode, false)] [InlineData(OpenIddictConstants.TokenUsages.IdToken, true)] [InlineData(OpenIddictConstants.TokenUsages.RefreshToken, false)] + [InlineData(OpenIddictConstants.TokenUsages.UserCode, false)] public void IsIdentityToken_ReturnsExpectedResult(string usage, bool result) { // Arrange @@ -2213,6 +2252,39 @@ namespace OpenIddict.Abstractions.Tests.Primitives Assert.Equal(result, principal.IsRefreshToken()); } + [Fact] + public void IsUserCode_ThrowsAnExceptionForNullPrincipal() + { + // Arrange + var principal = (ClaimsPrincipal) null; + + // Act and assert + var exception = Assert.Throws(() => principal.IsUserCode()); + + Assert.Equal("principal", exception.ParamName); + } + + [Theory] + [InlineData(null, false)] + [InlineData("unknown", false)] + [InlineData(OpenIddictConstants.TokenUsages.AccessToken, false)] + [InlineData(OpenIddictConstants.TokenUsages.AuthorizationCode, false)] + [InlineData(OpenIddictConstants.TokenUsages.DeviceCode, false)] + [InlineData(OpenIddictConstants.TokenUsages.IdToken, false)] + [InlineData(OpenIddictConstants.TokenUsages.RefreshToken, false)] + [InlineData(OpenIddictConstants.TokenUsages.UserCode, true)] + public void IsUserCode_ReturnsExpectedResult(string usage, bool result) + { + // Arrange + var identity = new ClaimsIdentity(); + var principal = new ClaimsPrincipal(identity); + + principal.SetClaim(OpenIddictConstants.Claims.Private.TokenUsage, usage); + + // Act and assert + Assert.Equal(result, principal.IsUserCode()); + } + [Theory] [InlineData(null)] [InlineData("")]