diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 5541fcaf..52ef7548 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -3312,6 +3312,9 @@ This may indicate that the hashed entry is corrupted or malformed. The revocation response returned by {Uri} was successfully extracted: {Response}. + + The payload of the '{0}' reference token was used instead of its reference identifier, which may indicate that the payload stored in the database has leaked and is being used as a regular token. + https://documentation.openiddict.com/errors/{0} diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs index d2239576..c49c5ced 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs @@ -580,6 +580,21 @@ public static partial class OpenIddictClientHandlers return; } + // If the token was not validated as a reference token but has a reference identifier attached, this + // may indicate that the payload stored in the database has leaked and is being used as a regular, + // non-reference token. To prevent this, reject the token if the reference identifier is not null. + if (!context.IsReferenceToken && !string.IsNullOrEmpty(await _tokenManager.GetReferenceIdAsync(token))) + { + context.Logger.LogWarning(6292, SR.GetResourceString(SR.ID6292), await _tokenManager.GetIdAsync(token)); + + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2019), + uri: SR.FormatID8000(SR.ID2019)); + + return; + } + // Restore the creation/expiration dates/identifiers from the token entry metadata. context.Principal .SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs index 7b704c49..bd229029 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs @@ -817,6 +817,35 @@ public static partial class OpenIddictServerHandlers return; } + // If the token was not validated as a reference token but has a reference identifier attached, this + // may indicate that the payload stored in the database has leaked and is being used as a regular, + // non-reference token. To prevent this, reject the token if the reference identifier is not null. + if (!context.IsReferenceToken && !string.IsNullOrEmpty(await _tokenManager.GetReferenceIdAsync(token))) + { + context.Logger.LogWarning(6292, SR.GetResourceString(SR.ID6292), await _tokenManager.GetIdAsync(token)); + + context.Reject( + error: Errors.InvalidToken, + description: context.Principal.GetTokenType() switch + { + TokenTypeIdentifiers.Private.AuthorizationCode => SR.GetResourceString(SR.ID2001), + TokenTypeIdentifiers.Private.DeviceCode => SR.GetResourceString(SR.ID2002), + TokenTypeIdentifiers.RefreshToken => SR.GetResourceString(SR.ID2003), + + _ => SR.GetResourceString(SR.ID2004) + }, + uri: context.Principal.GetTokenType() switch + { + TokenTypeIdentifiers.Private.AuthorizationCode => SR.FormatID8000(SR.ID2001), + TokenTypeIdentifiers.Private.DeviceCode => SR.FormatID8000(SR.ID2002), + TokenTypeIdentifiers.RefreshToken => SR.FormatID8000(SR.ID2003), + + _ => SR.FormatID8000(SR.ID2004) + }); + + return; + } + // Restore the creation/expiration dates/identifiers from the token entry metadata. context.Principal .SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs index 9edf40d6..0dbf133c 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs @@ -567,6 +567,21 @@ public static partial class OpenIddictValidationHandlers return; } + // If the token was not validated as a reference token but has a reference identifier attached, this + // may indicate that the payload stored in the database has leaked and is being used as a regular, + // non-reference token. To prevent this, reject the token if the reference identifier is not null. + if (!context.IsReferenceToken && !string.IsNullOrEmpty(await _tokenManager.GetReferenceIdAsync(token))) + { + context.Logger.LogWarning(6292, SR.GetResourceString(SR.ID6292), await _tokenManager.GetIdAsync(token)); + + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2019), + uri: SR.FormatID8000(SR.ID2019)); + + return; + } + // Restore the creation/expiration dates/identifiers from the token entry metadata. context.Principal .SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs index e9e02640..9c5aa557 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs @@ -8,6 +8,8 @@ using System.Collections.Immutable; using System.Security.Claims; +using Microsoft.Extensions.DependencyInjection; +using Moq; using Xunit; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlers.Protection; @@ -467,6 +469,78 @@ public abstract partial class OpenIddictServerIntegrationTests Assert.Equal?>([Scopes.OpenId, Scopes.Profile], (ImmutableArray?) response[Claims.Private.Scope]); } + [Fact] + public async Task ValidateToken_TokenPayloadUsedInsteadOfTokenReferenceIdentifierIsRejected() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeIdentifiers.AccessToken); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.GetReferenceIdAsync(token, It.IsAny())) + .ReturnsAsync("reference_id"); + }); + + await using var server = await CreateServerAsync(options => + { + options.SetUserInfoEndpointUris("/authenticate"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return ValueTask.CompletedTask; + })); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("token_payload", context.Token); + Assert.Equal([TokenTypeIdentifiers.AccessToken], context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeIdentifiers.AccessToken) + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetClaim(Claims.Subject, "Bob le Magnifique") + .SetClaims(Claims.Scope, [Scopes.OpenId, Scopes.Profile]); + + return ValueTask.CompletedTask; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(manager); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.GetAsync("/authenticate", new OpenIddictRequest + { + AccessToken = "token_payload" + }); + + // Assert + Assert.Null((string?) response[Claims.Subject]); + + Mock.Get(manager).Verify(manager => manager.GetReferenceIdAsync(token, It.IsAny()), Times.Once()); + } + [Fact] public async Task ValidateToken_MissingTokenTypeThrowsAnException() {