Browse Source

Add automatic revocation of old tokens when redeeming refresh tokens with rolling tokens enabled

pull/486/head
Kévin Chalet 8 years ago
parent
commit
cf3e649b0f
  1. 2
      build/dependencies.props
  2. 66
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  3. 2
      src/OpenIddict.Core/OpenIddictConstants.cs
  4. 10
      src/OpenIddict/OpenIddictExtensions.cs
  5. 10
      src/OpenIddict/OpenIddictInitializer.cs
  6. 3
      src/OpenIddict/OpenIddictOptions.cs
  7. 153
      src/OpenIddict/OpenIddictProvider.Exchange.cs
  8. 347
      src/OpenIddict/OpenIddictProvider.Helpers.cs
  9. 290
      src/OpenIddict/OpenIddictProvider.Serialization.cs
  10. 142
      src/OpenIddict/OpenIddictProvider.Signin.cs
  11. 31
      test/OpenIddict.Tests/OpenIddictInitializerTests.cs
  12. 300
      test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs
  13. 2
      test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs
  14. 1290
      test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs
  15. 721
      test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs
  16. 28
      test/OpenIddict.Tests/OpenIddictProviderTests.cs

2
build/dependencies.props

@ -2,7 +2,7 @@
<PropertyGroup>
<AspNetContribOpenIdExtensionsVersion>2.0.0-*</AspNetContribOpenIdExtensionsVersion>
<AspNetContribOpenIdServerVersion>2.0.0-*</AspNetContribOpenIdServerVersion>
<AspNetContribOpenIdServerVersion>2.0.0-rc1-final</AspNetContribOpenIdServerVersion>
<AspNetCoreVersion>2.0.0</AspNetCoreVersion>
<CoreFxVersion>4.4.0</CoreFxVersion>
<CryptoHelperVersion>3.0.0</CryptoHelperVersion>

66
src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs

@ -207,6 +207,69 @@ namespace OpenIddict.Core
return Store.GetIdAsync(authorization, cancellationToken);
}
/// <summary>
/// Retrieves the status associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the status associated with the specified authorization.
/// </returns>
public virtual Task<string> GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return Store.GetStatusAsync(authorization, cancellationToken);
}
/// <summary>
/// Determines whether a given authorization has been revoked.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the authorization has been revoked, <c>false</c> otherwise.</returns>
public virtual async Task<bool> IsRevokedAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
var status = await Store.GetStatusAsync(authorization, cancellationToken);
if (string.IsNullOrEmpty(status))
{
return false;
}
return string.Equals(status, OpenIddictConstants.Statuses.Revoked, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines whether a given authorization is valid.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the authorization is valid, <c>false</c> otherwise.</returns>
public virtual async Task<bool> IsValidAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
var status = await Store.GetStatusAsync(authorization, cancellationToken);
if (string.IsNullOrEmpty(status))
{
return false;
}
return string.Equals(status, OpenIddictConstants.Statuses.Valid, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Executes the specified query.
/// </summary>
@ -301,7 +364,8 @@ namespace OpenIddict.Core
var descriptor = new OpenIddictAuthorizationDescriptor
{
Status = await Store.GetStatusAsync(authorization, cancellationToken),
Subject = await Store.GetSubjectAsync(authorization, cancellationToken)
Subject = await Store.GetSubjectAsync(authorization, cancellationToken),
Type = await Store.GetTypeAsync(authorization, cancellationToken)
};
await ValidateAsync(descriptor, cancellationToken);

2
src/OpenIddict.Core/OpenIddictConstants.cs

@ -39,8 +39,8 @@ namespace OpenIddict.Core
public static class Properties
{
public const string AuthenticationTicket = ".authentication_ticket";
public const string AuthorizationId = ".authorization_id";
public const string TokenId = ".token_id";
}
public static class Separators

10
src/OpenIddict/OpenIddictExtensions.cs

@ -546,8 +546,8 @@ namespace Microsoft.Extensions.DependencyInjection
}
/// <summary>
/// Disables sliding expiration. When using this option, a single refresh token
/// is issued with a fixed expiration date: when it expires, a complete
/// Disables sliding expiration. When using this option, refresh tokens
/// are issued with a fixed expiration date: when it expires, a complete
/// authorization flow must be started to retrieve a new refresh token.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
@ -972,10 +972,8 @@ namespace Microsoft.Extensions.DependencyInjection
/// <summary>
/// Configures OpenIddict to use rolling refresh tokens. When this option is enabled,
/// a new refresh token is issued for each refresh token request and the previous one
/// is automatically revoked (when disabled, no new refresh token is issued and the
/// lifetime of the original refresh token is increased by updating the database entry).
/// Note: this option cannot be used when manually disabling sliding expiration.
/// a new refresh token is always issued for each refresh token request (and the previous
/// one is automatically revoked unless token revocation was explicitly disabled).
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <returns>The <see cref="OpenIddictBuilder"/>.</returns>

10
src/OpenIddict/OpenIddictInitializer.cs

@ -158,14 +158,10 @@ namespace OpenIddict
"Reference tokens cannot be used when configuring JWT as the access token format.");
}
if (options.UseRollingTokens && options.DisableTokenRevocation)
if (options.UseSlidingExpiration && options.DisableTokenRevocation && !options.UseRollingTokens)
{
throw new InvalidOperationException("Rolling tokens cannot be used when disabling token expiration.");
}
if (options.UseRollingTokens && !options.UseSlidingExpiration)
{
throw new InvalidOperationException("Rolling tokens cannot be used without enabling sliding expiration.");
throw new InvalidOperationException("Sliding expiration must be disabled when turning off " +
"token revocation if rolling tokens are not used.");
}
if (options.AccessTokenHandler != null && options.SigningCredentials.Count == 0)

3
src/OpenIddict/OpenIddictOptions.cs

@ -105,7 +105,8 @@ namespace OpenIddict
/// When disabled, no new token is issued and the refresh token lifetime is
/// dynamically managed by updating the token entry in the database.
/// When this option is enabled, a new refresh token is issued for each
/// refresh token request and the previous one is automatically revoked.
/// refresh token request (and the previous one is automatically revoked
/// unless token revocation was explicitly disabled in the options).
/// </summary>
public bool UseRollingTokens { get; set; }
}

153
src/OpenIddict/OpenIddictProvider.Exchange.cs

