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()
{