Browse Source

Validate the expiration date of a token before validating its database entry

pull/1313/head
Kévin Chalet 5 years ago
parent
commit
87c99d6bc7
  1. 7
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  2. 138
      src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
  3. 10
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  4. 16
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  5. 8
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
  6. 69
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs

7
src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs

@ -1760,10 +1760,13 @@ namespace OpenIddict.Server
return default; return default;
} }
context.Response.Error = context.Error switch context.Response.Error = context.Error switch
{ {
Errors.InvalidToken or Errors.ExpiredToken => Errors.InvalidGrant, // Keep "expired_token" errors as-is if the request is a device code token request.
Errors.ExpiredToken when context.Request.IsDeviceCodeGrantType() => Errors.ExpiredToken,
// Convert "invalid_token" errors to "invalid_grant".
Errors.InvalidToken => Errors.InvalidGrant,
_ => context.Error // Otherwise, keep the error as-is. _ => context.Error // Otherwise, keep the error as-is.
}; };

138
src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs

@ -40,9 +40,9 @@ namespace OpenIddict.Server
MapInternalClaims.Descriptor, MapInternalClaims.Descriptor,
RestoreReferenceTokenProperties.Descriptor, RestoreReferenceTokenProperties.Descriptor,
ValidatePrincipal.Descriptor, ValidatePrincipal.Descriptor,
ValidateExpirationDate.Descriptor,
ValidateTokenEntry.Descriptor, ValidateTokenEntry.Descriptor,
ValidateAuthorizationEntry.Descriptor, ValidateAuthorizationEntry.Descriptor,
ValidateExpirationDate.Descriptor,
/* /*
* Token generation: * Token generation:
@ -180,8 +180,12 @@ namespace OpenIddict.Server
} }
// If the type associated with the token entry doesn't match one of the expected types, return an error. // If the type associated with the token entry doesn't match one of the expected types, return an error.
if (context.ValidTokenTypes.Count > 0 && if (!(context.ValidTokenTypes.Count switch
!await _tokenManager.HasTypeAsync(token, context.ValidTokenTypes.ToImmutableArray())) {
0 => true, // If no specific token type is expected, accept all token types at this stage.
1 => await _tokenManager.HasTypeAsync(token, context.ValidTokenTypes.ElementAt(0)),
_ => await _tokenManager.HasTypeAsync(token, context.ValidTokenTypes.ToImmutableArray())
}))
{ {
context.Reject( context.Reject(
error: Errors.InvalidToken, error: Errors.InvalidToken,
@ -674,6 +678,69 @@ namespace OpenIddict.Server
} }
} }
/// <summary>
/// Contains the logic responsible of rejecting authentication demands that use an expired token.
/// </summary>
public class ValidateExpirationDate : IOpenIddictServerHandler<ValidateTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.UseSingletonHandler<ValidateExpirationDate>()
.SetOrder(ValidatePrincipal.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
if (context.DisableLifetimeValidation)
{
return default;
}
var date = context.Principal.GetExpirationDate();
if (date.HasValue && date.Value < DateTimeOffset.UtcNow)
{
context.Reject(
error: context.Principal.GetTokenType() switch
{
TokenTypeHints.DeviceCode => Errors.ExpiredToken,
_ => Errors.InvalidToken
},
description: context.Principal.GetTokenType() switch
{
TokenTypeHints.AuthorizationCode => SR.GetResourceString(SR.ID2016),
TokenTypeHints.DeviceCode => SR.GetResourceString(SR.ID2017),
TokenTypeHints.RefreshToken => SR.GetResourceString(SR.ID2018),
_ => SR.GetResourceString(SR.ID2019)
},
uri: context.Principal.GetTokenType() switch
{
TokenTypeHints.AuthorizationCode => SR.FormatID8000(SR.ID2016),
TokenTypeHints.DeviceCode => SR.FormatID8000(SR.ID2017),
TokenTypeHints.RefreshToken => SR.FormatID8000(SR.ID2018),
_ => SR.FormatID8000(SR.ID2019)
});
return default;
}
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible of rejecting authentication demands that /// Contains the logic responsible of rejecting authentication demands that
/// use a token whose entry is no longer valid (e.g was revoked). /// use a token whose entry is no longer valid (e.g was revoked).
@ -696,7 +763,7 @@ namespace OpenIddict.Server
.AddFilter<RequireDegradedModeDisabled>() .AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>() .AddFilter<RequireTokenStorageEnabled>()
.UseScopedHandler<ValidateTokenEntry>() .UseScopedHandler<ValidateTokenEntry>()
.SetOrder(ValidatePrincipal.Descriptor.Order + 1_000) .SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -947,69 +1014,6 @@ namespace OpenIddict.Server
} }
} }
/// <summary>
/// Contains the logic responsible of rejecting authentication demands that use an expired token.
/// </summary>
public class ValidateExpirationDate : IOpenIddictServerHandler<ValidateTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.UseSingletonHandler<ValidateExpirationDate>()
.SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
if (context.DisableLifetimeValidation)
{
return default;
}
var date = context.Principal.GetExpirationDate();
if (date.HasValue && date.Value < DateTimeOffset.UtcNow)
{
context.Reject(
error: context.Principal.GetTokenType() switch
{
TokenTypeHints.DeviceCode => Errors.ExpiredToken,
_ => Errors.InvalidToken
},
description: context.Principal.GetTokenType() switch
{
TokenTypeHints.AuthorizationCode => SR.GetResourceString(SR.ID2016),
TokenTypeHints.DeviceCode => SR.GetResourceString(SR.ID2017),
TokenTypeHints.RefreshToken => SR.GetResourceString(SR.ID2018),
_ => SR.GetResourceString(SR.ID2019)
},
uri: context.Principal.GetTokenType() switch
{
TokenTypeHints.AuthorizationCode => SR.FormatID8000(SR.ID2016),
TokenTypeHints.DeviceCode => SR.FormatID8000(SR.ID2017),
TokenTypeHints.RefreshToken => SR.FormatID8000(SR.ID2018),
_ => SR.FormatID8000(SR.ID2019)
});
return default;
}
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible of resolving the signing and encryption credentials used to protect tokens. /// Contains the logic responsible of resolving the signing and encryption credentials used to protect tokens.
/// </summary> /// </summary>

10
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -1790,6 +1790,7 @@ namespace OpenIddict.Server
/// </summary> /// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireDeviceCodeGenerated>()
.UseSingletonHandler<PrepareDeviceCodePrincipal>() .UseSingletonHandler<PrepareDeviceCodePrincipal>()
.SetOrder(PrepareAuthorizationCodePrincipal.Descriptor.Order + 1_000) .SetOrder(PrepareAuthorizationCodePrincipal.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
@ -1805,13 +1806,6 @@ namespace OpenIddict.Server
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Note: a device code principal is produced when a device code is included in the response or when a
// device code entry is replaced when processing a sign-in response sent to the verification endpoint.
if (context.EndpointType != OpenIddictServerEndpointType.Verification && !context.GenerateDeviceCode)
{
return default;
}
// Create a new principal containing only the filtered claims. // Create a new principal containing only the filtered claims.
// Actors identities are also filtered (delegation scenarios). // Actors identities are also filtered (delegation scenarios).
var principal = context.Principal.Clone(claim => var principal = context.Principal.Clone(claim =>
@ -2044,7 +2038,7 @@ namespace OpenIddict.Server
principal.SetClaim(Claims.Nonce, context.EndpointType switch principal.SetClaim(Claims.Nonce, context.EndpointType switch
{ {
OpenIddictServerEndpointType.Authorization => context.Request.Nonce, OpenIddictServerEndpointType.Authorization => context.Request.Nonce,
OpenIddictServerEndpointType.Token => context.Principal.GetClaim(Claims.Private.Nonce), OpenIddictServerEndpointType.Token => context.Principal.GetClaim(Claims.Private.Nonce),
_ => null _ => null
}); });

16
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -181,36 +181,40 @@ namespace OpenIddict.Server
/// <summary> /// <summary>
/// Gets or sets the period of time authorization codes remain valid after being issued. The default value is 5 minutes. /// Gets or sets the period of time authorization codes remain valid after being issued. The default value is 5 minutes.
/// While not recommended, this property can be set to <c>null</c> to issue codes that never expire. /// While not recommended, this property can be set to <see langword="null"/> to issue authorization codes that never expire.
/// </summary> /// </summary>
public TimeSpan? AuthorizationCodeLifetime { get; set; } = TimeSpan.FromMinutes(5); public TimeSpan? AuthorizationCodeLifetime { get; set; } = TimeSpan.FromMinutes(5);
/// <summary> /// <summary>
/// Gets or sets the period of time access tokens remain valid after being issued. The default value is 1 hour. /// Gets or sets the period of time access tokens remain valid after being issued. The default value is 1 hour.
/// The client application is expected to refresh or acquire a new access token after the token has expired. /// The client application is expected to refresh or acquire a new access token after the token has expired.
/// While not recommended, this property can be set to <c>null</c> to issue access tokens that never expire. /// While not recommended, this property can be set to <see langword="null"/> to issue access tokens that never expire.
/// </summary> /// </summary>
public TimeSpan? AccessTokenLifetime { get; set; } = TimeSpan.FromHours(1); public TimeSpan? AccessTokenLifetime { get; set; } = TimeSpan.FromHours(1);
/// <summary> /// <summary>
/// Gets or sets the period of time device codes remain valid after being issued. The default value is 10 minutes. /// Gets or sets the period of time device codes remain valid after being issued. The default value is 10 minutes.
/// The client application is expected to start a whole new authentication flow after the device code has expired. /// The client application is expected to start a whole new authentication flow after the device code has expired.
/// While not recommended, this property can be set to <c>null</c> to issue codes that never expire. /// While not recommended, this property can be set to <see langword="null"/> to issue device codes that never expire.
/// Note: the same value should be chosen for both <see cref="UserCodeLifetime"/> and this property. /// Note: the same value should be chosen for both <see cref="UserCodeLifetime"/> and this property.
/// </summary> /// </summary>
/// <remarks>
/// The expiration date of a device code is automatically extended when the user approves the
/// authorization demand to give the client application enough time to redeem the device code.
/// </remarks>
public TimeSpan? DeviceCodeLifetime { get; set; } = TimeSpan.FromMinutes(10); public TimeSpan? DeviceCodeLifetime { get; set; } = TimeSpan.FromMinutes(10);
/// <summary> /// <summary>
/// Gets or sets the period of time identity tokens remain valid after being issued. The default value is 20 minutes. /// Gets or sets the period of time identity tokens remain valid after being issued. The default value is 20 minutes.
/// The client application is expected to refresh or acquire a new identity token after the token has expired. /// The client application is expected to refresh or acquire a new identity token after the token has expired.
/// While not recommended, this property can be set to <c>null</c> to issue identity tokens that never expire. /// While not recommended, this property can be set to <see langword="null"/> to issue identity tokens that never expire.
/// </summary> /// </summary>
public TimeSpan? IdentityTokenLifetime { get; set; } = TimeSpan.FromMinutes(20); public TimeSpan? IdentityTokenLifetime { get; set; } = TimeSpan.FromMinutes(20);
/// <summary> /// <summary>
/// Gets or sets the period of time refresh tokens remain valid after being issued. The default value is 14 days. /// Gets or sets the period of time refresh tokens remain valid after being issued. The default value is 14 days.
/// The client application is expected to start a whole new authentication flow after the refresh token has expired. /// The client application is expected to start a whole new authentication flow after the refresh token has expired.
/// While not recommended, this property can be set to <c>null</c> to issue refresh tokens that never expire. /// While not recommended, this property can be set to <see langword="null"/> to issue refresh tokens that never expire.
/// </summary> /// </summary>
public TimeSpan? RefreshTokenLifetime { get; set; } = TimeSpan.FromDays(14); public TimeSpan? RefreshTokenLifetime { get; set; } = TimeSpan.FromDays(14);
@ -223,7 +227,7 @@ namespace OpenIddict.Server
/// <summary> /// <summary>
/// Gets or sets the period of time user codes remain valid after being issued. The default value is 10 minutes. /// Gets or sets the period of time user codes remain valid after being issued. The default value is 10 minutes.
/// The client application is expected to start a whole new authentication flow after the user code has expired. /// The client application is expected to start a whole new authentication flow after the user code has expired.
/// While not recommended, this property can be set to <c>null</c> to issue codes that never expire. /// While not recommended, this property can be set to <see langword="null"/> to issue user codes that never expire.
/// Note: the same value should be chosen for both <see cref="DeviceCodeLifetime"/> and this property. /// Note: the same value should be chosen for both <see cref="DeviceCodeLifetime"/> and this property.
/// </summary> /// </summary>
public TimeSpan? UserCodeLifetime { get; set; } = TimeSpan.FromMinutes(10); public TimeSpan? UserCodeLifetime { get; set; } = TimeSpan.FromMinutes(10);

8
src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs

@ -153,8 +153,12 @@ namespace OpenIddict.Validation
} }
// If the type associated with the token entry doesn't match one of the expected types, return an error. // If the type associated with the token entry doesn't match one of the expected types, return an error.
if (context.ValidTokenTypes.Count > 0 && if (!(context.ValidTokenTypes.Count switch
!await _tokenManager.HasTypeAsync(token, context.ValidTokenTypes.ToImmutableArray())) {
0 => true, // If no specific token type is expected, accept all token types at this stage.
1 => await _tokenManager.HasTypeAsync(token, context.ValidTokenTypes.ElementAt(0)),
_ => await _tokenManager.HasTypeAsync(token, context.ValidTokenTypes.ToImmutableArray())
}))
{ {
context.Reject( context.Reject(
error: Errors.InvalidToken, error: Errors.InvalidToken,

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

@ -474,6 +474,75 @@ namespace OpenIddict.Server.IntegrationTests
Assert.Equal(SR.FormatID8000(SR.ID2018), response.ErrorUri); Assert.Equal(SR.FormatID8000(SR.ID2018), response.ErrorUri);
} }
[Fact]
public async Task ValidateTokenRequest_ExpiredDeviceCodeCausesAnError()
{
// Arrange
var token = new OpenIddictToken();
var manager = CreateTokenManager(mock =>
{
mock.Setup(manager => manager.FindByReferenceIdAsync("g43LaWCUrz2RaLILz2L1bg1bOpMSv1hGrH12IIkB9H4", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.HasTypeAsync(token, TokenTypeHints.DeviceCode, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.GetPayloadAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS");
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.GetExpirationDateAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(DateTimeOffset.UtcNow - TimeSpan.FromDays(1));
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.DeviceCode);
});
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", context.Token);
Assert.Equal(new[] { TokenTypeHints.DeviceCode }, context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(manager);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.DeviceCode,
DeviceCode = "g43LaWCUrz2RaLILz2L1bg1bOpMSv1hGrH12IIkB9H4"
});
// Assert
Assert.Equal(Errors.ExpiredToken, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2017), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2017), response.ErrorUri);
Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasStatusAsync(token, Statuses.Inactive, It.IsAny<CancellationToken>()), Times.Never());
}
[Fact] [Fact]
public async Task ValidateTokenRequest_AuthorizationCodeCausesAnErrorWhenPresentersAreMissing() public async Task ValidateTokenRequest_AuthorizationCodeCausesAnErrorWhenPresentersAreMissing()
{ {

Loading…
Cancel
Save