Skip to content

Commit 63b56cc

Browse files
committed
Detect and reject reference token payloads directly used as regular tokens
1 parent 29c6668 commit 63b56cc

File tree

5 files changed

+136
-0
lines changed

5 files changed

+136
-0
lines changed

src/OpenIddict.Abstractions/OpenIddictResources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3312,6 +3312,9 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
33123312
<data name="ID6291" xml:space="preserve">
33133313
<value>The revocation response returned by {Uri} was successfully extracted: {Response}.</value>
33143314
</data>
3315+
<data name="ID6292" xml:space="preserve">
3316+
<value>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.</value>
3317+
</data>
33153318
<data name="ID8000" xml:space="preserve">
33163319
<value>https://documentation.openiddict.com/errors/{0}</value>
33173320
</data>

src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,21 @@ public async ValueTask HandleAsync(ValidateTokenContext context)
580580
return;
581581
}
582582

583+
// If the token was not validated as a reference token but has a reference identifier attached, this
584+
// may indicate that the payload stored in the database has leaked and is being used as a regular,
585+
// non-reference token. To prevent this, reject the token if the reference identifier is not null.
586+
if (!context.IsReferenceToken && !string.IsNullOrEmpty(await _tokenManager.GetReferenceIdAsync(token)))
587+
{
588+
context.Logger.LogWarning(6292, SR.GetResourceString(SR.ID6292), await _tokenManager.GetIdAsync(token));
589+
590+
context.Reject(
591+
error: Errors.InvalidToken,
592+
description: SR.GetResourceString(SR.ID2019),
593+
uri: SR.FormatID8000(SR.ID2019));
594+
595+
return;
596+
}
597+
583598
// Restore the creation/expiration dates/identifiers from the token entry metadata.
584599
context.Principal
585600
.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))

src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,35 @@ public async ValueTask HandleAsync(ValidateTokenContext context)
817817
return;
818818
}
819819

820+
// If the token was not validated as a reference token but has a reference identifier attached, this
821+
// may indicate that the payload stored in the database has leaked and is being used as a regular,
822+
// non-reference token. To prevent this, reject the token if the reference identifier is not null.
823+
if (!context.IsReferenceToken && !string.IsNullOrEmpty(await _tokenManager.GetReferenceIdAsync(token)))
824+
{
825+
context.Logger.LogWarning(6292, SR.GetResourceString(SR.ID6292), await _tokenManager.GetIdAsync(token));
826+
827+
context.Reject(
828+
error: Errors.InvalidToken,
829+
description: context.Principal.GetTokenType() switch
830+
{
831+
TokenTypeIdentifiers.Private.AuthorizationCode => SR.GetResourceString(SR.ID2001),
832+
TokenTypeIdentifiers.Private.DeviceCode => SR.GetResourceString(SR.ID2002),
833+
TokenTypeIdentifiers.RefreshToken => SR.GetResourceString(SR.ID2003),
834+
835+
_ => SR.GetResourceString(SR.ID2004)
836+
},
837+
uri: context.Principal.GetTokenType() switch
838+
{
839+
TokenTypeIdentifiers.Private.AuthorizationCode => SR.FormatID8000(SR.ID2001),
840+
TokenTypeIdentifiers.Private.DeviceCode => SR.FormatID8000(SR.ID2002),
841+
TokenTypeIdentifiers.RefreshToken => SR.FormatID8000(SR.ID2003),
842+
843+
_ => SR.FormatID8000(SR.ID2004)
844+
});
845+
846+
return;
847+
}
848+
820849
// Restore the creation/expiration dates/identifiers from the token entry metadata.
821850
context.Principal
822851
.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))

src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,21 @@ public async ValueTask HandleAsync(ValidateTokenContext context)
567567
return;
568568
}
569569

