Browse Source

Update the client and server stacks to redeem tokens earlier

pull/1498/head
Kévin Chalet 4 years ago
parent
commit
e1d7817825
  1. 3
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  2. 35
      src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
  3. 115
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  4. 191
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  5. 95
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs

3
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1730,6 +1730,9 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<data name="ID2138" xml:space="preserve">
<value>The current address doesn't match the address of the redirection endpoint selected during the initial authorization request.</value>
</data>
<data name="ID2139" xml:space="preserve">
<value>The specified state token has already been redeemed.</value>
</data>
<data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>

35
src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs

@ -575,8 +575,41 @@ public static partial class OpenIddictClientHandlers
return;
}
// If the token entry cannot be found, return a generic error.
var token = await _tokenManager.FindByIdAsync(identifier);
if (token is null || !await _tokenManager.HasStatusAsync(token, Statuses.Valid))
if (token is null)
{
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2019),
uri: SR.FormatID8000(SR.ID2019));
return;
}
if (await _tokenManager.HasStatusAsync(token, Statuses.Redeemed))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6002), identifier);
context.Reject(
error: Errors.InvalidToken,
description: context.Principal.GetTokenType() switch
{
TokenTypeHints.StateToken => SR.GetResourceString(SR.ID2139),
_ => SR.GetResourceString(SR.ID2013)
},
uri: context.Principal.GetTokenType() switch
{
TokenTypeHints.StateToken => SR.FormatID8000(SR.ID2139),
_ => SR.FormatID8000(SR.ID2013)
});
return;
}
if (!await _tokenManager.HasStatusAsync(token, Statuses.Valid))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6005), identifier);

115
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -30,6 +30,7 @@ public static partial class OpenIddictClientHandlers
ResolveValidatedStateToken.Descriptor,
ValidateRequiredStateToken.Descriptor,
ValidateStateToken.Descriptor,
RedeemStateTokenEntry.Descriptor,
ResolveClientRegistrationFromStateToken.Descriptor,
ValidateIssuerParameter.Descriptor,
ValidateFrontchannelErrorParameters.Descriptor,
@ -85,8 +86,6 @@ public static partial class OpenIddictClientHandlers
ValidateUserinfoTokenWellknownClaims.Descriptor,
ValidateUserinfoTokenSubject.Descriptor,
RedeemStateTokenEntry.Descriptor,
/*
* Challenge processing:
*/
@ -374,6 +373,66 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for redeeming the token entry corresponding to the received state token.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public class RedeemStateTokenEntry : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
public RedeemStateTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0318));
public RedeemStateTokenEntry(IOpenIddictTokenManager tokenManager)
=> _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireStateTokenPrincipal>()
.UseScopedHandler<RedeemStateTokenEntry>()
// Note: this handler is deliberately executed early in the pipeline to ensure
// that the state token entry is always marked as redeemed even if the authentication
// demand is rejected later in the pipeline (e.g because an error was returned).
.SetOrder(ValidateStateToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Extract the token identifier from the state token principal.
// If no token identifier can be found, this indicates that the token has no backing database entry.
var identifier = context.StateTokenPrincipal.GetTokenId();
if (string.IsNullOrEmpty(identifier))
{
return;
}
// Mark the token as redeemed to prevent future reuses.
var token = await _tokenManager.FindByIdAsync(identifier);
if (token is not null && !await _tokenManager.TryRedeemAsync(token))
{
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2139),
uri: SR.FormatID8000(SR.ID2139));
return;
}
}
}
/// <summary>
/// Contains the logic responsible for resolving the client registration
/// based on the authorization server identity stored in the state token.
@ -387,7 +446,7 @@ public static partial class OpenIddictClientHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenPrincipal>()
.UseSingletonHandler<ResolveClientRegistrationFromStateToken>()
.SetOrder(ValidateStateToken.Descriptor.Order + 1_000)
.SetOrder(RedeemStateTokenEntry.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
@ -3194,56 +3253,6 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for redeeming the token entry corresponding to the received state token.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public class RedeemStateTokenEntry : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
public RedeemStateTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0318));
public RedeemStateTokenEntry(IOpenIddictTokenManager tokenManager)
=> _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireStateTokenPrincipal>()
.UseScopedHandler<RedeemStateTokenEntry>()
.SetOrder(ValidateUserinfoTokenSubject.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Extract the token identifier from the state token principal.
// If no token identifier can be found, this indicates that the token has no backing database entry.
var identifier = context.StateTokenPrincipal.GetTokenId();
if (!string.IsNullOrEmpty(identifier))
{
// Mark the token as redeemed to prevent future reuses.
var token = await _tokenManager.FindByIdAsync(identifier);
if (token is not null)
{
await _tokenManager.TryRedeemAsync(token);
}
}
}
}
/// <summary>
/// Contains the logic responsible for rejecting invalid challenge demands.
/// </summary>