@ -202,6 +202,12 @@ namespace OpenIddict
{
var options = (OpenIddictOptions) context.Options;
if (context.Ticket != null)
{
// Store the authentication ticket as a request property so it can be later retrieved, if necessary.
context.Request.SetProperty(OpenIddictConstants.Properties.AuthenticationTicket, context.Ticket);
}
if (options.DisableTokenRevocation || (!context.Request.IsAuthorizationCodeGrantType() &&
!context.Request.IsRefreshTokenGrantType()))
{
@ -215,127 +221,58 @@ namespace OpenIddict
Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null.");
// Extract the token identifier from the authentication ticket.
var identifier = context.Ticket.GetProperty(OpenIdConnectConstants.Properties.TokenId);
Debug.Assert(!string.IsNullOrEmpty(identifier), "The authentication ticket should contain a ticket identifier.");
var identifier = context.Ticket.GetTokenId();
Debug.Assert(!string.IsNullOrEmpty(identifier),
"The authentication ticket should contain a ticket identifier.");
// Store the original authorization code/refresh token so it can be later retrieved.
context.Request.SetProperty(OpenIddictConstants.Properties.TokenId, identifier);
if (context.Request.IsAuthorizationCodeGrantType())
// Retrieve the authorization code/refresh token from the database and ensure it is still valid.
var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
if (token == null)
{
// Retrieve the authorization code from the database and ensure it is still valid.
var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
if (token == null)
{
Logger.LogError("The token request was rejected because the authorization " +
"code '{Identifier}' was not found in the database.", identifier);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified authorization code is no longer valid.");
return;
}
// If the authorization code is already marked as redeemed, this may indicate that the authorization
// code was compromised. In this case, revoke the authorization and all the associated tokens.
// See https://tools.ietf.org/html/rfc6749#section-10.5 for more information.
if (await Tokens.IsRedeemedAsync(token, context.HttpContext.RequestAborted))
{
var key = context.Ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId);
if (!string.IsNullOrEmpty(key))
{
var authorization = await Authorizations.FindByIdAsync(key, context.HttpContext.RequestAborted);
if (authorization != null)
{
Logger.LogInformation("The authorization '{Identifier}' was automatically revoked.", key);
await Authorizations.RevokeAsync(authorization, context.HttpContext.RequestAborted);
}
var tokens = await Tokens.FindByAuthorizationIdAsync(key, context.HttpContext.RequestAborted);
for (var index = 0; index < tokens.Length; index++)
{
Logger.LogInformation("The compromised token '{Identifier}' was automatically revoked.",
await Tokens.GetIdAsync(tokens[index], context.HttpContext.RequestAborted));
await Tokens.RevokeAsync(tokens[index], context.HttpContext.RequestAborted);
}
}
Logger.LogError("The token request was rejected because the authorization code " +
"'{Identifier}' has already been redeemed.", identifier);
Logger.LogError("The token request was rejected because the authorization code " +
"or refresh token '{Identifier}' was not found in the database.", identifier);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified authorization code has already been redeemed.");
return;
}
else if (!await Tokens.IsValidAsync(token, context.HttpContext.RequestAborted))
{
Logger.LogError("The token request was rejected because the authorization code " +
"'{Identifier}' was no longer valid.", identifier);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified authorization code is no longer valid.");
return;
}
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code is no longer valid." :
"The specified refresh token is no longer valid.");
// Mark the authorization code as redeemed to prevent token reuse.
await Tokens.RedeemAsync(token, context.HttpContext.RequestAborted);
return;
}
else
// If the authorization code/refresh token is already marked as redeemed, this may indicate that
// it was compromised. In this case, revoke the authorization and all the associated tokens.
// See https://tools.ietf.org/html/rfc6749#section-10.5 for more information.
if (await Tokens.IsRedeemedAsync(token, context.HttpContext.RequestAborted))
{
// Retrieve the token from the database and ensure it is still valid.
var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
if (token == null)
{
Logger.LogError("The token request was rejected because the refresh token " +
"'{Identifier}' was not found in the database.", identifier);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified refresh token is no longer valid.");
return;
}
else if (await Tokens.IsRedeemedAsync(token, context.HttpContext.RequestAborted))
{
Logger.LogError("The token request was rejected because the refresh token " +
"'{Identifier}' has already been redeemed.", identifier);
await RevokeAuthorizationAsync(context.Ticket, context.HttpContext);
await RevokeTokensAsync(context.Ticket, context.HttpContext);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified refresh token has already been redeemed.");
Logger.LogError("The token request was rejected because the authorization code " +
"or refresh token '{Identifier}' has already been redeemed.", identifier);
return;
}
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code has already been redeemed." :
"The specified refresh token has already been redeemed.");
else if (!await Tokens.IsValidAsync(token, context.HttpContext.RequestAborted))
{
Logger.LogError("The token request was rejected because the refresh token " +
"'{Identifier}' was no longer valid.", identifier);
return;
}
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified refresh token is no longer valid.");
else if (!await Tokens.IsValidAsync(token, context.HttpContext.RequestAborted))
{
Logger.LogError("The token request was rejected because the authorization code " +
"or refresh token '{Identifier}' was no longer valid.", identifier);
return;
}
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code is no longer valid." :
"The specified refresh token is no longer valid.");
// When rolling tokens are enabled, immediately
// redeem the refresh token to prevent future reuse.
// See https://tools.ietf.org/html/rfc6749#section-6.
if (options.UseRollingTokens)
{
await Tokens.RedeemAsync(token, context.HttpContext.RequestAborted);
}
return;
}
// Invoke the rest of the pipeline to allow

347
src/OpenIddict/OpenIddictProvider.Helpers.cs

