Browse Source

Detect and reject reference token payloads directly used as regular tokens

pull/2431/head
Kévin Chalet 4 weeks ago
parent
commit
63b56ccc82
  1. 3
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  2. 15
      src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
  3. 29
      src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
  4. 15
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
  5. 74
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs

3
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -3312,6 +3312,9 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6291" xml:space="preserve">
<value>The revocation response returned by {Uri} was successfully extracted: {Response}.</value>
</data>
<data name="ID6292" xml:space="preserve">
<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>
</data>
<data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value>
</data>

15
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))

29
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))

15
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))

74
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<IEnumerable<string?>?>([Scopes.OpenId, Scopes.Profile], (ImmutableArray<string?>?) 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<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeIdentifiers.AccessToken);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetReferenceIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("reference_id");
});
await using var server = await CreateServerAsync(options =>
{
options.SetUserInfoEndpointUris("/authenticate");
options.AddEventHandler<HandleUserInfoRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return ValueTask.CompletedTask;
}));
options.AddEventHandler<ValidateTokenContext>(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<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateToken_MissingTokenTypeThrowsAnException()
{

Loading…
Cancel
Save