191
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -47,6 +47,7 @@ public static partial class OpenIddictServerHandlers
* Sign-in processing:
*/
ValidateSignInDemand.Descriptor,
RedeemTokenEntry.Descriptor,
RestoreInternalClaims.Descriptor,
AttachDefaultScopes.Descriptor,
AttachDefaultPresenters.Descriptor,
@ -61,8 +62,6 @@ public static partial class OpenIddictServerHandlers
PrepareIdentityTokenPrincipal.Descriptor,
PrepareUserCodePrincipal.Descriptor,
RedeemTokenEntry.Descriptor,
GenerateAccessToken.Descriptor,
GenerateAuthorizationCode.Descriptor,
GenerateDeviceCode.Descriptor,
@ -1172,6 +1171,124 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for redeeming the token entry corresponding to
/// the received authorization code, device code, user code or refresh token.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public class RedeemTokenEntry : IOpenIddictServerHandler<ProcessSignInContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
public RedeemTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
public RedeemTokenEntry(IOpenIddictTokenManager tokenManager)
=> _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.UseScopedHandler<RedeemTokenEntry>()
// Note: this handler is deliberately executed early in the pipeline to ensure
// that the token database entry is always marked as redeemed even if the sign-in
// demand is rejected later in the pipeline (e.g because an error was returned).
.SetOrder(ValidateSignInDemand.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessSignInContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
switch (context.EndpointType)
{
case OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType():
case OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType():
case OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() &&
!context.Options.DisableRollingRefreshTokens:
case OpenIddictServerEndpointType.Verification:
break;
default: return;
}
var notification = context.Transaction.GetProperty<ProcessAuthenticationContext>(
typeof(ProcessAuthenticationContext).FullName!) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0007));
var principal = context.EndpointType switch
{
OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
=> notification.AuthorizationCodePrincipal,
OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
=> notification.DeviceCodePrincipal,
OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
=> notification.RefreshTokenPrincipal,
OpenIddictServerEndpointType.Verification => notification.UserCodePrincipal,
_ => null
};
Debug.Assert(principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Extract the token identifier from the authentication principal.
// If no token identifier can be found, this indicates that the token has no backing database entry.
var identifier = principal.GetTokenId();
if (string.IsNullOrEmpty(identifier))
{
return;
}
var token = await _tokenManager.FindByIdAsync(identifier);
if (token is null)
{
return;
}
// Mark the token as redeemed to prevent future reuses. If the request is a refresh token request, ignore
// errors returned while trying to mark the entry as redeemed (that may be caused by concurrent requests).
if (context.EndpointType is OpenIddictServerEndpointType.Token && context.Request.IsRefreshTokenGrantType())
{
await _tokenManager.TryRedeemAsync(token);
}
else if (!await _tokenManager.TryRedeemAsync(token))
{
context.Reject(
error: Errors.InvalidToken,
description: principal.GetTokenType() switch
{
TokenTypeHints.AuthorizationCode => SR.GetResourceString(SR.ID2010),
TokenTypeHints.DeviceCode => SR.GetResourceString(SR.ID2011),
TokenTypeHints.RefreshToken => SR.GetResourceString(SR.ID2012),
_ => SR.GetResourceString(SR.ID2013)
},
uri: principal.GetTokenType() switch
{
TokenTypeHints.AuthorizationCode => SR.FormatID8000(SR.ID2010),
TokenTypeHints.DeviceCode => SR.FormatID8000(SR.ID2011),
TokenTypeHints.RefreshToken => SR.FormatID8000(SR.ID2012),
_ => SR.FormatID8000(SR.ID2013)
});
return;
}
}
}
/// <summary>
/// Contains the logic responsible for re-attaching internal claims to the authentication principal.
/// </summary>
@ -1183,7 +1300,7 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.UseSingletonHandler<RestoreInternalClaims>()
.SetOrder(ValidateSignInDemand.Descriptor.Order + 1_000)
.SetOrder(RedeemTokenEntry.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -2125,72 +2242,6 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for redeeming the token entry corresponding to
/// the received authorization code, device code, user code or refresh token.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public class RedeemTokenEntry : IOpenIddictServerHandler<ProcessSignInContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
public RedeemTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
public RedeemTokenEntry(IOpenIddictTokenManager tokenManager)
=> _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.UseScopedHandler<RedeemTokenEntry>()
.SetOrder(100_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessSignInContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
switch (context.EndpointType)
{
case OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType():
case OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType():
case OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() &&
!context.Options.DisableRollingRefreshTokens:
case OpenIddictServerEndpointType.Verification:
break;
default: return;
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Extract the token identifier from the authentication principal.
// If no token identifier can be found, this indicates that the token has no backing database entry.
var identifier = context.Principal.GetTokenId();
if (string.IsNullOrEmpty(identifier))
{
return;
}
// If rolling tokens are enabled or if the request is a a code or device code token request
// or a user code verification request, mark the token as redeemed to prevent future reuses.
var token = await _tokenManager.FindByIdAsync(identifier);
if (token is not null)
{
await _tokenManager.TryRedeemAsync(token);
}
}
}
/// <summary>
/// Contains the logic responsible for generating an access token for the current sign-in operation.
/// </summary>
@ -2208,7 +2259,7 @@ public static partial class OpenIddictServerHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireAccessTokenGenerated>()
.UseScopedHandler<GenerateAccessToken>()
.SetOrder(RedeemTokenEntry.Descriptor.Order + 1_000)
.SetOrder(100_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();

95
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs

@ -3805,6 +3805,101 @@ public abstract partial class OpenIddictServerIntegrationTests
Mock.Get(manager).Verify(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeCannotBeRedeemed()
{
// 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(TokenTypeHints.AuthorizationCode);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.TryRedeemAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
mock.Setup(manager => manager.CreateAsync(It.IsAny<OpenIddictTokenDescriptor>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new OpenIddictToken());
});
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token);
Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeHints.AuthorizationCode)
.SetPresenters("Fabrikam")
.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.AddEventHandler<HandleTokenRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetClaim(Claims.Subject, "Bob le Magnifique");
return default;
}));
options.Services.AddSingleton(CreateApplicationManager(mock =>
{
var application = new OpenIddictApplication();
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
options.Services.AddSingleton(manager);
options.DisableAuthorizationStorage();
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientId = "Fabrikam",
Code = "SplxlOBeZQQYbYS6WxSbIA",
GrantType = GrantTypes.AuthorizationCode,
RedirectUri = "http://www.fabrikam.com/path"
});
// Assert
Assert.Equal(Errors.InvalidGrant, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2010), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2010), response.ErrorUri);
Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Theory]
[InlineData(GrantTypes.AuthorizationCode)]
[InlineData(GrantTypes.ClientCredentials)]

Loading…
Cancel
Save