@ -0,0 +1,347 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Core;
namespace OpenIddict
{
public partial class OpenIddictProvider<TApplication, TAuthorization, TScope, TToken> : OpenIdConnectServerProvider
where TApplication : class where TAuthorization : class where TScope : class where TToken : class
{
private async Task CreateAuthorizationAsync(
[NotNull] AuthenticationTicket ticket, [NotNull] OpenIddictOptions options,
[NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request)
{
if (options.DisableTokenRevocation)
{
return;
}
var descriptor = new OpenIddictAuthorizationDescriptor
{
Status = OpenIddictConstants.Statuses.Valid,
Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject),
Type = OpenIddictConstants.AuthorizationTypes.AdHoc
};
foreach (var scope in request.GetScopes())
{
descriptor.Scopes.Add(scope);
}
// If the client application is known, bind it to the authorization.
if (!string.IsNullOrEmpty(request.ClientId))
{
var application = await Applications.FindByClientIdAsync(request.ClientId, context.RequestAborted);
if (application == null)
{
throw new InvalidOperationException("The client application cannot be retrieved from the database.");
}
descriptor.ApplicationId = await Applications.GetIdAsync(application, context.RequestAborted);
}
var authorization = await Authorizations.CreateAsync(descriptor, context.RequestAborted);
if (authorization != null)
{
var identifier = await Authorizations.GetIdAsync(authorization, context.RequestAborted);
if (string.IsNullOrEmpty(request.ClientId))
{
Logger.LogInformation("An ad hoc authorization was automatically created and " +
"associated with an unknown application: {Identifier}.", identifier);
}
else
{
Logger.LogInformation("An ad hoc authorization was automatically created and " +
"associated with the '{ClientId}' application: {Identifier}.",
request.ClientId, identifier);
}
// Attach the unique identifier of the ad hoc authorization to the authentication ticket
// so that it is attached to all the derived tokens, allowing batched revocations support.
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, identifier);
}
}
private async Task<string> CreateTokenAsync(
[NotNull] string type, [NotNull] AuthenticationTicket ticket,
[NotNull] OpenIddictOptions options, [NotNull] HttpContext context,
[NotNull] OpenIdConnectRequest request,
[NotNull] ISecureDataFormat<AuthenticationTicket> format)
{
Debug.Assert(!(options.DisableTokenRevocation && options.UseReferenceTokens),
"Token revocation cannot be disabled when using reference tokens.");
Debug.Assert(type == OpenIdConnectConstants.TokenUsages.AccessToken ||
type == OpenIdConnectConstants.TokenUsages.AuthorizationCode ||
type == OpenIdConnectConstants.TokenUsages.RefreshToken,
"Only authorization codes, access and refresh tokens should be created using this method.");
// When sliding expiration is disabled, the expiration date of generated refresh tokens is fixed
// and must exactly match the expiration date of the refresh token used in the token request.
if (request.IsTokenRequest() && request.IsRefreshTokenGrantType() &&
!options.UseSlidingExpiration && type == OpenIdConnectConstants.TokenUsages.RefreshToken)
{
var properties = request.GetProperty<AuthenticationTicket>(
OpenIddictConstants.Properties.AuthenticationTicket)?.Properties;
Debug.Assert(properties != null, "The authentication properties shouldn't be null.");
ticket.Properties.ExpiresUtc = properties.ExpiresUtc;
}
if (options.DisableTokenRevocation)
{
return null;
}
var descriptor = new OpenIddictTokenDescriptor
{
AuthorizationId = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId),
CreationDate = ticket.Properties.IssuedUtc,
ExpirationDate = ticket.Properties.ExpiresUtc,
Status = OpenIddictConstants.Statuses.Valid,
Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject),
Type = type
};
string result = null;
// When reference tokens are enabled or when the token is an authorization code or a
// refresh token, remove the unnecessary properties from the authentication ticket.
if (options.UseReferenceTokens ||
(type == OpenIdConnectConstants.TokenUsages.AuthorizationCode ||
type == OpenIdConnectConstants.TokenUsages.RefreshToken))
{
ticket.Properties.IssuedUtc = ticket.Properties.ExpiresUtc = null;
ticket.RemoveProperty(OpenIddictConstants.Properties.AuthorizationId)
.RemoveProperty(OpenIdConnectConstants.Properties.TokenId);
}
// If reference tokens are enabled, create a new entry for
// authorization codes, refresh tokens and access tokens.
if (options.UseReferenceTokens)
{
// Note: the data format is automatically replaced at startup time to ensure
// that encrypted tokens stored in the database cannot be considered as
// valid tokens if the developer decides to disable reference tokens support.
descriptor.Ciphertext = format.Protect(ticket);
// Generate a new crypto-secure random identifier that will be
// substituted to the ciphertext returned by the data format.
var bytes = new byte[256 / 8];
options.RandomNumberGenerator.GetBytes(bytes);
result = Base64UrlEncoder.Encode(bytes);
// Compute the digest of the generated identifier and use
// it as the hashed identifier of the reference token.
// Doing that prevents token identifiers stolen from
// the database from being used as valid reference tokens.
using (var algorithm = SHA256.Create())
{
descriptor.Hash = Convert.ToBase64String(algorithm.ComputeHash(bytes));
}
}
// Otherwise, only create a token metadata entry for authorization codes and refresh tokens.
else if (type != OpenIdConnectConstants.TokenUsages.AuthorizationCode &&
type != OpenIdConnectConstants.TokenUsages.RefreshToken)
{
return null;
}
// If the client application is known, associate it with the token.
if (!string.IsNullOrEmpty(request.ClientId))
{
var application = await Applications.FindByClientIdAsync(request.ClientId, context.RequestAborted);
if (application == null)
{
throw new InvalidOperationException("The client application cannot be retrieved from the database.");
}
descriptor.ApplicationId = await Applications.GetIdAsync(application, context.RequestAborted);
}
// If a null value was returned by CreateAsync(), return immediately.
var token = await Tokens.CreateAsync(descriptor, context.RequestAborted);
if (token == null)
{
return null;
}
// Throw an exception if the token identifier can't be resolved.
var identifier = await Tokens.GetIdAsync(token, context.RequestAborted);
if (string.IsNullOrEmpty(identifier))
{
throw new InvalidOperationException("The unique key associated with a refresh token cannot be null or empty.");
}
// Restore the token identifier using the unique
// identifier attached with the database entry.
ticket.SetTokenId(identifier);
// Dynamically set the creation and expiration dates.
ticket.Properties.IssuedUtc = await Tokens.GetCreationDateAsync(token, context.RequestAborted);
ticket.Properties.ExpiresUtc = await Tokens.GetExpirationDateAsync(token, context.RequestAborted);
// Restore the authorization identifier using the identifier attached with the database entry.
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId,
await Tokens.GetAuthorizationIdAsync(token, context.RequestAborted));
if (!string.IsNullOrEmpty(result))
{
Logger.LogTrace("A new reference token was successfully generated and persisted " +
"in the database: {Token} ; {Claims} ; {Properties}.",
result, ticket.Principal.Claims, ticket.Properties.Items);
}
return result;
}
private async Task<AuthenticationTicket> ReceiveTokenAsync(
[NotNull] string value, [NotNull] OpenIddictOptions options,
[NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request,
[NotNull] ISecureDataFormat<AuthenticationTicket> format)
{
if (!options.UseReferenceTokens)
{
return null;
}
string hash;
try
{
// Compute the digest of the received token and use it
// to retrieve the reference token from the database.
using (var algorithm = SHA256.Create())
{
hash = Convert.ToBase64String(algorithm.ComputeHash(Base64UrlEncoder.DecodeBytes(value)));
}
}
// Swallow format-related exceptions to ensure badly formed
// or tampered tokens don't cause an exception at this stage.
catch
{
return null;
}
// Retrieve the token entry from the database. If it
// cannot be found, assume the token is not valid.
var token = await Tokens.FindByHashAsync(hash, context.RequestAborted);
if (token == null)
{
Logger.LogInformation("The reference token corresponding to the '{Hash}' hashed " +
"identifier cannot be found in the database.", hash);
return null;
}
var identifier = await Tokens.GetIdAsync(token, context.RequestAborted);
if (string.IsNullOrEmpty(identifier))
{
Logger.LogWarning("The identifier associated with the received token cannot be retrieved. " +
"This may indicate that the token entry is corrupted.");
return null;
}
// Extract the encrypted payload from the token. If it's null or empty,
// assume the token is not a reference token and consider it as invalid.
var ciphertext = await Tokens.GetCiphertextAsync(token, context.RequestAborted);
if (string.IsNullOrEmpty(ciphertext))
{
Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be retrieved. " +
"This may indicate that the token is not a reference token.", identifier);
return null;
}
var ticket = format.Unprotect(ciphertext);
if (ticket == null)
{
Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be decrypted. " +
"This may indicate that the token entry is corrupted or tampered.",
await Tokens.GetIdAsync(token, context.RequestAborted));
return null;
}
// Restore the token identifier using the unique
// identifier attached with the database entry.
ticket.SetTokenId(identifier);
// Dynamically set the creation and expiration dates.
ticket.Properties.IssuedUtc = await Tokens.GetCreationDateAsync(token, context.RequestAborted);
ticket.Properties.ExpiresUtc = await Tokens.GetExpirationDateAsync(token, context.RequestAborted);
// Restore the authorization identifier using the identifier attached with the database entry.
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId,
await Tokens.GetAuthorizationIdAsync(token, context.RequestAborted));
Logger.LogTrace("The reference token '{Identifier}' was successfully retrieved " +
"from the database and decrypted: {Claims} ; {Properties}.",
identifier, ticket.Principal.Claims, ticket.Properties.Items);
return ticket;
}
private async Task RevokeAuthorizationAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context)
{
var identifier = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId);
if (string.IsNullOrEmpty(identifier))
{
return;
}
var authorization = await Authorizations.FindByIdAsync(identifier, context.RequestAborted);
if (authorization == null)
{
return;
}
await Authorizations.RevokeAsync(authorization, context.RequestAborted);
Logger.LogInformation("The authorization '{Identifier}' was automatically revoked.", identifier);
}
private async Task RevokeTokensAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context)
{
var identifier = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId);
if (string.IsNullOrEmpty(identifier))
{
return;
}
foreach (var token in await Tokens.FindByAuthorizationIdAsync(identifier, context.RequestAborted))
{
// Don't overwrite the status of the token used in the token request.
if (string.Equals(ticket.GetTokenId(), await Tokens.GetIdAsync(token, context.RequestAborted)))
{
continue;
}
await Tokens.RevokeAsync(token, context.RequestAborted);
Logger.LogInformation("The token '{Identifier}' was automatically revoked.",
await Tokens.GetIdAsync(token, context.RequestAborted));
}
}
}
}

