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;
}
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.
};

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

@ -40,9 +40,9 @@ namespace OpenIddict.Server
MapInternalClaims.Descriptor,
RestoreReferenceTokenProperties.Descriptor,
ValidatePrincipal.Descriptor,
ValidateExpirationDate.Descriptor,
ValidateTokenEntry.Descriptor,
ValidateAuthorizationEntry.Descriptor,
ValidateExpirationDate.Descriptor,
/*
* 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 (context.ValidTokenTypes.Count > 0 &&
!await _tokenManager.HasTypeAsync(token, context.ValidTokenTypes.ToImmutableArray()))
if (!(context.ValidTokenTypes.Count switch
{
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(
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>
/// Contains the logic responsible of rejecting authentication demands that
/// use a token whose entry is no longer valid (e.g was revoked).
@ -696,7 +763,7 @@ namespace OpenIddict.Server
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.UseScopedHandler<ValidateTokenEntry>()
.SetOrder(ValidatePrincipal.Descriptor.Order + 1_000)
.SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.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>
/// Contains the logic responsible of resolving the signing and encryption credentials used to protect tokens.
/// </summary>

10
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -1790,6 +1790,7 @@ namespace OpenIddict.Server
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireDeviceCodeGenerated>()
.UseSingletonHandler<PrepareDeviceCodePrincipal>()
.SetOrder(PrepareAuthorizationCodePrincipal.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
@ -1805,13 +1806,6 @@ namespace OpenIddict.Server
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.
// Actors identities are also filtered (delegation scenarios).
var principal = context.Principal.Clone(claim =>
@ -2044,7 +2038,7 @@ namespace OpenIddict.Server
principal.SetClaim(Claims.Nonce, context.EndpointType switch
{
OpenIddictServerEndpointType.Authorization => context.Request.Nonce,
OpenIddictServerEndpointType.Token => context.Principal.GetClaim(Claims.Private.Nonce),
OpenIddictServerEndpointType.Token => context.Principal.GetClaim(Claims.Private.Nonce),
_ => null
});

16
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -181,36 +181,40 @@ namespace OpenIddict.Server
/// <summary>
/// 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>
public TimeSpan? AuthorizationCodeLifetime { get; set; } = TimeSpan.FromMinutes(5);
/// <summary>
/// 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.
/// 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>
public TimeSpan? AccessTokenLifetime { get; set; } = TimeSpan.FromHours(1);
/// <summary>
/// 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.
/// 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.
/// </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);
/// <summary>
/// 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.
/// 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>
public TimeSpan? IdentityTokenLifetime { get; set; } = TimeSpan.FromMinutes(20);
/// <summary>
/// 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.
/// 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>
public TimeSpan? RefreshTokenLifetime { get; set; } = TimeSpan.FromDays(14);
@ -223,7 +227,7 @@ namespace OpenIddict.Server
/// <summary>
/// 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.
/// 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.
/// </summary>
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 (context.ValidTokenTypes.Count > 0 &&
!await _tokenManager.HasTypeAsync(token, context.ValidTokenTypes.ToImmutableArray()))
if (!(context.ValidTokenTypes.Count switch
{
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(
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);
}
[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]
public async Task ValidateTokenRequest_AuthorizationCodeCausesAnErrorWhenPresentersAreMissing()
{

Loading…
Cancel
Save