570+
// If the token was not validated as a reference token but has a reference identifier attached, this
571+
// may indicate that the payload stored in the database has leaked and is being used as a regular,
572+
// non-reference token. To prevent this, reject the token if the reference identifier is not null.
573+
if (!context.IsReferenceToken && !string.IsNullOrEmpty(await _tokenManager.GetReferenceIdAsync(token)))
574+
{
575+
context.Logger.LogWarning(6292, SR.GetResourceString(SR.ID6292), await _tokenManager.GetIdAsync(token));
576+
577+
context.Reject(
578+
error: Errors.InvalidToken,
579+
description: SR.GetResourceString(SR.ID2019),
580+
uri: SR.FormatID8000(SR.ID2019));
581+
582+
return;
583+
}
584+
570585
// Restore the creation/expiration dates/identifiers from the token entry metadata.
571586
context.Principal
572587
.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))

test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
using System.Collections.Immutable;
1010
using System.Security.Claims;
11+
using Microsoft.Extensions.DependencyInjection;
12+
using Moq;
1113
using Xunit;
1214
using static OpenIddict.Server.OpenIddictServerEvents;
1315
using static OpenIddict.Server.OpenIddictServerHandlers.Protection;
@@ -467,6 +469,78 @@ public async Task ValidateToken_MultiplePublicScopesAreMappedToPrivateClaims()
467469
Assert.Equal<IEnumerable<string?>?>([Scopes.OpenId, Scopes.Profile], (ImmutableArray<string?>?) response[Claims.Private.Scope]);
468470
}
469471

472+
[Fact]
473+
public async Task ValidateToken_TokenPayloadUsedInsteadOfTokenReferenceIdentifierIsRejected()
474+
{
475+
// Arrange
476+
var token = new OpenIddictToken();
477+
478+
var manager = CreateTokenManager(mock =>
479+
{
480+
mock.Setup(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
481+
.ReturnsAsync(token);
482+
483+
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
484+
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
485+
486+
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
487+
.ReturnsAsync(TokenTypeIdentifiers.AccessToken);
488+
489+
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
490+
.ReturnsAsync(true);
491+
492+
mock.Setup(manager => manager.GetReferenceIdAsync(token, It.IsAny<CancellationToken>()))
493+
.ReturnsAsync("reference_id");
494+
});
495+
496+
await using var server = await CreateServerAsync(options =>
497+
{
498+
options.SetUserInfoEndpointUris("/authenticate");
499+
500+
options.AddEventHandler<HandleUserInfoRequestContext>(builder =>
501+
builder.UseInlineHandler(context =>
502+
{
503+
context.SkipRequest();
504+
505+
return ValueTask.CompletedTask;
506+
}));
507+
508+
options.AddEventHandler<ValidateTokenContext>(builder =>
509+
{
510+
builder.UseInlineHandler(context =>
511+
{
512+
Assert.Equal("token_payload", context.Token);
513+
Assert.Equal([TokenTypeIdentifiers.AccessToken], context.ValidTokenTypes);
514+
515+
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
516+
.SetTokenType(TokenTypeIdentifiers.AccessToken)
517+
.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56")
518+
.SetClaim(Claims.Subject, "Bob le Magnifique")
519+
.SetClaims(Claims.Scope, [Scopes.OpenId, Scopes.Profile]);
520+
521+
return ValueTask.CompletedTask;
522+
});
523+
524+
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
525+
});
526+
527+
options.Services.AddSingleton(manager);
528+
});
529+
530+
await using var client = await server.CreateClientAsync();
531+
532+
// Act
533+
var response = await client.GetAsync("/authenticate", new OpenIddictRequest
534+
{
535+
AccessToken = "token_payload"
536+
});
537+
538+
// Assert
539+
Assert.Null((string?) response[Claims.Subject]);
540+
541+
Mock.Get(manager).Verify(manager => manager.GetReferenceIdAsync(token, It.IsAny<CancellationToken>()), Times.Once());
542+
}
543+
470544
[Fact]
471545
public async Task ValidateToken_MissingTokenTypeThrowsAnException()
472546
{

0 commit comments

Comments
 (0)