290
src/OpenIddict/OpenIddictProvider.Serialization.cs

@ -4,19 +4,11 @@
* the license and the contributors participating to this project.
*/
using System;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Core;
namespace OpenIddict
{
@ -116,36 +108,11 @@ namespace OpenIddict
public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context)
{
var options = (OpenIddictOptions) context.Options;
Debug.Assert(context.Request.IsTokenRequest(), "The request should be a token request.");
// When rolling tokens are disabled, extend the expiration date associated with the
// existing token instead of returning a new refresh token with a new expiration date.
if (options.UseSlidingExpiration && !options.UseRollingTokens && context.Request.IsRefreshTokenGrantType())
{
var identifier = context.Request.GetProperty<string>(OpenIddictConstants.Properties.TokenId);
var entry = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
if (entry != null)
{
Logger.LogInformation("The expiration date of the '{Identifier}' token was automatically updated: {Date}.",
identifier, context.Ticket.Properties.ExpiresUtc);
await Tokens.ExtendAsync(entry, context.Ticket.Properties.ExpiresUtc, context.HttpContext.RequestAborted);
context.RefreshToken = null;
context.HandleSerialization();
return;
}
// If the refresh token entry could not be
// found in the database, generate a new one.
}
var token = await CreateTokenAsync(
OpenIdConnectConstants.TokenUsages.RefreshToken, context.Ticket, options,
OpenIdConnectConstants.TokenUsages.RefreshToken,
context.Ticket, (OpenIddictOptions) context.Options,
context.HttpContext, context.Request, context.DataFormat);
// If a reference token was returned by CreateTokenAsync(),
@ -159,258 +126,5 @@ namespace OpenIddict
// Otherwise, let the OpenID Connect server middleware
// serialize the token using its default internal logic.
}
private async Task<string> CreateTokenAsync(
[NotNull] string type, [NotNull] AuthenticationTicket ticket,
[NotNull] OpenIddictOptions options, [NotNull] HttpContext context,
[NotNull] OpenIdConnectRequest request,
[NotNull] ISecureDataFormat<AuthenticationTicket> format)
{
Debug.Assert(!(options.DisableTokenRevocation && options.UseReferenceTokens),
"Token revocation cannot be disabled when using reference tokens.");
Debug.Assert(!(options.DisableTokenRevocation && options.UseRollingTokens),
"Token revocation cannot be disabled when using rolling tokens.");
Debug.Assert(type != OpenIdConnectConstants.TokenUsages.IdToken,
"Identity tokens shouldn't be stored in the database.");
if (options.DisableTokenRevocation)
{
return null;
}
var descriptor = new OpenIddictTokenDescriptor
{
CreationDate = ticket.Properties.IssuedUtc,
ExpirationDate = ticket.Properties.ExpiresUtc,
Status = OpenIddictConstants.Statuses.Valid,
Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject),
Type = type
};
string result = null;
// When reference tokens are enabled or when the token is an authorization code or a
// refresh token, remove the unnecessary properties from the authentication ticket.
if (options.UseReferenceTokens ||
(type == OpenIdConnectConstants.TokenUsages.AuthorizationCode ||
type == OpenIdConnectConstants.TokenUsages.RefreshToken))
{
ticket.Properties.IssuedUtc = ticket.Properties.ExpiresUtc = null;
ticket.RemoveProperty(OpenIdConnectConstants.Properties.TokenId);
}
// If reference tokens are enabled, create a new entry for
// authorization codes, refresh tokens and access tokens.
if (options.UseReferenceTokens)
{
// Note: the data format is automatically replaced at startup time to ensure
// that encrypted tokens stored in the database cannot be considered as
// valid tokens if the developer decides to disable reference tokens support.
descriptor.Ciphertext = format.Protect(ticket);
// Generate a new crypto-secure random identifier that will be
// substituted to the ciphertext returned by the data format.
var bytes = new byte[256 / 8];
options.RandomNumberGenerator.GetBytes(bytes);
result = Base64UrlEncoder.Encode(bytes);
// Compute the digest of the generated identifier and use
// it as the hashed identifier of the reference token.
// Doing that prevents token identifiers stolen from
// the database from being used as valid reference tokens.
using (var algorithm = SHA256.Create())
{
descriptor.Hash = Convert.ToBase64String(algorithm.ComputeHash(bytes));
}
}
// Otherwise, only create a token metadata entry for authorization codes and refresh tokens.
else if (type != OpenIdConnectConstants.TokenUsages.AuthorizationCode &&
type != OpenIdConnectConstants.TokenUsages.RefreshToken)
{
return null;
}
// If the client application is known, associate it with the token.
if (!string.IsNullOrEmpty(request.ClientId))
{
var application = await Applications.FindByClientIdAsync(request.ClientId, context.RequestAborted);
if (application == null)
{
throw new InvalidOperationException("The client application cannot be retrieved from the database.");
}
descriptor.ApplicationId = await Applications.GetIdAsync(application, context.RequestAborted);
}
// If an authorization identifier was specified, bind it to the token.
if (ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId))
{
descriptor.AuthorizationId = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId);
}
// Otherwise, create an ad hoc authorization if the token is an authorization code.
else if (type == OpenIdConnectConstants.TokenUsages.AuthorizationCode)
{
Debug.Assert(!string.IsNullOrEmpty(descriptor.ApplicationId), "The client identifier shouldn't be null.");
var authorization = await CreateAuthorizationAsync(descriptor, context, request);
if (authorization != null)
{
descriptor.AuthorizationId = await Authorizations.GetIdAsync(authorization, context.RequestAborted);
Logger.LogInformation("An ad hoc authorization was automatically created and " +
"associated with the '{ClientId}' application: {Identifier}.",
request.ClientId, descriptor.AuthorizationId);
}
}
// If a null value was returned by CreateAsync(), return immediately.
var token = await Tokens.CreateAsync(descriptor, context.RequestAborted);
if (token == null)
{
return null;
}
// Throw an exception if the token identifier can't be resolved.
var identifier = await Tokens.GetIdAsync(token, context.RequestAborted);
if (string.IsNullOrEmpty(identifier))
{
throw new InvalidOperationException("The unique key associated with a refresh token cannot be null or empty.");
}
// Restore the token identifier using the unique
// identifier attached with the database entry.
ticket.SetTokenId(identifier);
// Dynamically set the creation and expiration dates.
ticket.Properties.IssuedUtc = await Tokens.GetCreationDateAsync(token, context.RequestAborted);
ticket.Properties.ExpiresUtc = await Tokens.GetExpirationDateAsync(token, context.RequestAborted);
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, descriptor.AuthorizationId);
if (!string.IsNullOrEmpty(result))
{
Logger.LogTrace("A new reference token was successfully generated and persisted " +
"in the database: {Token} ; {Claims} ; {Properties}.",
result, ticket.Principal.Claims, ticket.Properties.Items);
}
return result;
}
private async Task<AuthenticationTicket> ReceiveTokenAsync(
[NotNull] string value, [NotNull] OpenIddictOptions options,
[NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request,
[NotNull] ISecureDataFormat<AuthenticationTicket> format)
{
if (!options.UseReferenceTokens)
{
return null;
}
string hash;
try
{
// Compute the digest of the received token and use it
// to retrieve the reference token from the database.
using (var algorithm = SHA256.Create())
{
hash = Convert.ToBase64String(algorithm.ComputeHash(Base64UrlEncoder.DecodeBytes(value)));
}
}
// Swallow format-related exceptions to ensure badly formed
// or tampered tokens don't cause an exception at this stage.
catch
{
return null;
}
// Retrieve the token entry from the database. If it
// cannot be found, assume the token is not valid.
var token = await Tokens.FindByHashAsync(hash, context.RequestAborted);
if (token == null)
{
Logger.LogInformation("The reference token corresponding to the '{Hash}' hashed " +
"identifier cannot be found in the database.", hash);
return null;
}
var identifier = await Tokens.GetIdAsync(token, context.RequestAborted);
if (string.IsNullOrEmpty(identifier))
{
Logger.LogWarning("The identifier associated with the received token cannot be retrieved. " +
"This may indicate that the token entry is corrupted.");
return null;
}
// Extract the encrypted payload from the token. If it's null or empty,
// assume the token is not a reference token and consider it as invalid.
var ciphertext = await Tokens.GetCiphertextAsync(token, context.RequestAborted);
if (string.IsNullOrEmpty(ciphertext))
{
Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be retrieved. " +
"This may indicate that the token is not a reference token.", identifier);
return null;
}
var ticket = format.Unprotect(ciphertext);
if (ticket == null)
{
Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be decrypted. " +
"This may indicate that the token entry is corrupted or tampered.",
await Tokens.GetIdAsync(token, context.RequestAborted));
return null;
}
// Restore the token identifier using the unique
// identifier attached with the database entry.
ticket.SetTokenId(identifier);
// Dynamically set the creation and expiration dates.
ticket.Properties.IssuedUtc = await Tokens.GetCreationDateAsync(token, context.RequestAborted);
ticket.Properties.ExpiresUtc = await Tokens.GetExpirationDateAsync(token, context.RequestAborted);
// If the authorization identifier cannot be found in the ticket properties,
// try to restore it using the identifier associated with the database entry.
if (!ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId))
{
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId,
await Tokens.GetAuthorizationIdAsync(token, context.RequestAborted));
}
Logger.LogTrace("The reference token '{Identifier}' was successfully retrieved " +
"from the database and decrypted: {Claims} ; {Properties}.",
identifier, ticket.Principal.Claims, ticket.Properties.Items);
return ticket;
}
private Task<TAuthorization> CreateAuthorizationAsync(
[NotNull] OpenIddictTokenDescriptor token,
[NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request)
{
var descriptor = new OpenIddictAuthorizationDescriptor
{
ApplicationId = token.ApplicationId,
Status = OpenIddictConstants.Statuses.Valid,
Subject = token.Subject,
Type = OpenIddictConstants.AuthorizationTypes.AdHoc
};
foreach (var scope in request.GetScopes())
{
descriptor.Scopes.Add(scope);
}
return Authorizations.CreateAsync(descriptor, context.RequestAborted);
}
}
}

142
src/OpenIddict/OpenIddictProvider.Signin.cs

@ -0,0 +1,142 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Diagnostics;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using OpenIddict.Core;
namespace OpenIddict
{
public partial class OpenIddictProvider<TApplication, TAuthorization, TScope, TToken> : OpenIdConnectServerProvider
where TApplication : class where TAuthorization : class where TScope : class where TToken : class
{
public override async Task ProcessSigninResponse([NotNull] ProcessSigninResponseContext context)
{
var options = (OpenIddictOptions) context.Options;
if (context.Request.IsTokenRequest() && (context.Request.IsAuthorizationCodeGrantType() ||
context.Request.IsRefreshTokenGrantType()))
{
// Note: when handling a grant_type=authorization_code or refresh_token request,
// the OpenID Connect server middleware allows creating authentication tickets
// that are completely disconnected from the original code or refresh token ticket.
// This scenario is deliberately not supported in OpenIddict and all the tickets
// must be linked. To ensure the properties are preserved from an authorization code
// or a refresh token to the new ticket, they are manually restored if necessary.
// Retrieve the original authentication ticket from the request properties.
var ticket = context.Request.GetProperty<AuthenticationTicket>(
OpenIddictConstants.Properties.AuthenticationTicket);
Debug.Assert(ticket != null, "The authentication ticket shouldn't be null.");
// If the properties instances of the two authentication tickets differ,
// restore the missing properties in the new authentication ticket.
if (!ReferenceEquals(ticket.Properties, context.Ticket.Properties))
{
foreach (var property in ticket.Properties.Items)
{
// Don't override the properties that have been
// manually set on the new authentication ticket.
if (context.Ticket.HasProperty(property.Key))
{
continue;
}
context.Ticket.AddProperty(property.Key, property.Value);
}
// Always include the "openid" scope when the developer doesn't explicitly call SetScopes.
// Note: the application is allowed to specify a different "scopes": in this case,
// don't replace the "scopes" property stored in the authentication ticket.
if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OpenId) && !context.Ticket.HasScope())
{
context.Ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId);
}
context.IncludeIdentityToken = context.Ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId);
}
// Always include a refresh token for grant_type=refresh_token requests if
// rolling tokens are enabled and if the offline_access scope was specified.
context.IncludeRefreshToken = context.Request.IsRefreshTokenGrantType() && options.UseRollingTokens &&
context.Ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess);
// If token revocation was explicitly disabled,
// none of the following security routines apply.
if (options.DisableTokenRevocation)
{
return;
}
// Extract the token identifier from the authentication ticket.
var identifier = context.Ticket.GetTokenId();
Debug.Assert(!string.IsNullOrEmpty(identifier),
"The authentication ticket should contain a ticket identifier.");
// If rolling tokens are enabled or if the request is a grant_type=authorization_code request,
// mark the authorization code or the refresh token as redeemed to prevent future reuses.
// See https://tools.ietf.org/html/rfc6749#section-6 for more information.
if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType())
{
var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
if (token != null)
{
await Tokens.RedeemAsync(token, context.HttpContext.RequestAborted);
Logger.LogInformation("The token '{Identifier}' was automatically marked as redeemed.", identifier);
}
}
// When rolling tokens are enabled, revoke all the previously issued tokens associated
// with the authorization if the request is a grant_type=refresh_token request.
if (options.UseRollingTokens && context.Request.IsRefreshTokenGrantType())
{
await RevokeTokensAsync(context.Ticket, context.HttpContext);
}
// When rolling tokens are disabled, extend the expiration date
// of the existing token instead of returning a new refresh token
// with a new expiration date if sliding expiration was not disabled.
else if (options.UseSlidingExpiration && context.Request.IsRefreshTokenGrantType())
{
var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
if (token != null)
{
// Compute the new expiration date of the refresh token.
var date = context.Options.SystemClock.UtcNow +
(context.Ticket.GetRefreshTokenLifetime() ??
context.Options.RefreshTokenLifetime);
await Tokens.ExtendAsync(token, date, context.HttpContext.RequestAborted);
Logger.LogInformation("The expiration date of the refresh token '{Identifier}' " +
"was automatically updated: {Date}.", identifier, date);
context.IncludeRefreshToken = false;
}
// If the refresh token entry could not be
// found in the database, generate a new one.
}
}
// If no authorization was explicitly attached to the authentication ticket,
// create an ad hoc authorization if an authorization code or a refresh token
// is going to be returned to the client application as part of the response.
if (!context.Ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId) &&
(context.IncludeAuthorizationCode || context.IncludeRefreshToken))
{
await CreateAuthorizationAsync(context.Ticket, options, context.HttpContext, context.Request);
}
}
}
}

31
test/OpenIddict.Tests/OpenIddictInitializerTests.cs

@ -162,38 +162,14 @@ namespace OpenIddict.Tests
}
[Fact]
public async Task PostConfigure_ThrowsAnExceptionWhenUsingRollingTokensWithTokenRevocationDisabled()
public async Task PostConfigure_ThrowsAnExceptionWhenUsingSlidingExpirationWithoutRollingTokensAndWithTokenRevocationDisabled()
{
// Arrange
var server = CreateAuthorizationServer(builder =>
{
builder.EnableAuthorizationEndpoint("/connect/authorize")
.AllowImplicitFlow()
.DisableTokenRevocation()
.UseRollingTokens();
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act and assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(delegate
{
return client.GetAsync("/");
});
Assert.Equal("Rolling tokens cannot be used when disabling token expiration.", exception.Message);
}
[Fact]
public async Task PostConfigure_ThrowsAnExceptionWhenUsingRollingTokensWithSlidingExpirationDisabled()
{
// Arrange
var server = CreateAuthorizationServer(builder =>
{
builder.EnableAuthorizationEndpoint("/connect/authorize")
.AllowImplicitFlow()
.UseRollingTokens()
.Configure(options => options.UseSlidingExpiration = false);
.DisableTokenRevocation();
});
var client = new OpenIdConnectClient(server.CreateClient());
@ -204,7 +180,8 @@ namespace OpenIddict.Tests
return client.GetAsync("/");
});
Assert.Equal("Rolling tokens cannot be used without enabling sliding expiration.", exception.Message);
Assert.Equal("Sliding expiration must be disabled when turning off " +
"token revocation if rolling tokens are not used.", exception.Message);
}
[Fact]

300
test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs

@ -436,6 +436,7 @@ namespace OpenIddict.Tests
builder.Configure(options => options.RevocationEndpointPath = PathString.Empty);
builder.DisableTokenRevocation();
builder.DisableSlidingExpiration();
});
var client = new OpenIdConnectClient(server.CreateClient());
@ -487,6 +488,7 @@ namespace OpenIddict.Tests
builder.Configure(options => options.RevocationEndpointPath = PathString.Empty);
builder.DisableTokenRevocation();
builder.DisableSlidingExpiration();
});
var client = new OpenIdConnectClient(server.CreateClient());
@ -562,6 +564,63 @@ namespace OpenIddict.Tests
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsUnknown()
{
// Arrange
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("8xLOxBtZp8"))
.Returns(ticket);
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(value: null);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(OpenIddictConstants.ClientTypes.Public);
}));
builder.Services.AddSingleton(manager);
builder.Configure(options => options.RefreshTokenFormat = format.Object);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsAlreadyRedeemed()
{
@ -629,7 +688,7 @@ namespace OpenIddict.Tests
}
[Fact]
public async Task HandleTokenRequest_RevokesTokensWhenAuthorizationCodeIsAlreadyRedeemed()
public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsAlreadyRedeemed()
{
// Arrange
var ticket = new AuthenticationTicket(
@ -637,33 +696,23 @@ namespace OpenIddict.Tests
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetPresenters("Fabrikam");
ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode);
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA"))
format.Setup(mock => mock.Unprotect("8xLOxBtZp8"))
.Returns(ticket);
var tokens = new[]
{
new OpenIddictToken(),
new OpenIddictToken(),
new OpenIddictToken()
};
var token = new OpenIddictToken();
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(tokens[0]);
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny<CancellationToken>()))
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(tokens);
});
var server = CreateAuthorizationServer(builder =>
@ -679,17 +728,9 @@ namespace OpenIddict.Tests
.ReturnsAsync(OpenIddictConstants.ClientTypes.Public);
}));
builder.Services.AddSingleton(CreateAuthorizationManager(instance =>
{
var authorization = new OpenIddictAuthorization();
instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(authorization);
}));
builder.Services.AddSingleton(manager);
builder.Configure(options => options.AuthorizationCodeFormat = format.Object);
builder.Configure(options => options.RefreshTokenFormat = format.Object);
});
var client = new OpenIdConnectClient(server.CreateClient());
@ -697,25 +738,20 @@ namespace OpenIddict.Tests
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
ClientId = "Fabrikam",
Code = "SplxlOBeZQQYbYS6WxSbIA",
GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode,
RedirectUri = "http://www.fabrikam.com/path"
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription);
Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsInvalid()
public async Task HandleTokenRequest_RevokesAuthorizationWhenAuthorizationCodeIsAlreadyRedeemed()
{
// Arrange
var ticket = new AuthenticationTicket(
@ -726,24 +762,19 @@ namespace OpenIddict.Tests
ticket.SetPresenters("Fabrikam");
ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode);
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA"))
.Returns(ticket);
var token = new OpenIddictToken();
var authorization = new OpenIddictAuthorization();
var manager = CreateTokenManager(instance =>
var manager = CreateAuthorizationManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(authorization);
});
var server = CreateAuthorizationServer(builder =>
@ -759,6 +790,17 @@ namespace OpenIddict.Tests
.ReturnsAsync(OpenIddictConstants.ClientTypes.Public);
}));
builder.Services.AddSingleton(CreateTokenManager(instance =>
{
var token = new OpenIddictToken();
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
builder.Services.AddSingleton(manager);
builder.Configure(options => options.AuthorizationCodeFormat = format.Object);
@ -777,15 +819,14 @@ namespace OpenIddict.Tests
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription);
Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(authorization, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsUnknown()
public async Task HandleTokenRequest_RevokesAuthorizationWhenRefreshTokenIsAlreadyRedeemed()
{
// Arrange
var ticket = new AuthenticationTicket(
@ -793,18 +834,21 @@ namespace OpenIddict.Tests
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103");
ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("8xLOxBtZp8"))
.Returns(ticket);
var manager = CreateTokenManager(instance =>
var authorization = new OpenIddictAuthorization();
var manager = CreateAuthorizationManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(value: null);
instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(authorization);
});
var server = CreateAuthorizationServer(builder =>
@ -820,6 +864,17 @@ namespace OpenIddict.Tests
.ReturnsAsync(OpenIddictConstants.ClientTypes.Public);
}));
builder.Services.AddSingleton(CreateTokenManager(instance =>
{
var token = new OpenIddictToken();
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
builder.Services.AddSingleton(manager);
builder.Configure(options => options.RefreshTokenFormat = format.Object);
@ -836,13 +891,14 @@ namespace OpenIddict.Tests
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription);
Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(authorization, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsAlreadyRedeemed()
public async Task HandleTokenRequest_RevokesTokensWhenAuthorizationCodeIsAlreadyRedeemed()
{
// Arrange
var ticket = new AuthenticationTicket(
@ -850,23 +906,33 @@ namespace OpenIddict.Tests
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
ticket.SetPresenters("Fabrikam");
ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode);
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("8xLOxBtZp8"))
format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA"))
.Returns(ticket);
var token = new OpenIddictToken();
var tokens = new[]
{
new OpenIddictToken(),
new OpenIddictToken(),
new OpenIddictToken()
};
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(tokens[0]);
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(tokens);
});
var server = CreateAuthorizationServer(builder =>
@ -882,9 +948,17 @@ namespace OpenIddict.Tests
.ReturnsAsync(OpenIddictConstants.ClientTypes.Public);
}));
builder.Services.AddSingleton(CreateAuthorizationManager(instance =>
{
var authorization = new OpenIddictAuthorization();
instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(authorization);
}));
builder.Services.AddSingleton(manager);
builder.Configure(options => options.RefreshTokenFormat = format.Object);
builder.Configure(options => options.AuthorizationCodeFormat = format.Object);
});
var client = new OpenIdConnectClient(server.CreateClient());
@ -892,20 +966,25 @@ namespace OpenIddict.Tests
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
ClientId = "Fabrikam",
Code = "SplxlOBeZQQYbYS6WxSbIA",
GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode,
RedirectUri = "http://www.fabrikam.com/path"
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription);
Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsInvalid()
public async Task HandleTokenRequest_RevokesTokensWhenRefreshTokenIsAlreadyRedeemed()
{
// Arrange
var ticket = new AuthenticationTicket(
@ -913,26 +992,32 @@ namespace OpenIddict.Tests
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103");
ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("8xLOxBtZp8"))
.Returns(ticket);
var token = new OpenIddictToken();
var tokens = new[]
{
new OpenIddictToken(),
new OpenIddictToken(),
new OpenIddictToken()
};
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(tokens[0]);
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(tokens);
});
var server = CreateAuthorizationServer(builder =>
@ -948,6 +1033,14 @@ namespace OpenIddict.Tests
.ReturnsAsync(OpenIddictConstants.ClientTypes.Public);
}));
builder.Services.AddSingleton(CreateAuthorizationManager(instance =>
{
var authorization = new OpenIddictAuthorization();
instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(authorization);
}));
builder.Services.AddSingleton(manager);
builder.Configure(options => options.RefreshTokenFormat = format.Object);
@ -964,21 +1057,21 @@ namespace OpenIddict.Tests
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription);
Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleTokenRequest_AuthorizationCodeIsAutomaticallyRedeemed()
public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsInvalid()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur");
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new ClaimsPrincipal(),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
@ -998,8 +1091,11 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
.ReturnsAsync(false);
});
var server = CreateAuthorizationServer(builder =>
@ -1032,19 +1128,20 @@ namespace OpenIddict.Tests
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleTokenRequest_RefreshTokenIsAutomaticallyRedeemedWhenRollingTokensAreEnabled()
public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsInvalid()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur");
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new ClaimsPrincipal(),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
@ -1067,7 +1164,7 @@ namespace OpenIddict.Tests
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
.ReturnsAsync(false);
});
var server = CreateAuthorizationServer(builder =>
@ -1085,8 +1182,6 @@ namespace OpenIddict.Tests
builder.Services.AddSingleton(manager);
builder.UseRollingTokens();
builder.Configure(options => options.RefreshTokenFormat = format.Object);
});
@ -1100,8 +1195,11 @@ namespace OpenIddict.Tests
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Theory]

2
test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs

@ -272,6 +272,7 @@ namespace OpenIddict.Tests
builder.Configure(options => options.RevocationEndpointPath = PathString.Empty);
builder.DisableTokenRevocation();
builder.DisableSlidingExpiration();
});
var client = new OpenIdConnectClient(server.CreateClient());
@ -328,6 +329,7 @@ namespace OpenIddict.Tests
builder.Configure(options => options.RevocationEndpointPath = PathString.Empty);
builder.DisableTokenRevocation();
builder.DisableSlidingExpiration();
});
var client = new OpenIdConnectClient(server.CreateClient());

1290
test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs

File diff suppressed because it is too large

721
test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs

@ -0,0 +1,721 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Client;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using OpenIddict.Core;
using OpenIddict.Models;
using Xunit;
namespace OpenIddict.Tests
{
public partial class OpenIddictProviderTests
{
[Fact]
public async Task ProcessSigninResponse_AuthenticationPropertiesAreAutomaticallyRestored()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur");
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess);
ticket.SetProperty("custom_property_in_original_ticket", "original_value");
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Protect(It.IsAny<AuthenticationTicket>()))
.Returns("8xLOxBtZp8");
format.Setup(mock => mock.Unprotect("8xLOxBtZp8"))
.Returns(ticket);
var token = new OpenIddictToken();
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.UseRollingTokens();
builder.Configure(options => options.RefreshTokenFormat = format.Object);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8",
["do-not-flow-original-properties"] = true
});
// Assert
Assert.NotNull(response.IdToken);
Assert.NotNull(response.RefreshToken);
format.Verify(mock => mock.Protect(
It.Is<AuthenticationTicket>(value =>
value.Properties.Items["custom_property_in_original_ticket"] == "original_value" &&
value.Properties.Items["custom_property_in_new_ticket"] == "new_value")));
}
[Fact]
public async Task ProcessSigninResponse_RefreshTokenIsAlwaysIssuedWhenRollingTokensAreEnabled()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur");
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess);
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Protect(It.IsAny<AuthenticationTicket>()))
.Returns("8xLOxBtZp8");
format.Setup(mock => mock.Unprotect("8xLOxBtZp8"))
.Returns(ticket);
var token = new OpenIddictToken();
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.UseRollingTokens();
builder.Configure(options => options.RefreshTokenFormat = format.Object);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.NotNull(response.RefreshToken);
}
[Fact]
public async Task ProcessSigninResponse_RefreshTokenIsNotIssuedWhenRollingTokensAreDisabled()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur");
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess);
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Protect(It.IsAny<AuthenticationTicket>()))
.Returns("8xLOxBtZp8");
format.Setup(mock => mock.Unprotect("8xLOxBtZp8"))
.Returns(ticket);
var token = new OpenIddictToken();
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.Configure(options => options.RefreshTokenFormat = format.Object);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.Null(response.RefreshToken);
}
[Fact]
public async Task ProcessSigninResponse_AuthorizationCodeIsAutomaticallyRedeemed()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur");
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetPresenters("Fabrikam");
ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode);
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA"))
.Returns(ticket);
var token = new OpenIddictToken();
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(OpenIddictConstants.ClientTypes.Public);
}));
builder.Services.AddSingleton(manager);
builder.Configure(options => options.AuthorizationCodeFormat = format.Object);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
ClientId = "Fabrikam",
Code = "SplxlOBeZQQYbYS6WxSbIA",
GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode,
RedirectUri = "http://www.fabrikam.com/path"
});
// Assert
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Exactly(2));
Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ProcessSigninResponse_RefreshTokenIsAutomaticallyRedeemedWhenRollingTokensAreEnabled()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur");
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess);
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("8xLOxBtZp8"))
.Returns(ticket);
var token = new OpenIddictToken();
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.UseRollingTokens();
builder.Configure(options => options.RefreshTokenFormat = format.Object);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Exactly(2));
Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ProcessSigninResponse_RefreshTokenIsNotRedeemedWhenRollingTokensAreDisabled()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur");
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess);
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("8xLOxBtZp8"))
.Returns(ticket);
var token = new OpenIddictToken();
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.Configure(options => options.RefreshTokenFormat = format.Object);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Exactly(2));
Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task ProcessSigninResponse_PreviousTokensAreAutomaticallyRevokedWhenRollingTokensAreEnabled()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur");
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess);
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("8xLOxBtZp8"))
.Returns(ticket);
var tokens = new[]
{
new OpenIddictToken(),
new OpenIddictToken(),
new OpenIddictToken()
};
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(tokens[0]);
instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(tokens);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.UseRollingTokens();
builder.Configure(options => options.RefreshTokenFormat = format.Object);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Exactly(2));
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ProcessSigninResponse_PreviousTokensAreNotRevokedWhenRollingTokensAreDisabled()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur");
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess);
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("8xLOxBtZp8"))
.Returns(ticket);
var tokens = new[]
{
new OpenIddictToken(),
new OpenIddictToken(),
new OpenIddictToken()
};
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(tokens[0]);
instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(tokens);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.Configure(options => options.RefreshTokenFormat = format.Object);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Exactly(2));
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task ProcessSigninResponse_ExtendsLifetimeWhenRollingTokensAreDisabledAndSlidingExpirationEnabled()
{
// Arrange
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess);
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Protect(It.IsAny<AuthenticationTicket>()))
.Returns("8xLOxBtZp8");
format.Setup(mock => mock.Unprotect("8xLOxBtZp8"))
.Returns(ticket);
var token = new OpenIddictToken();
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.Configure(options =>
{
options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow ==
new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero));
options.RefreshTokenLifetime = TimeSpan.FromDays(10);
options.RefreshTokenFormat = format.Object;
});
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.Null(response.RefreshToken);
Mock.Get(manager).Verify(mock => mock.ExtendAsync(token,
new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero),
It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ProcessSigninResponse_DoesNotExtendLifetimeWhenSlidingExpirationIsDisabled()
{
// Arrange
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess);
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Protect(It.IsAny<AuthenticationTicket>()))
.Returns("8xLOxBtZp8");
format.Setup(mock => mock.Unprotect("8xLOxBtZp8"))
.Returns(ticket);
var token = new OpenIddictToken();
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.DisableSlidingExpiration();
builder.Configure(options =>
{
options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow ==
new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero));
options.RefreshTokenLifetime = TimeSpan.FromDays(10);
options.RefreshTokenFormat = format.Object;
});
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken,
RefreshToken = "8xLOxBtZp8"
});
// Assert
Assert.Null(response.RefreshToken);
Mock.Get(manager).Verify(mock => mock.ExtendAsync(token,
new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero),
It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task ProcessSigninResponse_AdHocAuthorizationIsAutomaticallyCreated()
{
// Arrange
var token = new OpenIddictToken();
var manager = CreateAuthorizationManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny<CancellationToken>()))
.ReturnsAsync(new OpenIddictAuthorization());
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(OpenIddictConstants.ClientTypes.Public);
instance.Setup(mock => mock.GetIdAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
}));
builder.Services.AddSingleton(CreateTokenManager(instance =>
{
instance.Setup(mock => mock.CreateAsync(It.IsAny<OpenIddictTokenDescriptor>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
}));
builder.Services.AddSingleton(manager);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest
{
ClientId = "Fabrikam",
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = OpenIdConnectConstants.ResponseTypes.Code,
});
// Assert
Assert.NotNull(response.Code);
Mock.Get(manager).Verify(mock => mock.CreateAsync(
It.Is<OpenIddictAuthorizationDescriptor>(descriptor =>
descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" &&
descriptor.Subject == "Bob le Magnifique" &&
descriptor.Type == OpenIddictConstants.AuthorizationTypes.AdHoc),
It.IsAny<CancellationToken>()), Times.Once());
}
}
}

28
test/OpenIddict.Tests/OpenIddictProviderTests.cs

@ -158,6 +158,14 @@ namespace OpenIddict.Tests
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70");
}
if (request.HasParameter("do-not-flow-original-properties"))
{
var properties = new AuthenticationProperties();
properties.SetProperty("custom_property_in_new_ticket", "new_value");
return context.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, properties);
}
return context.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties);
}
@ -184,46 +192,50 @@ namespace OpenIddict.Tests
return new TestServer(builder);
}
private static OpenIddictApplicationManager<OpenIddictApplication> CreateApplicationManager(Action<Mock<OpenIddictApplicationManager<OpenIddictApplication>>> setup = null)
private static OpenIddictApplicationManager<OpenIddictApplication> CreateApplicationManager(
Action<Mock<OpenIddictApplicationManager<OpenIddictApplication>>> configuration = null)
{
var manager = new Mock<OpenIddictApplicationManager<OpenIddictApplication>>(
Mock.Of<IOpenIddictApplicationStore<OpenIddictApplication>>(),
Mock.Of<ILogger<OpenIddictApplicationManager<OpenIddictApplication>>>());
setup?.Invoke(manager);
configuration?.Invoke(manager);
return manager.Object;
}
private static OpenIddictAuthorizationManager<OpenIddictAuthorization> CreateAuthorizationManager(Action<Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>> setup = null)
private static OpenIddictAuthorizationManager<OpenIddictAuthorization> CreateAuthorizationManager(
Action<Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>> configuration = null)
{
var manager = new Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>(
Mock.Of<IOpenIddictAuthorizationStore<OpenIddictAuthorization>>(),
Mock.Of<ILogger<OpenIddictAuthorizationManager<OpenIddictAuthorization>>>());
setup?.Invoke(manager);
configuration?.Invoke(manager);
return manager.Object;
}
private static OpenIddictScopeManager<OpenIddictScope> CreateScopeManager(Action<Mock<OpenIddictScopeManager<OpenIddictScope>>> setup = null)
private static OpenIddictScopeManager<OpenIddictScope> CreateScopeManager(
Action<Mock<OpenIddictScopeManager<OpenIddictScope>>> configuration = null)
{
var manager = new Mock<OpenIddictScopeManager<OpenIddictScope>>(
Mock.Of<IOpenIddictScopeStore<OpenIddictScope>>(),
Mock.Of<ILogger<OpenIddictScopeManager<OpenIddictScope>>>());
setup?.Invoke(manager);
configuration?.Invoke(manager);
return manager.Object;
}
private static OpenIddictTokenManager<OpenIddictToken> CreateTokenManager(Action<Mock<OpenIddictTokenManager<OpenIddictToken>>> setup = null)
private static OpenIddictTokenManager<OpenIddictToken> CreateTokenManager(
Action<Mock<OpenIddictTokenManager<OpenIddictToken>>> configuration = null)
{
var manager = new Mock<OpenIddictTokenManager<OpenIddictToken>>(
Mock.Of<IOpenIddictTokenStore<OpenIddictToken>>(),
Mock.Of<ILogger<OpenIddictTokenManager<OpenIddictToken>>>());
setup?.Invoke(manager);
configuration?.Invoke(manager);
return manager.Object;
}

Loading…
Cancel
Save