Browse Source

Backport the reference tokens support to OpenIddict 1.x

pull/553/head
Kévin Chalet 9 years ago
parent
commit
4338aa759b
  1. 2
      build/version.props
  2. 10
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  3. 76
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  4. 244
      src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
  5. 7
      src/OpenIddict.Core/OpenIddictConstants.cs
  6. 4
      src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs
  7. 47
      src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs
  8. 116
      src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs
  9. 3
      src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs
  10. 4
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
  11. 95
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs
  12. 220
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs
  13. 5
      src/OpenIddict.Models/OpenIddictAuthorization.cs
  14. 31
      src/OpenIddict.Models/OpenIddictToken.cs
  15. 83
      src/OpenIddict/OpenIddictExtensions.cs
  16. 18
      src/OpenIddict/OpenIddictOptions.cs
  17. 37
      src/OpenIddict/OpenIddictProvider.Authentication.cs
  18. 6
      src/OpenIddict/OpenIddictProvider.Discovery.cs
  19. 138
      src/OpenIddict/OpenIddictProvider.Exchange.cs
  20. 13
      src/OpenIddict/OpenIddictProvider.Introspection.cs
  21. 58
      src/OpenIddict/OpenIddictProvider.Revocation.cs
  22. 410
      src/OpenIddict/OpenIddictProvider.Serialization.cs
  23. 24
      src/OpenIddict/OpenIddictProvider.Session.cs
  24. 63
      test/OpenIddict.Tests/OpenIddictExtensionsTests.cs
  25. 8
      test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs
  26. 316
      test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs
  27. 282
      test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs
  28. 95
      test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs
  29. 477
      test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs
  30. 54
      test/OpenIddict.Tests/OpenIddictProviderTests.cs

2
build/version.props

@ -2,7 +2,7 @@
<PropertyGroup>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>beta2</VersionSuffix>
<VersionSuffix>rc1</VersionSuffix>
<VersionSuffix Condition=" '$(VersionSuffix)' != '' AND '$(BuildNumber)' != '' ">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
</PropertyGroup>

10
src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs

@ -162,9 +162,9 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns>
public virtual Task<TApplication[]> FindByLogoutRedirectUri(string address, CancellationToken cancellationToken)
public virtual Task<TApplication[]> FindByLogoutRedirectUriAsync(string address, CancellationToken cancellationToken)
{
return Store.FindByLogoutRedirectUri(address, cancellationToken);
return Store.FindByLogoutRedirectUriAsync(address, cancellationToken);
}
/// <summary>
@ -176,9 +176,9 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
/// </returns>
public virtual Task<TApplication[]> FindByRedirectUri(string address, CancellationToken cancellationToken)
public virtual Task<TApplication[]> FindByRedirectUriAsync(string address, CancellationToken cancellationToken)
{
return Store.FindByRedirectUri(address, cancellationToken);
return Store.FindByRedirectUriAsync(address, cancellationToken);
}
/// <summary>
@ -527,7 +527,7 @@ namespace OpenIddict.Core
{
// Warning: SQL engines like Microsoft SQL Server are known to use case-insensitive lookups by default.
// To ensure a case-sensitive comparison is used, string.Equals(Ordinal) is manually called here.
foreach (var application in await Store.FindByLogoutRedirectUri(address, cancellationToken))
foreach (var application in await Store.FindByLogoutRedirectUriAsync(address, cancellationToken))
{
// Note: the post_logout_redirect_uri must be compared using case-sensitive "Simple String Comparison".
if (string.Equals(address, await Store.GetLogoutRedirectUriAsync(application, cancellationToken), StringComparison.Ordinal))

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

@ -5,6 +5,7 @@
*/
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
@ -42,9 +43,9 @@ namespace OpenIddict.Core
/// <param name="authorization">The application to create.</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.
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result returns the authorization.
/// </returns>
public virtual Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
public virtual Task<TAuthorization> CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
@ -54,6 +55,38 @@ namespace OpenIddict.Core
return Store.CreateAsync(authorization, cancellationToken);
}
/// <summary>
/// Creates a new authorization.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="scopes">The scopes associated with 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 authorization.
/// </returns>
public virtual Task<TAuthorization> CreateAsync(
[NotNull] string subject, [NotNull] string client,
[NotNull] IEnumerable<string> scopes, CancellationToken cancellationToken)
{
if (scopes == null)
{
throw new ArgumentNullException(nameof(scopes));
}
if (string.IsNullOrEmpty(subject))
{
throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
}
if (string.IsNullOrEmpty(client))
{
throw new ArgumentException("The client cannot be null or empty.", nameof(subject));
}
return Store.CreateAsync(subject, client, scopes, cancellationToken);
}
/// <summary>
/// Retrieves an authorization using its associated subject/client.
/// </summary>
@ -102,6 +135,45 @@ namespace OpenIddict.Core
return Store.GetIdAsync(authorization, cancellationToken);
}
/// <summary>
/// Revokes an authorization.
/// </summary>
/// <param name="authorization">The authorization to revoke.</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.</returns>
public virtual async Task RevokeAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
var status = await Store.GetStatusAsync(authorization, cancellationToken);
if (!string.Equals(status, OpenIddictConstants.Statuses.Revoked, StringComparison.OrdinalIgnoreCase))
{
await Store.SetStatusAsync(authorization, OpenIddictConstants.Statuses.Revoked, cancellationToken);
await UpdateAsync(authorization, cancellationToken);
}
}
/// <summary>
/// Updates an existing authorization.
/// </summary>
/// <param name="authorization">The authorization to update.</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.
/// </returns>
public virtual Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return Store.UpdateAsync(authorization, cancellationToken);
}
/// <summary>
/// Validates the authorization to ensure it's in a consistent state.
/// </summary>

244
src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs

@ -61,21 +61,50 @@ namespace OpenIddict.Core
/// </summary>
/// <param name="type">The token type.</param>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="start">The date on which the token will start to be considered valid.</param>
/// <param name="end">The date on which the token will no longer be considered valid.</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 token.
/// </returns>
public virtual async Task<TToken> CreateAsync([NotNull] string type, [NotNull] string subject, CancellationToken cancellationToken)
public virtual Task<TToken> CreateAsync(
[NotNull] string type, [NotNull] string subject,
[CanBeNull] DateTimeOffset? start,
[CanBeNull] DateTimeOffset? end, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(type))
{
throw new ArgumentException("The token type cannot be null or empty.", nameof(type));
}
if (!string.Equals(type, OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(type, OpenIdConnectConstants.TokenTypeHints.RefreshToken, StringComparison.OrdinalIgnoreCase))
if (string.IsNullOrEmpty(subject))
{
throw new ArgumentException("The specified token type is not supported by the default token manager.");
throw new ArgumentException("The subject cannot be null or empty.");
}
return Store.CreateAsync(type, subject, start, end, cancellationToken);
}
/// <summary>
/// Creates a new reference token, which is associated with a particular subject.
/// </summary>
/// <param name="type">The token type.</param>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="hash">The hash of the crypto-secure random identifier associated with the token.</param>
/// <param name="ciphertext">The ciphertext associated with the token.</param>
/// <param name="start">The date on which the token will start to be considered valid.</param>
/// <param name="end">The date on which the token will no longer be considered valid.</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 token.
/// </returns>
public virtual Task<TToken> CreateAsync(
[NotNull] string type, [NotNull] string subject, [NotNull] string hash, [NotNull] string ciphertext,
[CanBeNull] DateTimeOffset? start, [CanBeNull] DateTimeOffset? end, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(type))
{
throw new ArgumentException("The token type cannot be null or empty.", nameof(type));
}
if (string.IsNullOrEmpty(subject))
@ -83,7 +112,40 @@ namespace OpenIddict.Core
throw new ArgumentException("The subject cannot be null or empty.");
}
return await Store.CreateAsync(type, subject, cancellationToken);
if (string.IsNullOrEmpty(ciphertext))
{
throw new ArgumentException("The ciphertext cannot be null or empty.", nameof(ciphertext));
}
return Store.CreateAsync(type, subject, hash, ciphertext, start, end, cancellationToken);
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified authorization identifier.
/// </summary>
/// <param name="identifier">The authorization identifier associated with the tokens.</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 tokens corresponding to the specified authorization.
/// </returns>
public virtual Task<TToken[]> FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken)
{
return Store.FindByAuthorizationIdAsync(identifier, cancellationToken);
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified hash.
/// </summary>
/// <param name="hash">The hashed crypto-secure random identifier associated with the tokens.</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 tokens corresponding to the specified hash.
/// </returns>
public virtual Task<TToken> FindByHashAsync(string hash, CancellationToken cancellationToken)
{
return Store.FindByHashAsync(hash, cancellationToken);
}
/// <summary>
@ -114,6 +176,63 @@ namespace OpenIddict.Core
return Store.FindBySubjectAsync(subject, cancellationToken);
}
/// <summary>
/// Retrieves the optional authorization identifier associated with a token.
/// </summary>
/// <param name="token">The token.</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 authorization identifier associated with the token.
/// </returns>
public virtual Task<string> GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return Store.GetAuthorizationIdAsync(token, cancellationToken);
}
/// <summary>
/// Retrieves the ciphertext associated with a token.
/// </summary>
/// <param name="token">The token.</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 ciphertext associated with the specified token.
/// </returns>
public virtual Task<string> GetCiphertextAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return Store.GetCiphertextAsync(token, cancellationToken);
}
/// <summary>
/// Retrieves the hashed identifier associated with a token.
/// </summary>
/// <param name="token">The token.</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 hashed identifier associated with the specified token.
/// </returns>
public virtual Task<string> GetHashAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return Store.GetHashAsync(token, cancellationToken);
}
/// <summary>
/// Retrieves the unique identifier associated with a token.
/// </summary>
@ -133,20 +252,131 @@ namespace OpenIddict.Core
return Store.GetIdAsync(token, cancellationToken);
}
/// <summary>
/// Retrieves the status associated with a token.
/// </summary>
/// <param name="token">The token.</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 token.
/// </returns>
public virtual Task<string> GetStatusAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return Store.GetStatusAsync(token, cancellationToken);
}
/// <summary>
/// Determines whether a given token has already been redemeed.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token has already been redemeed, <c>false</c> otherwise.</returns>
public virtual async Task<bool> IsRedeemedAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
var status = await Store.GetStatusAsync(token, cancellationToken);
if (string.IsNullOrEmpty(status))
{
return false;
}
return string.Equals(status, OpenIddictConstants.Statuses.Redeemed, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines whether a given token has been revoked.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token has been revoked, <c>false</c> otherwise.</returns>
public virtual async Task<bool> IsRevokedAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
var status = await Store.GetStatusAsync(token, cancellationToken);
if (string.IsNullOrEmpty(status))
{
return false;
}
return string.Equals(status, OpenIddictConstants.Statuses.Revoked, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines whether a given token is valid.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token is valid, <c>false</c> otherwise.</returns>
public virtual async Task<bool> IsValidAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
var status = await Store.GetStatusAsync(token, cancellationToken);
if (string.IsNullOrEmpty(status))
{
return false;
}
return string.Equals(status, OpenIddictConstants.Statuses.Valid, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Redeems a token.
/// </summary>
/// <param name="token">The token to redeem.</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.</returns>
public virtual async Task RedeemAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
var status = await Store.GetStatusAsync(token, cancellationToken);
if (!string.Equals(status, OpenIddictConstants.Statuses.Redeemed, StringComparison.OrdinalIgnoreCase))
{
await Store.SetStatusAsync(token, OpenIddictConstants.Statuses.Redeemed, cancellationToken);
await UpdateAsync(token, cancellationToken);
}
}
/// <summary>
/// Revokes a token.
/// </summary>
/// <param name="token">The token to revoke.</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.</returns>
public virtual Task RevokeAsync([NotNull] TToken token, CancellationToken cancellationToken)
public virtual async Task RevokeAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return Store.RevokeAsync(token, cancellationToken);
var status = await Store.GetStatusAsync(token, cancellationToken);
if (!string.Equals(status, OpenIddictConstants.Statuses.Revoked, StringComparison.OrdinalIgnoreCase))
{
await Store.SetStatusAsync(token, OpenIddictConstants.Statuses.Revoked, cancellationToken);
await UpdateAsync(token, cancellationToken);
}
}
/// <summary>

7
src/OpenIddict.Core/OpenIddictConstants.cs

@ -39,5 +39,12 @@ namespace OpenIddict.Core
{
public const string Roles = "roles";
}
public static class Statuses
{
public const string Redeemed = "redeemed";
public const string Revoked = "revoked";
public const string Valid = "valid";
}
}
}

4
src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs

@ -68,7 +68,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns>
Task<TApplication[]> FindByLogoutRedirectUri(string address, CancellationToken cancellationToken);
Task<TApplication[]> FindByLogoutRedirectUriAsync(string address, CancellationToken cancellationToken);
/// <summary>
/// Retrieves all the applications associated with the specified redirect_uri.
@ -79,7 +79,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
/// </returns>
Task<TApplication[]> FindByRedirectUri(string address, CancellationToken cancellationToken);
Task<TApplication[]> FindByRedirectUriAsync(string address, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the client identifier associated with an application.

47
src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs

@ -4,6 +4,7 @@
* the license and the contributors participating to this project.
*/
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
@ -26,6 +27,20 @@ namespace OpenIddict.Core
/// </returns>
Task<TAuthorization> CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
/// <summary>
/// Creates a new authorization.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="scopes">The scopes associated with 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 authorization.
/// </returns>
Task<TAuthorization> CreateAsync(
[NotNull] string subject, [NotNull] string client,
[NotNull] IEnumerable<string> scopes, CancellationToken cancellationToken);
/// <summary>
/// Retrieves an authorization using its unique identifier.
/// </summary>
@ -60,6 +75,17 @@ namespace OpenIddict.Core
/// </returns>
Task<string> GetIdAsync([NotNull] TAuthorization authorization, CancellationToken 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>
Task<string> GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the subject associated with an authorization.
/// </summary>
@ -70,5 +96,26 @@ namespace OpenIddict.Core
/// whose result returns the subject associated with the specified authorization.
/// </returns>
Task<string> GetSubjectAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
/// <summary>
/// Sets the status associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="status">The status associated with 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.
/// </returns>
Task SetStatusAsync([NotNull] TAuthorization authorization, [NotNull] string status, CancellationToken cancellationToken);
/// <summary>
/// Updates an existing authorization.
/// </summary>
/// <param name="authorization">The authorization to update.</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.
/// </returns>
Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
}
}

116
src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs

@ -4,6 +4,7 @@
* the license and the contributors participating to this project.
*/
using System;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
@ -31,11 +32,63 @@ namespace OpenIddict.Core
/// </summary>
/// <param name="type">The token type.</param>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="start">The date on which the token will start to be considered valid.</param>
/// <param name="end">The date on which the token will no longer be considered valid.</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 token.
/// </returns>
Task<TToken> CreateAsync([NotNull] string type, [NotNull] string subject, CancellationToken cancellationToken);
Task<TToken> CreateAsync(
[NotNull] string type, [NotNull] string subject,
[CanBeNull] DateTimeOffset? start,
[CanBeNull] DateTimeOffset? end, CancellationToken cancellationToken);
/// <summary>
/// Creates a new reference token, which is associated with a particular subject.
/// </summary>
/// <param name="type">The token type.</param>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="hash">The hash of the crypto-secure random identifier associated with the token.</param>
/// <param name="ciphertext">The ciphertext associated with the token.</param>
/// <param name="start">The date on which the token will start to be considered valid.</param>
/// <param name="end">The date on which the token will no longer be considered valid.</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 token.
/// </returns>
Task<TToken> CreateAsync(
[NotNull] string type, [NotNull] string subject, [NotNull] string hash, [NotNull] string ciphertext,
[CanBeNull] DateTimeOffset? start, [CanBeNull] DateTimeOffset? end, CancellationToken cancellationToken);
/// <summary>
/// Removes a token.
/// </summary>
/// <param name="token">The token to delete.</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.</returns>
Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of tokens corresponding to the specified authorization identifier.
/// </summary>
/// <param name="identifier">The authorization identifier associated with the tokens.</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 tokens corresponding to the specified authorization.
/// </returns>
Task<TToken[]> FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of tokens corresponding to the specified hash.
/// </summary>
/// <param name="hash">The hashed crypto-secure random identifier associated with the tokens.</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 tokens corresponding to the specified hash.
/// </returns>
Task<TToken> FindByHashAsync(string hash, CancellationToken cancellationToken);
/// <summary>
/// Retrieves an token using its unique identifier.
@ -59,6 +112,39 @@ namespace OpenIddict.Core
/// </returns>
Task<TToken[]> FindBySubjectAsync(string subject, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the optional authorization identifier associated with a token.
/// </summary>
/// <param name="token">The token.</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 authorization identifier associated with the token.
/// </returns>
Task<string> GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the ciphertext associated with a token.
/// </summary>
/// <param name="token">The token.</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 ciphertext associated with the specified token.
/// </returns>
Task<string> GetCiphertextAsync([NotNull] TToken token, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the hashed identifier associated with a token.
/// </summary>
/// <param name="token">The token.</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 hashed identifier associated with the specified token.
/// </returns>
Task<string> GetHashAsync([NotNull] TToken token, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the unique identifier associated with a token.
/// </summary>
@ -71,15 +157,15 @@ namespace OpenIddict.Core
Task<string> GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the token type associated with a token.
/// Retrieves the status associated with a token.
/// </summary>
/// <param name="token">The token.</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 token type associated with the specified token.
/// whose result returns the status associated with the specified token.
/// </returns>
Task<string> GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken);
Task<string> GetStatusAsync([NotNull] TToken token, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the subject associated with a token.
@ -93,12 +179,15 @@ namespace OpenIddict.Core
Task<string> GetSubjectAsync([NotNull] TToken token, CancellationToken cancellationToken);
/// <summary>
/// Revokes a token.
/// Retrieves the token type associated with a token.
/// </summary>
/// <param name="token">The token to revoke.</param>
/// <param name="token">The token.</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.</returns>
Task RevokeAsync([NotNull] TToken token, CancellationToken cancellationToken);
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token type associated with the specified token.
/// </returns>
Task<string> GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken);
/// <summary>
/// Sets the authorization associated with a token.
@ -122,6 +211,17 @@ namespace OpenIddict.Core
/// </returns>
Task SetClientAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Sets the status associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="status">The status associated with 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.
/// </returns>
Task SetStatusAsync([NotNull] TToken token, [NotNull] string status, CancellationToken cancellationToken);
/// <summary>
/// Updates an existing token.
/// </summary>

3
src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs

@ -264,6 +264,9 @@ namespace Microsoft.Extensions.DependencyInjection
{
entity.HasKey(token => token.Id);
entity.HasIndex(token => token.Hash)
.IsUnique(unique: true);
entity.ToTable("OpenIddictTokens");
});

4
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs

@ -164,7 +164,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns>
public virtual Task<TApplication[]> FindByLogoutRedirectUri(string address, CancellationToken cancellationToken)
public virtual Task<TApplication[]> FindByLogoutRedirectUriAsync(string address, CancellationToken cancellationToken)
{
return Applications.Where(application => application.LogoutRedirectUri == address).ToArrayAsync(cancellationToken);
}
@ -178,7 +178,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
/// </returns>
public virtual Task<TApplication[]> FindByRedirectUri(string address, CancellationToken cancellationToken)
public virtual Task<TApplication[]> FindByRedirectUriAsync(string address, CancellationToken cancellationToken)
{
return Applications.Where(application => application.RedirectUri == address).ToArrayAsync(cancellationToken);
}

95
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs

@ -5,6 +5,7 @@
*/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
@ -104,6 +105,48 @@ namespace OpenIddict.EntityFrameworkCore
return authorization;
}
/// <summary>
/// Creates a new authorization.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="scopes">The scopes associated with 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 authorization.
/// </returns>
public virtual async Task<TAuthorization> CreateAsync(
[NotNull] string subject, [NotNull] string client,
[NotNull] IEnumerable<string> scopes, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(subject))
{
throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
}
if (string.IsNullOrEmpty(client))
{
throw new ArgumentException("The client cannot be null or empty.", nameof(subject));
}
var key = ConvertIdentifierFromString(client);
var application = await Applications.SingleOrDefaultAsync(entity => entity.Id.Equals(key));
if (application == null)
{
throw new InvalidOperationException("The application associated with the authorization cannot be found.");
}
var authorization = new TAuthorization
{
Application = application,
Scope = string.Join(" ", scopes),
Subject = subject
};
return await CreateAsync(authorization, cancellationToken);
}
/// <summary>
/// Retrieves an authorization using its associated subject/client.
/// </summary>
@ -160,6 +203,20 @@ namespace OpenIddict.EntityFrameworkCore
return Task.FromResult(ConvertIdentifierToString(authorization.Id));
}
/// <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)
{
return Task.FromResult(authorization.Status);
}
/// <summary>
/// Retrieves the subject associated with an authorization.
/// </summary>
@ -179,6 +236,44 @@ namespace OpenIddict.EntityFrameworkCore
return Task.FromResult(authorization.Subject);
}
/// <summary>
/// Sets the status associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="status">The status associated with 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.
/// </returns>
public virtual Task SetStatusAsync([NotNull] TAuthorization authorization, [NotNull] string status, CancellationToken cancellationToken)
{
authorization.Status = status;
return Task.FromResult(0);
}
/// <summary>
/// Updates an existing authorization.
/// </summary>
/// <param name="authorization">The authorization to update.</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.
/// </returns>
public virtual async Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
Context.Attach(authorization);
Context.Update(authorization);
try
{
await Context.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateConcurrencyException) { }
}
/// <summary>
/// Converts the provided identifier to a strongly typed key object.
/// </summary>

220
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs

@ -114,18 +114,129 @@ namespace OpenIddict.EntityFrameworkCore
/// </summary>
/// <param name="type">The token type.</param>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="start">The date on which the token will start to be considered valid.</param>
/// <param name="end">The date on which the token will no longer be considered valid.</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 token.
/// </returns>
public virtual Task<TToken> CreateAsync([NotNull] string type, [NotNull] string subject, CancellationToken cancellationToken)
public virtual Task<TToken> CreateAsync(
[NotNull] string type, [NotNull] string subject,
[CanBeNull] DateTimeOffset? start,
[CanBeNull] DateTimeOffset? end, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(type))
{
throw new ArgumentException("The token type cannot be null or empty.");
}
return CreateAsync(new TToken { Subject = subject, Type = type }, cancellationToken);
if (string.IsNullOrEmpty(subject))
{
throw new ArgumentException("The subject cannot be null or empty.");
}
var token = new TToken
{
End = end,
Start = start,
Subject = subject,
Type = type
};
return CreateAsync(token, cancellationToken);
}
/// <summary>
/// Creates a new reference token, which is associated with a particular subject.
/// </summary>
/// <param name="type">The token type.</param>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="hash">The hash of the crypto-secure random identifier associated with the token.</param>
/// <param name="ciphertext">The ciphertext associated with the token.</param>
/// <param name="start">The date on which the token will start to be considered valid.</param>
/// <param name="end">The date on which the token will no longer be considered valid.</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 token.
/// </returns>
public virtual Task<TToken> CreateAsync(
[NotNull] string type, [NotNull] string subject, [NotNull] string hash, [NotNull] string ciphertext,
[CanBeNull] DateTimeOffset? start, [CanBeNull] DateTimeOffset? end, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(type))
{
throw new ArgumentException("The token type cannot be null or empty.");
}
if (string.IsNullOrEmpty(subject))
{
throw new ArgumentException("The subject cannot be null or empty.");
}
var token = new TToken
{
Ciphertext = ciphertext,
End = end,
Hash = hash,
Start = start,
Subject = subject,
Type = type
};
return CreateAsync(token, cancellationToken);
}
/// <summary>
/// Removes a token.
/// </summary>
/// <param name="token">The token to delete.</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.</returns>
public virtual async Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
Context.Remove(token);
try
{
await Context.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateConcurrencyException) { }
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified authorization identifier.
/// </summary>
/// <param name="identifier">The authorization identifier associated with the tokens.</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 tokens corresponding to the specified authorization.
/// </returns>
public virtual Task<TToken[]> FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken)
{
var key = ConvertIdentifierFromString(identifier);
return Tokens.Where(token => token.Authorization.Id.Equals(key)).ToArrayAsync(cancellationToken);
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified hash.
/// </summary>
/// <param name="hash">The hashed crypto-secure random identifier associated with the tokens.</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 tokens corresponding to the specified hash.
/// </returns>
public virtual Task<TToken> FindByHashAsync(string hash, CancellationToken cancellationToken)
{
return Tokens.SingleOrDefaultAsync(token => token.Hash == hash, cancellationToken);
}
/// <summary>
@ -158,6 +269,67 @@ namespace OpenIddict.EntityFrameworkCore
return Tokens.Where(token => token.Subject == subject).ToArrayAsync();
}
/// <summary>
/// Retrieves the optional authorization identifier associated with a token.
/// </summary>
/// <param name="token">The token.</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 authorization identifier associated with the token.
/// </returns>
public virtual async Task<string> GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
var key = await (from authorization in Authorizations
where authorization.Tokens.Any(entity => entity.Id.Equals(token.Id))
select authorization.Id).SingleOrDefaultAsync();
return ConvertIdentifierToString(key);
}
/// <summary>
/// Retrieves the ciphertext associated with a token.
/// </summary>
/// <param name="token">The token.</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 ciphertext associated with the specified token.
/// </returns>
public virtual Task<string> GetCiphertextAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return Task.FromResult(token.Ciphertext);
}
/// <summary>
/// Retrieves the hashed identifier associated with a token.
/// </summary>
/// <param name="token">The token.</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 hashed identifier associated with the specified token.
/// </returns>
public virtual Task<string> GetHashAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return Task.FromResult(token.Hash);
}
/// <summary>
/// Retrieves the unique identifier associated with a token.
/// </summary>
@ -178,22 +350,22 @@ namespace OpenIddict.EntityFrameworkCore
}
/// <summary>
/// Retrieves the token type associated with a token.
/// Retrieves the status associated with a token.
/// </summary>
/// <param name="token">The token.</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 token type associated with the specified token.
/// whose result returns the status associated with the specified token.
/// </returns>
public virtual Task<string> GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken)
public virtual Task<string> GetStatusAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return Task.FromResult(token.Type);
return Task.FromResult(token.Status);
}
/// <summary>
@ -216,26 +388,22 @@ namespace OpenIddict.EntityFrameworkCore
}
/// <summary>
/// Revokes a token.
/// Retrieves the token type associated with a token.
/// </summary>
/// <param name="token">The token to revoke.</param>
/// <param name="token">The token.</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.</returns>
public virtual async Task RevokeAsync([NotNull] TToken token, CancellationToken cancellationToken)
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token type associated with the specified token.
/// </returns>
public virtual Task<string> GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
Context.Remove(token);
try
{
await Context.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateConcurrencyException) { }
return Task.FromResult(token.Type);
}
/// <summary>
@ -324,6 +492,22 @@ namespace OpenIddict.EntityFrameworkCore
}
}
/// <summary>
/// Sets the status associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="status">The status associated with 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.
/// </returns>
public virtual Task SetStatusAsync([NotNull] TToken token, [NotNull] string status, CancellationToken cancellationToken)
{
token.Status = status;
return Task.FromResult(0);
}
/// <summary>
/// Updates an existing token.
/// </summary>

5
src/OpenIddict.Models/OpenIddictAuthorization.cs

@ -50,6 +50,11 @@ namespace OpenIddict.Models
/// </summary>
public virtual string Scope { get; set; }
/// <summary>
/// Gets or sets the status of the current authorization.
/// </summary>
public virtual string Status { get; set; } = "valid";
/// <summary>
/// Gets or sets the subject associated with the current authorization.
/// </summary>

31
src/OpenIddict.Models/OpenIddictToken.cs

@ -43,12 +43,43 @@ namespace OpenIddict.Models
/// </summary>
public virtual TAuthorization Authorization { get; set; }
/// <summary>
/// Gets or sets the encrypted payload
/// of the current token, if applicable.
/// This property is only used for reference tokens.
/// </summary>
public virtual string Ciphertext { get; set; }
/// <summary>
/// Gets or sets the date on which the token
/// will no longer be considered valid.
/// </summary>
public virtual DateTimeOffset? End { get; set; }
/// <summary>
/// Gets or sets the hashed identifier associated
/// with the current token, if applicable.
/// This property is only used for reference tokens.
/// </summary>
public virtual string Hash { get; set; }
/// <summary>
/// Gets or sets the unique identifier
/// associated with the current token.
/// </summary>
public virtual TKey Id { get; set; }
/// <summary>
/// Gets or sets the date on which the token
/// will start to be considered valid.
/// </summary>
public virtual DateTimeOffset? Start { get; set; }
/// <summary>
/// Gets or sets the status of the current token.
/// </summary>
public virtual string Status { get; set; } = "valid";
/// <summary>
/// Gets or sets the subject associated with the current token.
/// </summary>

83
src/OpenIddict/OpenIddictExtensions.cs

@ -14,6 +14,7 @@ using System.Security.Cryptography.X509Certificates;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
@ -75,6 +76,55 @@ namespace Microsoft.AspNetCore.Builder
}
}
// If OpenIddict was configured to use reference tokens, replace the default access tokens/
// authorization codes/refresh tokens formats using a specific data protector to ensure
// that encrypted tokens stored in the database cannot be treated as valid tokens if the
// reference tokens option is later turned off by the developer.
if (options.UseReferenceTokens)
{
// Note: a default data protection provider is always registered by
// the OpenID Connect server handler when none is explicitly set but
// this initializer is registered to be invoked before ASOS' initializer.
// To ensure the provider property is never null, it's manually set here.
if (options.DataProtectionProvider == null)
{
options.DataProtectionProvider = app.ApplicationServices.GetDataProtectionProvider();
}
if (options.AccessTokenFormat == null)
{
var protector = options.DataProtectionProvider.CreateProtector(
nameof(OpenIdConnectServerHandler),
nameof(options.AccessTokenFormat),
nameof(options.UseReferenceTokens),
options.AuthenticationScheme);
options.AccessTokenFormat = new TicketDataFormat(protector);
}
if (options.AuthorizationCodeFormat == null)
{
var protector = options.DataProtectionProvider.CreateProtector(
nameof(OpenIdConnectServerHandler),
nameof(options.AuthorizationCodeFormat),
nameof(options.UseReferenceTokens),
options.AuthenticationScheme);
options.AuthorizationCodeFormat = new TicketDataFormat(protector);
}
if (options.RefreshTokenFormat == null)
{
var protector = options.DataProtectionProvider.CreateProtector(
nameof(OpenIdConnectServerHandler),
nameof(options.RefreshTokenFormat),
nameof(options.UseReferenceTokens),
options.AuthenticationScheme);
options.RefreshTokenFormat = new TicketDataFormat(protector);
}
}
// Ensure at least one flow has been enabled.
if (options.GrantTypes.Count == 0)
{
@ -106,6 +156,18 @@ namespace Microsoft.AspNetCore.Builder
throw new InvalidOperationException("The revocation endpoint cannot be enabled when token revocation is disabled.");
}
if (options.UseReferenceTokens && options.DisableTokenRevocation)
{
throw new InvalidOperationException(
"Reference tokens cannot be used when disabling token revocation.");
}
if (options.UseReferenceTokens && options.AccessTokenHandler != null)
{
throw new InvalidOperationException(
"Reference tokens cannot be used when configuring JWT as the access token format.");
}
// Ensure at least one asymmetric signing certificate/key was registered if the implicit flow was enabled.
if (!options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey) &&
options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))
@ -883,5 +945,26 @@ namespace Microsoft.AspNetCore.Builder
};
});
}
/// <summary>
/// Configures OpenIddict to use reference tokens, so that authorization codes,
/// access tokens and refresh tokens are stored as ciphertext in the database
/// (only an identifier is returned to the client application). Enabling this option
/// is useful to keep track of all the issued tokens, when storing a very large
/// number of claims in the authorization codes, access tokens and refresh tokens
/// or when immediate revocation of reference access tokens is desired.
/// Note: this option cannot be used when configuring JWT as the access token format.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <returns>The <see cref="OpenIddictBuilder"/>.</returns>
public static OpenIddictBuilder UseReferenceTokens([NotNull] this OpenIddictBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
return builder.Configure(options => options.UseReferenceTokens = true);
}
}
}

18
src/OpenIddict/OpenIddictOptions.cs

@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using AspNet.Security.OpenIdConnect.Server;
using Microsoft.Extensions.Caching.Distributed;
@ -48,11 +49,28 @@ namespace OpenIddict
/// </summary>
public ISet<string> GrantTypes { get; } = new HashSet<string>(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the random number generator used to generate crypto-secure identifiers.
/// </summary>
public RandomNumberGenerator RandomNumberGenerator { get; set; } = RandomNumberGenerator.Create();
/// <summary>
/// Gets or sets a boolean determining whether client identification is required.
/// Enabling this option requires registering a client application and sending a
/// valid client_id when communicating with the token and revocation endpoints.
/// </summary>
public bool RequireClientIdentification { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether reference tokens should be used.
/// When set to <c>true</c>, authorization codes, access tokens and refresh tokens
/// are stored as ciphertext in the database and a crypto-secure random identifier
/// is returned to the client application. Enabling this option is useful
/// to keep track of all the issued tokens, when storing a very large number
/// of claims in the authorization codes, access tokens and refresh tokens
/// or when immediate revocation of reference access tokens is desired.
/// Note: this option cannot be used when configuring JWT as the access token format.
/// </summary>
public bool UseReferenceTokens { get; set; }
}
}

37
src/OpenIddict/OpenIddictProvider.Authentication.cs

@ -16,7 +16,6 @@ using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using Newtonsoft.Json.Linq;
@ -29,8 +28,9 @@ namespace OpenIddict
{
public override async Task ExtractAuthorizationRequest([NotNull] ExtractAuthorizationRequestContext context)
{
var options = (OpenIddictOptions) context.Options;
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
// Reject requests using the unsupported request parameter.
if (!string.IsNullOrEmpty(context.Request.Request))
@ -63,7 +63,7 @@ namespace OpenIddict
if (!string.IsNullOrEmpty(context.Request.RequestId))
{
// Return an error if request caching support was not enabled.
if (!options.Value.EnableRequestCaching)
if (!options.EnableRequestCaching)
{
logger.LogError("The authorization request was rejected because " +
"request caching support was not enabled.");
@ -79,7 +79,7 @@ namespace OpenIddict
// to avoid collisions with the other types of cached requests.
var key = OpenIddictConstants.Environment.AuthorizationRequest + context.Request.RequestId;
var payload = await options.Value.Cache.GetAsync(key);
var payload = await options.Cache.GetAsync(key);
if (payload == null)
{
logger.LogError("The authorization request was rejected because an unknown " +
@ -111,9 +111,10 @@ namespace OpenIddict
public override async Task ValidateAuthorizationRequest([NotNull] ValidateAuthorizationRequestContext context)
{
var options = (OpenIddictOptions) context.Options;
var applications = context.HttpContext.RequestServices.GetRequiredService<OpenIddictApplicationManager<TApplication>>();
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
// Note: the OpenID Connect server middleware supports authorization code, implicit, hybrid,
// none and custom flows but OpenIddict uses a stricter policy rejecting unknown flows.
@ -132,7 +133,7 @@ namespace OpenIddict
// Reject code flow authorization requests if the authorization code flow is not enabled.
if (context.Request.IsAuthorizationCodeFlow() &&
!options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode))
!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode))
{
logger.LogError("The authorization request was rejected because " +
"the authorization code flow was not enabled.");
@ -145,7 +146,7 @@ namespace OpenIddict
}
// Reject implicit flow authorization requests if the implicit flow is not enabled.
if (context.Request.IsImplicitFlow() && !options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))
if (context.Request.IsImplicitFlow() && !options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))
{
logger.LogError("The authorization request was rejected because the implicit flow was not enabled.");
@ -157,8 +158,8 @@ namespace OpenIddict
}
// Reject hybrid flow authorization requests if the authorization code or the implicit flows are not enabled.
if (context.Request.IsHybridFlow() && (!options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) ||
!options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)))
if (context.Request.IsHybridFlow() && (!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) ||
!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)))
{
logger.LogError("The authorization request was rejected because the " +
"authorization code flow or the implicit flow was not enabled.");
@ -172,7 +173,7 @@ namespace OpenIddict
// Reject authorization requests that specify scope=offline_access if the refresh token flow is not enabled.
if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) &&
!options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))
!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
@ -317,12 +318,12 @@ namespace OpenIddict
public override async Task HandleAuthorizationRequest([NotNull] HandleAuthorizationRequestContext context)
{
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
// If no request_id parameter can be found in the current request, assume the OpenID Connect request
// was not serialized yet and store the entire payload in the distributed cache to make it easier
// to flow across requests and internal/external authentication/registration workflows.
if (options.Value.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId))
if (options.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId))
{
// Generate a request identifier. Note: using a crypto-secure
// random number generator is not necessary in this case.
@ -342,7 +343,7 @@ namespace OpenIddict
// to avoid collisions with the other types of cached requests.
var key = OpenIddictConstants.Environment.AuthorizationRequest + context.Request.RequestId;
await options.Value.Cache.SetAsync(key, stream.ToArray(), new DistributedCacheEntryOptions
await options.Cache.SetAsync(key, stream.ToArray(), new DistributedCacheEntryOptions
{
AbsoluteExpiration = context.Options.SystemClock.UtcNow + TimeSpan.FromMinutes(30),
SlidingExpiration = TimeSpan.FromMinutes(10)
@ -368,10 +369,10 @@ namespace OpenIddict
public override async Task ApplyAuthorizationResponse([NotNull] ApplyAuthorizationResponseContext context)
{
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
// Remove the authorization request from the distributed cache.
if (options.Value.EnableRequestCaching && !string.IsNullOrEmpty(context.Request.RequestId))
if (options.EnableRequestCaching && !string.IsNullOrEmpty(context.Request.RequestId))
{
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached requests.
@ -380,11 +381,11 @@ namespace OpenIddict
// Note: the ApplyAuthorizationResponse event is called for both successful
// and errored authorization responses but discrimination is not necessary here,
// as the authorization request must be removed from the distributed cache in both cases.
await options.Value.Cache.RemoveAsync(key);
await options.Cache.RemoveAsync(key);
}
if (!options.Value.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Error) &&
string.IsNullOrEmpty(context.RedirectUri))
if (!options.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Error) &&
string.IsNullOrEmpty(context.RedirectUri))
{
// Determine if the status code pages middleware has been enabled for this request.
// If it was not registered or enabled, let the OpenID Connect server middleware render

6
src/OpenIddict/OpenIddictProvider.Discovery.cs

@ -9,8 +9,6 @@ using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using OpenIddict.Core;
@ -21,7 +19,7 @@ namespace OpenIddict
{
public override Task HandleConfigurationRequest([NotNull] HandleConfigurationRequestContext context)
{
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
// Note: though it's natively supported by the OpenID Connect server middleware,
// OpenIddict disallows the use of the unsecure code_challenge_method=plain method,
@ -32,7 +30,7 @@ namespace OpenIddict
// Note: the OpenID Connect server middleware automatically populates grant_types_supported
// by determining whether the authorization and token endpoints are enabled or not but
// OpenIddict uses a different approach and relies on a configurable "grants list".
context.GrantTypes.IntersectWith(options.Value.GrantTypes);
context.GrantTypes.IntersectWith(options.GrantTypes);
// Note: the "openid" scope is automatically
// added by the OpenID Connect server middleware.

138
src/OpenIddict/OpenIddictProvider.Exchange.cs

@ -12,7 +12,6 @@ using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Core;
namespace OpenIddict
@ -22,12 +21,13 @@ namespace OpenIddict
{
public override async Task ValidateTokenRequest([NotNull] ValidateTokenRequestContext context)
{
var options = (OpenIddictOptions) context.Options;
var applications = context.HttpContext.RequestServices.GetRequiredService<OpenIddictApplicationManager<TApplication>>();
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
// Reject token requests that don't specify a supported grant type.
if (!options.Value.GrantTypes.Contains(context.Request.GrantType))
if (!options.GrantTypes.Contains(context.Request.GrantType))
{
logger.LogError("The token request was rejected because the '{Grant}' " +
"grant is not supported.", context.Request.GrantType);
@ -41,7 +41,7 @@ namespace OpenIddict
// Reject token requests that specify scope=offline_access if the refresh token flow is not enabled.
if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) &&
!options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))
!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
@ -100,7 +100,7 @@ namespace OpenIddict
if (string.IsNullOrEmpty(context.ClientId))
{
// Reject the request if client identification is mandatory.
if (options.Value.RequireClientIdentification)
if (options.RequireClientIdentification)
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
@ -200,60 +200,114 @@ namespace OpenIddict
public override async Task HandleTokenRequest([NotNull] HandleTokenRequestContext context)
{
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
var authorizations = context.HttpContext.RequestServices.GetRequiredService<OpenIddictAuthorizationManager<TAuthorization>>();
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var tokens = context.HttpContext.RequestServices.GetRequiredService<OpenIddictTokenManager<TToken>>();
if (!options.Value.DisableTokenRevocation && (context.Request.IsAuthorizationCodeGrantType() ||
context.Request.IsRefreshTokenGrantType()))
if (options.DisableTokenRevocation || (!context.Request.IsAuthorizationCodeGrantType() &&
!context.Request.IsRefreshTokenGrantType()))
{
Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null.");
// Invoke the rest of the pipeline to allow
// the user code to handle the token request.
context.SkipToNextMiddleware();
return;
}
// 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.");
Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null.");
if (context.Request.IsAuthorizationCodeGrantType())
// 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.");
if (context.Request.IsAuthorizationCodeGrantType())
{
// 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)
{
// 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 authorization code was revoked.");
logger.LogError("The token request was rejected because the authorization code was no longer valid.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified authorization code is no longer valid.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The authorization code is no longer valid.");
return;
}
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 items = await tokens.FindByAuthorizationIdAsync(key, context.HttpContext.RequestAborted);
for (var index = 0; index < items.Length; index++)
{
logger.LogInformation("The compromised token '{Identifier}' was automatically revoked.",
await tokens.GetIdAsync(items[index], context.HttpContext.RequestAborted));
await tokens.RevokeAsync(items[index], context.HttpContext.RequestAborted);
}
}
// Revoke the authorization code to prevent token reuse.
await tokens.RevokeAsync(token, context.HttpContext.RequestAborted);
logger.LogError("The token request was rejected because the authorization code was already redeemed.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified authorization code has already been redemeed.");
return;
}
else if (!await tokens.IsValidAsync(token, context.HttpContext.RequestAborted))
{
logger.LogError("The token request was rejected because the authorization code was no longer valid.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified authorization code is no longer valid.");
return;
}
else if (context.Request.IsRefreshTokenGrantType())
// Mark the authorization code as redeemed to prevent token reuse.
await tokens.RedeemAsync(token, context.HttpContext.RequestAborted);
}
else
{
// Retrieve the token from the database and ensure it is still valid.
var token = await tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
if (token == null || !await tokens.IsValidAsync(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 was revoked.");
logger.LogError("The token request was rejected because the refresh token was no longer valid.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The refresh token is no longer valid.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The specified refresh token is no longer valid.");
return;
}
return;
}
// When sliding expiration is enabled, immediately
// revoke the refresh token to prevent future reuse.
// See https://tools.ietf.org/html/rfc6749#section-6.
if (context.Options.UseSlidingExpiration)
{
await tokens.RevokeAsync(token, context.HttpContext.RequestAborted);
}
// When sliding expiration is enabled, immediately
// redeem the refresh token to prevent future reuse.
// See https://tools.ietf.org/html/rfc6749#section-6.
if (options.UseSlidingExpiration)
{
await tokens.RedeemAsync(token, context.HttpContext.RequestAborted);
}
}

13
src/OpenIddict/OpenIddictProvider.Introspection.cs

@ -13,7 +13,6 @@ using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Core;
namespace OpenIddict
@ -100,7 +99,8 @@ namespace OpenIddict
public override async Task HandleIntrospectionRequest([NotNull] HandleIntrospectionRequestContext context)
{
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var tokens = context.HttpContext.RequestServices.GetRequiredService<OpenIddictTokenManager<TToken>>();
@ -124,13 +124,18 @@ namespace OpenIddict
return;
}
if (options.DisableTokenRevocation)
{
return;
}
// When the received ticket is revocable, ensure it is still valid.
if (!options.Value.DisableTokenRevocation && (context.Ticket.IsAuthorizationCode() || context.Ticket.IsRefreshToken()))
if (options.UseReferenceTokens || context.Ticket.IsAuthorizationCode() || context.Ticket.IsRefreshToken())
{
// Retrieve the token from the database using the unique identifier stored in the authentication ticket:
// if the corresponding entry cannot be found, return Active = false to indicate that is is no longer valid.
var token = await tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
if (token == null)
if (token == null || !await tokens.IsValidAsync(token, context.HttpContext.RequestAborted))
{
logger.LogInformation("The token {Identifier} was declared as inactive because " +
"it was revoked.", identifier);

58
src/OpenIddict/OpenIddictProvider.Revocation.cs

@ -12,7 +12,6 @@ using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Core;
namespace OpenIddict
@ -22,23 +21,36 @@ namespace OpenIddict
{
public override async Task ValidateRevocationRequest([NotNull] ValidateRevocationRequestContext context)
{
var options = (OpenIddictOptions) context.Options;
var applications = context.HttpContext.RequestServices.GetRequiredService<OpenIddictApplicationManager<TApplication>>();
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
Debug.Assert(!options.Value.DisableTokenRevocation, "Token revocation support shouldn't be disabled at this stage.");
Debug.Assert(!options.DisableTokenRevocation, "Token revocation support shouldn't be disabled at this stage.");
// When token_type_hint is specified, reject the request if it doesn't correspond to a revocable token.
if (!string.IsNullOrEmpty(context.Request.TokenTypeHint) &&
!string.Equals(context.Request.TokenTypeHint, OpenIdConnectConstants.TokenTypeHints.AuthorizationCode) &&
!string.Equals(context.Request.TokenTypeHint, OpenIdConnectConstants.TokenTypeHints.RefreshToken))
if (!string.IsNullOrEmpty(context.Request.TokenTypeHint))
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedTokenType,
description: "Only authorization codes and refresh tokens can be revoked. When specifying a token_type_hint " +
"parameter, its value must be equal to 'authorization_code' or 'refresh_token'.");
if (string.Equals(context.Request.TokenTypeHint, OpenIdConnectConstants.TokenTypeHints.IdToken))
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedTokenType,
description: "Identity tokens cannot be revoked. When specifying a token_type_hint parameter, " +
"its value must be equal to 'access_token', 'authorization_code' or 'refresh_token'.");
return;
return;
}
if (!options.UseReferenceTokens &&
string.Equals(context.Request.TokenTypeHint, OpenIdConnectConstants.TokenTypeHints.AccessToken))
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedTokenType,
description: "Access tokens cannot be revoked. When specifying a token_type_hint parameter, " +
"its value must be equal to 'authorization_code' or 'refresh_token'.");
return;
}
}
// Skip client authentication if the client identifier is missing or reject
@ -49,7 +61,7 @@ namespace OpenIddict
if (string.IsNullOrEmpty(context.ClientId))
{
// Reject the request if client identification is mandatory.
if (options.Value.RequireClientIdentification)
if (options.RequireClientIdentification)
{
logger.LogError("The revocation request was rejected becaused the " +
"mandatory client_id parameter was missing or empty.");
@ -128,6 +140,8 @@ namespace OpenIddict
public override async Task HandleRevocationRequest([NotNull] HandleRevocationRequestContext context)
{
var options = (OpenIddictOptions) context.Options;
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var tokens = context.HttpContext.RequestServices.GetRequiredService<OpenIddictTokenManager<TToken>>();
@ -135,13 +149,25 @@ namespace OpenIddict
// If the received token is not an authorization code or a refresh token,
// return an error to indicate that the token cannot be revoked.
if (!context.Ticket.IsAuthorizationCode() && !context.Ticket.IsRefreshToken())
if (context.Ticket.IsIdentityToken())
{
logger.LogError("The revocation request was rejected because identity tokens are not revocable.");
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedTokenType,
description: "Identity tokens cannot be revoked.");
return;
}
// If the received token is an access token, return an error if reference tokens are not enabled.
if (!options.UseReferenceTokens && context.Ticket.IsAccessToken())
{
logger.LogError("The revocation request was rejected because the token was not revocable.");
logger.LogError("The revocation request was rejected because the access token was not revocable.");
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedTokenType,
description: "Only authorization codes and refresh tokens can be revoked.");
description: "The specified access token cannot be revoked.");
return;
}
@ -153,7 +179,7 @@ namespace OpenIddict
// Retrieve the token from the database. If the token cannot be found,
// assume it is invalid and consider the revocation as successful.
var token = await tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted);
if (token == null)
if (token == null || await tokens.IsRevokedAsync(token, context.HttpContext.RequestAborted))
{
logger.LogInformation("The token '{Identifier}' was already revoked.", identifier);

410
src/OpenIddict/OpenIddictProvider.Serialization.cs

@ -6,13 +6,17 @@
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.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Core;
namespace OpenIddict
@ -20,112 +24,376 @@ 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 DeserializeAccessToken([NotNull] DeserializeAccessTokenContext context)
{
var options = (OpenIddictOptions) context.Options;
if (!options.UseReferenceTokens)
{
return;
}
var ticket = await ReceiveTokenAsync(
context.AccessToken, options, context.HttpContext,
context.Request, context.DataFormat);
// If a valid ticket was returned by ReceiveTokenAsync(),
// force the OpenID Connect server middleware to use it.
if (ticket != null)
{
context.Ticket = ticket;
context.HandleResponse();
}
// Otherwise, let the OpenID Connect server middleware
// deserialize the token using its default internal logic.
}
public override async Task DeserializeAuthorizationCode([NotNull] DeserializeAuthorizationCodeContext context)
{
var options = (OpenIddictOptions) context.Options;
if (!options.UseReferenceTokens)
{
return;
}
var ticket = await ReceiveTokenAsync(
context.AuthorizationCode, options, context.HttpContext,
context.Request, context.DataFormat);
// If a valid ticket was returned by ReceiveTokenAsync(),
// force the OpenID Connect server middleware to use it.
if (ticket != null)
{
context.Ticket = ticket;
context.HandleResponse();
}
// Otherwise, let the OpenID Connect server middleware
// deserialize the token using its default internal logic.
}
public override async Task DeserializeRefreshToken([NotNull] DeserializeRefreshTokenContext context)
{
var options = (OpenIddictOptions) context.Options;
if (!options.UseReferenceTokens)
{
return;
}
var ticket = await ReceiveTokenAsync(
context.RefreshToken, options, context.HttpContext,
context.Request, context.DataFormat);
// If a valid ticket was returned by ReceiveTokenAsync(),
// force the OpenID Connect server middleware to use it.
if (ticket != null)
{
context.Ticket = ticket;
context.HandleResponse();
}
// Otherwise, let the OpenID Connect server middleware
// deserialize the token using its default internal logic.
}
public override async Task SerializeAccessToken([NotNull] SerializeAccessTokenContext context)
{
var token = await CreateTokenAsync(
OpenIdConnectConstants.TokenUsages.AccessToken,
context.Ticket, (OpenIddictOptions) context.Options,
context.HttpContext, context.Request, context.DataFormat);
// If a reference token was returned by CreateTokenAsync(),
// force the OpenID Connect server middleware to use it.
if (!string.IsNullOrEmpty(token))
{
context.AccessToken = token;
context.HandleResponse();
}
// Otherwise, let the OpenID Connect server middleware
// serialize the token using its default internal logic.
}
public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context)
{
var applications = context.HttpContext.RequestServices.GetRequiredService<OpenIddictApplicationManager<TApplication>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var tokens = context.HttpContext.RequestServices.GetRequiredService<OpenIddictTokenManager<TToken>>();
var token = await CreateTokenAsync(
OpenIdConnectConstants.TokenUsages.AuthorizationCode,
context.Ticket, (OpenIddictOptions) context.Options,
context.HttpContext, context.Request, context.DataFormat);
// If a reference token was returned by CreateTokenAsync(),
// force the OpenID Connect server middleware to use it.
if (!string.IsNullOrEmpty(token))
{
context.AuthorizationCode = token;
context.HandleResponse();
}
Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId), "The client identifier shouldn't be null or empty.");
// Otherwise, let the OpenID Connect server middleware
// serialize the token using its default internal logic.
}
public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context)
{
var token = await CreateTokenAsync(
OpenIdConnectConstants.TokenUsages.RefreshToken,
context.Ticket, (OpenIddictOptions) context.Options,
context.HttpContext, context.Request, context.DataFormat);
if (!options.Value.DisableTokenRevocation)
// If a reference token was returned by CreateTokenAsync(),
// force the OpenID Connect server middleware to use it.
if (!string.IsNullOrEmpty(token))
{
// Resolve the subject from the authentication ticket. If it cannot be found, throw an exception.
var subject = context.Ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject);
if (string.IsNullOrEmpty(subject))
{
throw new InvalidOperationException("The subject associated with the authentication ticket cannot be retrieved.");
}
context.RefreshToken = token;
context.HandleResponse();
}
// 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)
{
var applications = context.RequestServices.GetRequiredService<OpenIddictApplicationManager<TApplication>>();
var authorizations = context.RequestServices.GetRequiredService<OpenIddictAuthorizationManager<TAuthorization>>();
var logger = context.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var tokens = context.RequestServices.GetRequiredService<OpenIddictTokenManager<TToken>>();
Debug.Assert(!(options.DisableTokenRevocation && options.UseReferenceTokens),
"Token revocation cannot be disabled when using reference tokens.");
Debug.Assert(!string.Equals(type, OpenIdConnectConstants.TokenUsages.IdToken, StringComparison.OrdinalIgnoreCase),
"Identity tokens shouldn't be stored in the database.");
if (options.DisableTokenRevocation)
{
return null;
}
// Resolve the subject from the authentication ticket. If it cannot be found, throw an exception.
var subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject);
if (string.IsNullOrEmpty(subject))
{
throw new InvalidOperationException("The subject associated with the authentication ticket cannot be retrieved.");
}
TToken token;
string result = null;
// If reference tokens are enabled, create a new entry for
// authorization codes, refresh tokens and access tokens.
if (options.UseReferenceTokens)
{
// When the token is a reference token, remove the token identifier from the
// authentication ticket as it is restored when receiving and decrypting it.
ticket.RemoveProperty(OpenIdConnectConstants.Properties.TokenId);
// 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.
var ciphertext = format.Protect(ticket);
// If a null value was returned by CreateAsync, return immediately.
var token = await tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, subject, context.HttpContext.RequestAborted);
if (token == null)
// 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.
string hash;
using (var algorithm = SHA256.Create())
{
return;
hash = Convert.ToBase64String(algorithm.ComputeHash(bytes));
}
// Throw an exception if the token identifier can't be resolved.
var identifier = await tokens.GetIdAsync(token, context.HttpContext.RequestAborted);
if (string.IsNullOrEmpty(identifier))
token = await tokens.CreateAsync(type, subject, hash, ciphertext,
ticket.Properties.IssuedUtc,
ticket.Properties.ExpiresUtc, context.RequestAborted);
}
// Otherwise, only create a token metadata entry for authorization codes and refresh tokens.
else if (string.Equals(type, OpenIdConnectConstants.TokenUsages.AuthorizationCode, StringComparison.OrdinalIgnoreCase) ||
string.Equals(type, OpenIdConnectConstants.TokenUsages.RefreshToken, StringComparison.OrdinalIgnoreCase))
{
token = await tokens.CreateAsync(type, subject,
ticket.Properties.IssuedUtc,
ticket.Properties.ExpiresUtc, context.RequestAborted);
}
else
{
return null;
}
// If a null value was returned by CreateAsync(), return immediately.
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.");
}
// Attach the key returned by the underlying store
// to the refresh token to override the default GUID
// generated by the OpenID Connect server middleware.
ticket.SetTokenId(identifier);
// 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 unique key associated with an authorization code cannot be null or empty.");
throw new InvalidOperationException("The client application cannot be retrieved from the database.");
}
// Attach the key returned by the underlying store
// to the authorization code to override the default GUID
// generated by the OpenID Connect server middleware.
context.Ticket.SetProperty(OpenIdConnectConstants.Properties.TokenId, identifier);
var key = await applications.GetIdAsync(application, context.RequestAborted);
await tokens.SetClientAsync(token, key, context.RequestAborted);
}
// If an authorization identifier was specified, bind it to the token.
if (ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId))
{
await tokens.SetAuthorizationAsync(token,
ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId), context.RequestAborted);
}
var application = await applications.FindByClientIdAsync(context.Request.ClientId, context.HttpContext.RequestAborted);
// Otherwise, create an ad-hoc authorization if the token is an authorization code.
else if (string.Equals(type, OpenIdConnectConstants.TokenUsages.AuthorizationCode, StringComparison.OrdinalIgnoreCase))
{
Debug.Assert(!string.IsNullOrEmpty(request.ClientId), "The client identifier shouldn't be null.");
var application = await applications.FindByClientIdAsync(request.ClientId, context.RequestAborted);
if (application == null)
{
throw new InvalidOperationException("The client application cannot be retrieved from the database.");
}
await tokens.SetClientAsync(token, await applications.GetIdAsync(application, context.HttpContext.RequestAborted), context.HttpContext.RequestAborted);
var authorization = await authorizations.CreateAsync(subject,
await applications.GetIdAsync(application, context.RequestAborted), request.GetScopes(), context.RequestAborted);
// If an authorization identifier was specified, bind it to the token.
var authorization = context.Ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId);
if (!string.IsNullOrEmpty(authorization))
if (authorization != null)
{
await tokens.SetAuthorizationAsync(token, authorization, context.HttpContext.RequestAborted);
var key = await authorizations.GetIdAsync(authorization, context.RequestAborted);
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, key);
await tokens.SetAuthorizationAsync(token, key, 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;
}
public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context)
private async Task<AuthenticationTicket> ReceiveTokenAsync(
[NotNull] string value, [NotNull] OpenIddictOptions options,
[NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request,
[NotNull] ISecureDataFormat<AuthenticationTicket> format)
{
var applications = context.HttpContext.RequestServices.GetRequiredService<OpenIddictApplicationManager<TApplication>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var tokens = context.HttpContext.RequestServices.GetRequiredService<OpenIddictTokenManager<TToken>>();
var logger = context.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var tokens = context.RequestServices.GetRequiredService<OpenIddictTokenManager<TToken>>();
if (!options.Value.DisableTokenRevocation)
if (!options.UseReferenceTokens)
{
// Resolve the subject from the authentication ticket. If it cannot be found, throw an exception.
var subject = context.Ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject);
if (string.IsNullOrEmpty(subject))
{
throw new InvalidOperationException("The subject associated with the authentication ticket cannot be retrieved.");
}
return null;
}
// If a null value was returned by CreateAsync, return immediately.
var token = await tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, subject, context.HttpContext.RequestAborted);
if (token == 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())
{
return;
hash = Convert.ToBase64String(algorithm.ComputeHash(Base64UrlEncoder.DecodeBytes(value)));
}
}
// Throw an exception if the token identifier can't be resolved.
var identifier = await tokens.GetIdAsync(token, context.HttpContext.RequestAborted);
if (string.IsNullOrEmpty(identifier))
{
throw new InvalidOperationException("The unique key associated with a refresh token cannot be null or empty.");
}
// Swallow format-related exceptions to ensure badly formed
// or tampered tokens don't cause an exception at this stage.
catch
{
return null;
}
// Attach the key returned by the underlying store
// to the refresh token to override the default GUID
// generated by the OpenID Connect server middleware.
context.Ticket.SetProperty(OpenIdConnectConstants.Properties.TokenId, identifier);
// 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);
// If the client application is known, associate it with the token.
if (!string.IsNullOrEmpty(context.Request.ClientId))
{
var application = await applications.FindByClientIdAsync(context.Request.ClientId, context.HttpContext.RequestAborted);
if (application == null)
{
throw new InvalidOperationException("The client application cannot be retrieved from the database.");
}
return null;
}
await tokens.SetClientAsync(token, await applications.GetIdAsync(application, context.HttpContext.RequestAborted), context.HttpContext.RequestAborted);
}
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.");
// If an authorization identifier was specified, bind it to the token.
var authorization = context.Ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId);
if (!string.IsNullOrEmpty(authorization))
{
await tokens.SetAuthorizationAsync(token, authorization, context.HttpContext.RequestAborted);
}
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);
// 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;
}
}
}

24
src/OpenIddict/OpenIddictProvider.Session.cs

@ -15,7 +15,6 @@ using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using Newtonsoft.Json.Linq;
@ -28,15 +27,16 @@ namespace OpenIddict
{
public override async Task ExtractLogoutRequest([NotNull] ExtractLogoutRequestContext context)
{
var options = (OpenIddictOptions) context.Options;
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
// If a request_id parameter can be found in the logout request,
// restore the complete logout request from the distributed cache.
if (!string.IsNullOrEmpty(context.Request.RequestId))
{
// Return an error if request caching support was not enabled.
if (!options.Value.EnableRequestCaching)
if (!options.EnableRequestCaching)
{
logger.LogError("The logout request was rejected because " +
"request caching support was not enabled.");
@ -52,7 +52,7 @@ namespace OpenIddict
// to avoid collisions with the other types of cached requests.
var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId;
var payload = await options.Value.Cache.GetAsync(key);
var payload = await options.Cache.GetAsync(key);
if (payload == null)
{
logger.LogError("The logout request was rejected because an unknown " +
@ -106,12 +106,12 @@ namespace OpenIddict
public override async Task HandleLogoutRequest([NotNull] HandleLogoutRequestContext context)
{
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
// If no request_id parameter can be found in the current request, assume the OpenID Connect
// request was not serialized yet and store the entire payload in the distributed cache
// to make it easier to flow across requests and internal/external logout workflows.
if (options.Value.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId))
if (options.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId))
{
// Generate a request identifier. Note: using a crypto-secure
// random number generator is not necessary in this case.
@ -131,7 +131,7 @@ namespace OpenIddict
// to avoid collisions with the other types of cached requests.
var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId;
await options.Value.Cache.SetAsync(key, stream.ToArray(), new DistributedCacheEntryOptions
await options.Cache.SetAsync(key, stream.ToArray(), new DistributedCacheEntryOptions
{
AbsoluteExpiration = context.Options.SystemClock.UtcNow + TimeSpan.FromMinutes(30),
SlidingExpiration = TimeSpan.FromMinutes(10)
@ -155,10 +155,10 @@ namespace OpenIddict
public override async Task ApplyLogoutResponse([NotNull] ApplyLogoutResponseContext context)
{
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<OpenIddictOptions>>();
var options = (OpenIddictOptions) context.Options;
// Remove the logout request from the distributed cache.
if (options.Value.EnableRequestCaching && !string.IsNullOrEmpty(context.Request.RequestId))
if (options.EnableRequestCaching && !string.IsNullOrEmpty(context.Request.RequestId))
{
// Note: the cache key is always prefixed with a specific marker
// to avoid collisions with the other types of cached requests.
@ -167,11 +167,11 @@ namespace OpenIddict
// Note: the ApplyLogoutResponse event is called for both successful
// and errored logout responses but discrimination is not necessary here,
// as the logout request must be removed from the distributed cache in both cases.
await options.Value.Cache.RemoveAsync(key);
await options.Cache.RemoveAsync(key);
}
if (!options.Value.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Error) &&
string.IsNullOrEmpty(context.PostLogoutRedirectUri))
if (!options.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Error) &&
string.IsNullOrEmpty(context.PostLogoutRedirectUri))
{
// Determine if the status code pages middleware has been enabled for this request.
// If it was not registered or enabled, let the OpenID Connect server middleware render

63
test/OpenIddict.Tests/OpenIddictExtensionsTests.cs

@ -128,6 +128,48 @@ namespace OpenIddict.Tests
Assert.Equal("The revocation endpoint cannot be enabled when token revocation is disabled.", exception.Message);
}
[Fact]
public void UseOpenIddict_ThrowsAnExceptionWhenUsingReferenceTokensWithTokenRevocationDisabled()
{
// Arrange
var services = new ServiceCollection();
services.AddDataProtection();
services.AddOpenIddict()
.EnableAuthorizationEndpoint("/connect/authorize")
.AllowImplicitFlow()
.DisableTokenRevocation()
.UseReferenceTokens();
var builder = new ApplicationBuilder(services.BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.UseOpenIddict());
Assert.Equal("Reference tokens cannot be used when disabling token revocation.", exception.Message);
}
[Fact]
public void UseOpenIddict_ThrowsAnExceptionWhenUsingReferenceTokensIfAnAccessTokenHandlerIsSet()
{
// Arrange
var services = new ServiceCollection();
services.AddDataProtection();
services.AddOpenIddict()
.EnableAuthorizationEndpoint("/connect/authorize")
.AllowImplicitFlow()
.UseReferenceTokens()
.UseJsonWebTokens();
var builder = new ApplicationBuilder(services.BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => builder.UseOpenIddict());
Assert.Equal("Reference tokens cannot be used when configuring JWT as the access token format.", exception.Message);
}
[Fact]
public void UseOpenIddict_ThrowsAnExceptionWhenNoSigningKeyIsRegisteredIfTheImplicitFlowIsEnabled()
{
@ -764,7 +806,26 @@ namespace OpenIddict.Tests
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
// Assert
Assert.IsType(typeof(JwtSecurityTokenHandler), options.Value.AccessTokenHandler);
Assert.IsType<JwtSecurityTokenHandler>(options.Value.AccessTokenHandler);
}
[Fact]
public void UseReferenceTokens_ReferenceTokensAreEnabled()
{
// Arrange
var services = new ServiceCollection();
services.AddOptions();
var builder = new OpenIddictBuilder(services);
// Act
builder.UseReferenceTokens();
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OpenIddictOptions>>();
// Assert
Assert.True(options.Value.UseReferenceTokens);
}
}
}

8
test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs

@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -558,7 +559,10 @@ namespace OpenIddict.Tests
{
var token = new OpenIddictToken();
instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", It.IsAny<CancellationToken>()))
instance.Setup(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique",
It.IsAny<DateTimeOffset?>(), It.IsAny<DateTimeOffset?>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))

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

@ -1,4 +1,5 @@
using System.Security.Claims;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Client;
@ -458,7 +459,7 @@ namespace OpenIddict.Tests
}
[Fact]
public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsExpired()
public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsUnknown()
{
// Arrange
var ticket = new AuthenticationTicket(
@ -512,13 +513,235 @@ namespace OpenIddict.Tests
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The authorization code is no longer valid.", response.ErrorDescription);
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());
}
[Fact]
public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsExpired()
public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsAlreadyRedeemed()
{
// Arrange
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(),
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.IsRedeemedAsync(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
Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified authorization code has already been redemeed.", 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());
}
[Fact]
public async Task HandleTokenRequest_RevokesTokensWhenAuthorizationCodeIsAlreadyRedeemed()
{
// Arrange
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(),
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");
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA"))
.Returns(ticket);
var tokens = new[]
{
new OpenIddictToken(),
new OpenIddictToken(),
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.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 =>
{
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(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);
});
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
Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified authorization code has already been redemeed.", 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());
}
[Fact]
public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsInvalid()
{
// Arrange
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(),
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.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
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
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.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_RequestIsRejectedWhenRefreshTokenIsUnknown()
{
// Arrange
var ticket = new AuthenticationTicket(
@ -569,13 +792,76 @@ namespace OpenIddict.Tests
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The refresh token is no longer valid.", response.ErrorDescription);
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_AuthorizationCodeIsAutomaticallyRevoked()
public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsInvalid()
{
// 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 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.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
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());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleTokenRequest_AuthorizationCodeIsAutomaticallyRedeemed()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
@ -601,6 +887,9 @@ namespace OpenIddict.Tests
{
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 =>
@ -634,11 +923,11 @@ namespace OpenIddict.Tests
// Assert
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleTokenRequest_RefreshTokenIsAutomaticallyRevokedWhenSlidingExpirationIsEnabled()
public async Task HandleTokenRequest_RefreshTokenIsAutomaticallyRedeemedWhenSlidingExpirationIsEnabled()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
@ -663,6 +952,12 @@ namespace OpenIddict.Tests
{
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 =>
@ -694,7 +989,7 @@ namespace OpenIddict.Tests
// Assert
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Theory]
@ -739,6 +1034,9 @@ namespace OpenIddict.Tests
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
var server = CreateAuthorizationServer(builder =>

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

@ -342,7 +342,145 @@ namespace OpenIddict.Tests
}
[Fact]
public async Task HandleIntrospectionRequest_RequestIsRejectedWhenAuthorizationCodeIsRevoked()
public async Task HandleIntrospectionRequest_RequestIsRejectedWhenAccessTokenIsUnknown()
{
// 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("3E228451-1555-46F7-A471-951EFBA23A56");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken);
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"))
.Returns(ticket);
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", 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.Confidential);
instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
builder.Services.AddSingleton(manager);
builder.Configure(options => options.AccessTokenFormat = format.Object);
builder.UseReferenceTokens();
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest
{
ClientId = "Fabrikam",
ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
Token = "2YotnFZFEjr1zCsicMWpAA"
});
// Assert
Assert.Single(response.GetParameters());
Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleIntrospectionRequest_RequestIsRejectedWhenAccessTokenIsInvalid()
{
// 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("3E228451-1555-46F7-A471-951EFBA23A56");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken);
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"))
.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(false);
});
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.Confidential);
instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
builder.Services.AddSingleton(manager);
builder.Configure(options => options.AccessTokenFormat = format.Object);
builder.UseReferenceTokens();
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest
{
ClientId = "Fabrikam",
ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
Token = "2YotnFZFEjr1zCsicMWpAA"
});
// Assert
Assert.Single(response.GetParameters());
Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleIntrospectionRequest_RequestIsRejectedWhenAuthorizationCodeIsUnknown()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
@ -406,7 +544,77 @@ namespace OpenIddict.Tests
}
[Fact]
public async Task HandleIntrospectionRequest_RequestIsRejectedWhenRefreshTokenIsRevoked()
public async Task HandleIntrospectionRequest_RequestIsRejectedWhenAuthorizationCodeIsInvalid()
{
// 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("3E228451-1555-46F7-A471-951EFBA23A56");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode);
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"))
.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(false);
});
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.Confidential);
instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
builder.Services.AddSingleton(manager);
builder.Configure(options => options.AuthorizationCodeFormat = format.Object);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest
{
ClientId = "Fabrikam",
ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
Token = "2YotnFZFEjr1zCsicMWpAA"
});
// Assert
Assert.Single(response.GetParameters());
Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleIntrospectionRequest_RequestIsRejectedWhenRefreshTokenIsUnknown()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
@ -468,5 +676,75 @@ namespace OpenIddict.Tests
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleIntrospectionRequest_RequestIsRejectedWhenRefreshTokenIsInvalid()
{
// 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("3E228451-1555-46F7-A471-951EFBA23A56");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"))
.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(false);
});
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.Confidential);
instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
builder.Services.AddSingleton(manager);
builder.Configure(options => options.RefreshTokenFormat = format.Object);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest
{
ClientId = "Fabrikam",
ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
Token = "2YotnFZFEjr1zCsicMWpAA"
});
// Assert
Assert.Single(response.GetParameters());
Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
}
}

95
test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs

@ -1,6 +1,5 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
@ -22,10 +21,30 @@ namespace OpenIddict.Tests
{
public partial class OpenIddictProviderTests
{
[Theory]
[InlineData(OpenIdConnectConstants.TokenTypeHints.AccessToken)]
[InlineData(OpenIdConnectConstants.TokenTypeHints.IdToken)]
public async Task ValidateRevocationRequest_UnknownTokenTokenHintIsRejected(string hint)
[Fact]
public async Task ValidateRevocationRequest_IdTokenTokenTokenHintIsRejected()
{
// Arrange
var server = CreateAuthorizationServer();
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest
{
Token = "SlAV32hkKG",
TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.IdToken
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.UnsupportedTokenType, response.Error);
Assert.Equal(
"Identity tokens cannot be revoked. When specifying a token_type_hint parameter, " +
"its value must be equal to 'access_token', 'authorization_code' or 'refresh_token'.", response.ErrorDescription);
}
[Fact]
public async Task ValidateRevocationRequest_AccessTokenTokenTokenHintIsRejectedWhenReferenceTokensAreDisabled()
{
// Arrange
var server = CreateAuthorizationServer();
@ -36,13 +55,14 @@ namespace OpenIddict.Tests
var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest
{
Token = "SlAV32hkKG",
TokenTypeHint = hint
TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.UnsupportedTokenType, response.Error);
Assert.Equal("Only authorization codes and refresh tokens can be revoked. When specifying a token_type_hint " +
"parameter, its value must be equal to 'authorization_code' or 'refresh_token'.", response.ErrorDescription);
Assert.Equal(
"Access tokens cannot be revoked. When specifying a token_type_hint parameter, " +
"its value must be equal to 'authorization_code' or 'refresh_token'.", response.ErrorDescription);
}
[Fact]
@ -219,7 +239,7 @@ namespace OpenIddict.Tests
}
[Fact]
public async Task HandleRevocationRequest_RequestIsRejectedWhenTokenIsAnAccessToken()
public async Task HandleRevocationRequest_RequestIsRejectedWhenTokenIsAnAccessTokenIfReferenceTokensAreDisabled()
{
// Arrange
var ticket = new AuthenticationTicket(
@ -250,13 +270,13 @@ namespace OpenIddict.Tests
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.UnsupportedTokenType, response.Error);
Assert.Equal("Only authorization codes and refresh tokens can be revoked.", response.ErrorDescription);
Assert.Equal("The specified access token cannot be revoked.", response.ErrorDescription);
format.Verify(mock => mock.Unprotect("SlAV32hkKG"), Times.Once());
}
[Fact]
public async Task HandleRevocationRequest_RequestIsNotRejectedWhenTokenIsAnIdentityToken()
public async Task HandleRevocationRequest_RequestIsRejectedWhenTokenIsAnIdentityToken()
{
// Arrange
var token = Mock.Of<SecurityToken>(mock =>
@ -290,7 +310,7 @@ namespace OpenIddict.Tests
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.UnsupportedTokenType, response.Error);
Assert.Equal("Only authorization codes and refresh tokens can be revoked.", response.ErrorDescription);
Assert.Equal("Identity tokens cannot be revoked.", response.ErrorDescription);
handler.As<ISecurityTokenValidator>()
.Verify(mock => mock.CanReadToken("SlAV32hkKG"), Times.Once());
@ -300,7 +320,7 @@ namespace OpenIddict.Tests
}
[Fact]
public async Task HandleRevocationRequest_TokenIsNotRevokedWhenItIsAlreadyInvalid()
public async Task HandleRevocationRequest_TokenIsNotRevokedWhenItIsUnknown()
{
// Arrange
var ticket = new AuthenticationTicket(
@ -343,6 +363,55 @@ namespace OpenIddict.Tests
Mock.Get(manager).Verify(mock => mock.RevokeAsync(It.IsAny<OpenIddictToken>(), It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task HandleRevocationRequest_TokenIsNotRevokedWhenItIsAlreadyRevoked()
{
// Arrange
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56");
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("SlAV32hkKG"))
.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.IsRevokedAsync(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(RevocationEndpoint, new OpenIdConnectRequest
{
Token = "SlAV32hkKG"
});
// Assert
Assert.Empty(response.GetParameters());
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(It.IsAny<OpenIddictToken>(), It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task HandleRevocationRequest_TokenIsSuccessfullyRevoked()
{

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

@ -1,7 +1,11 @@
using System.Threading;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Client;
using AspNet.Security.OpenIdConnect.Primitives;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
@ -14,6 +18,209 @@ namespace OpenIddict.Tests
{
public partial class OpenIddictProviderTests
{
[Fact]
public async Task SerializeAccessToken_AccessTokenIsNotPersistedWhenReferenceTokensAreDisabled()
{
// Arrange
var manager = CreateTokenManager();
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.Configure(options => options.RevocationEndpointPath = PathString.Empty);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w",
Scope = OpenIdConnectConstants.Scopes.OfflineAccess
});
// Assert
Assert.NotNull(response.AccessToken);
Mock.Get(manager).Verify(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.AccessToken, "Bob le Magnifique",
It.IsAny<DateTimeOffset?>(), It.IsAny<DateTimeOffset?>(),
It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task SerializeAccessToken_ReferenceAccessTokenIsCorrectlyPersisted()
{
// Arrange
var token = new OpenIddictToken
{
End = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero),
Start = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero),
};
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.AccessToken, "Bob le Magnifique",
It.IsNotNull<string>(), It.IsNotNull<string>(),
token.Start, token.End, It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.UseReferenceTokens();
builder.Configure(options =>
{
options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == token.Start.Value);
options.AccessTokenLifetime = token.End.Value - token.Start.Value;
});
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w",
Scope = OpenIdConnectConstants.Scopes.OfflineAccess
});
// Assert
Assert.NotNull(response.AccessToken);
Mock.Get(manager).Verify(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.AccessToken, "Bob le Magnifique",
It.IsNotNull<string>(), It.IsNotNull<string>(),
token.Start, token.End, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task SerializeAccessToken_ClientApplicationIsAutomaticallyAttached()
{
// Arrange
var token = new OpenIddictToken();
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.AccessToken, "Bob le Magnifique",
It.IsNotNull<string>(), It.IsNotNull<string>(),
It.IsAny<DateTimeOffset?>(), It.IsAny<DateTimeOffset?>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
instance.Setup(mock => mock.SetClientAsync(token, "3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(0));
});
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);
instance.Setup(mock => mock.GetIdAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
}));
builder.Services.AddSingleton(manager);
builder.UseReferenceTokens();
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
ClientId = "Fabrikam",
GrantType = OpenIdConnectConstants.GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w",
Scope = OpenIdConnectConstants.Scopes.OfflineAccess
});
// Assert
Assert.NotNull(response.AccessToken);
Mock.Get(manager).Verify(mock => mock.SetClientAsync(token, "3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task SerializeAccessToken_AuthorizationIsAutomaticallyAttached()
{
// Arrange
var token = new OpenIddictToken();
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.AccessToken, "Bob le Magnifique",
It.IsNotNull<string>(), It.IsNotNull<string>(),
It.IsAny<DateTimeOffset?>(), It.IsAny<DateTimeOffset?>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
instance.Setup(mock => mock.SetAuthorizationAsync(token, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(0));
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateAuthorizationManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny<CancellationToken>()))
.ReturnsAsync(new OpenIddictAuthorization());
}));
builder.Services.AddSingleton(manager);
builder.UseReferenceTokens();
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w",
Scope = OpenIdConnectConstants.Scopes.OfflineAccess,
["attach-authorization"] = true
});
// Assert
Assert.NotNull(response.AccessToken);
Mock.Get(manager).Verify(mock => mock.SetAuthorizationAsync(token, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task SerializeAuthorizationCode_AuthorizationCodeIsNotPersistedWhenRevocationIsDisabled()
{
@ -59,18 +266,27 @@ namespace OpenIddict.Tests
// Assert
Assert.NotNull(response.Code);
Mock.Get(manager).Verify(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique",
It.IsAny<DateTimeOffset?>(), It.IsAny<DateTimeOffset?>(),
It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task SerializeAuthorizationCode_AuthorizationCodeIsCorrectlyPersisted()
{
// Arrange
var token = new OpenIddictToken();
var token = new OpenIddictToken
{
End = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero),
Start = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero),
};
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", It.IsAny<CancellationToken>()))
instance.Setup(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique",
token.Start, token.End, It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
@ -97,6 +313,12 @@ namespace OpenIddict.Tests
}));
builder.Services.AddSingleton(manager);
builder.Configure(options =>
{
options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == token.Start.Value);
options.AuthorizationCodeLifetime = token.End.Value - token.Start.Value;
});
});
var client = new OpenIdConnectClient(server.CreateClient());
@ -112,7 +334,81 @@ namespace OpenIddict.Tests
// Assert
Assert.NotNull(response.Code);
Mock.Get(manager).Verify(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique",
token.Start, token.End, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task SerializeAuthorizationCode_ReferenceAuthorizationCodeIsCorrectlyPersisted()
{
// Arrange
var token = new OpenIddictToken
{
End = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero),
Start = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero),
};
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique",
It.IsNotNull<string>(), It.IsNotNull<string>(),
token.Start, token.End, It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
});
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.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
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);
}));
builder.Services.AddSingleton(manager);
builder.UseReferenceTokens();
builder.Configure(options =>
{
options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == token.Start.Value);
options.AuthorizationCodeLifetime = token.End.Value - token.Start.Value;
});
});
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(
OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique",
It.IsNotNull<string>(), It.IsNotNull<string>(),
token.Start, token.End, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
@ -124,7 +420,10 @@ namespace OpenIddict.Tests
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", It.IsAny<CancellationToken>()))
instance.Setup(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique",
It.IsAny<DateTimeOffset?>(), It.IsAny<DateTimeOffset?>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
@ -183,7 +482,10 @@ namespace OpenIddict.Tests
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", It.IsAny<CancellationToken>()))
instance.Setup(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique",
It.IsAny<DateTimeOffset?>(), It.IsAny<DateTimeOffset?>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
@ -235,6 +537,7 @@ namespace OpenIddict.Tests
ClientId = "Fabrikam",
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = OpenIdConnectConstants.ResponseTypes.Code,
["attach-authorization"] = true
});
// Assert
@ -243,6 +546,72 @@ namespace OpenIddict.Tests
Mock.Get(manager).Verify(mock => mock.SetAuthorizationAsync(token, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task SerializeAuthorizationCode_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.HasRedirectUriAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
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(
OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique",
It.IsAny<DateTimeOffset?>(), It.IsAny<DateTimeOffset?>(),
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("Bob le Magnifique", "3E228451-1555-46F7-A471-951EFBA23A56",
It.IsAny<IEnumerable<string>>(), It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task SerializeRefreshToken_RefreshTokenIsNotPersistedWhenRevocationIsDisabled()
{
@ -272,18 +641,80 @@ namespace OpenIddict.Tests
// Assert
Assert.NotNull(response.RefreshToken);
Mock.Get(manager).Verify(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique",
It.IsAny<DateTimeOffset?>(), It.IsAny<DateTimeOffset?>(),
It.IsAny<CancellationToken>()), Times.Never());
}
[Fact]
public async Task SerializeRefreshToken_RefreshTokenIsCorrectlyPersisted()
{
// Arrange
var token = new OpenIddictToken();
var token = new OpenIddictToken
{
End = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero),
Start = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero)
};
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique",
token.Start, token.End, It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.Configure(options =>
{
options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == token.Start.Value);
options.RefreshTokenLifetime = token.End.Value - token.Start.Value;
});
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w",
Scope = OpenIdConnectConstants.Scopes.OfflineAccess
});
// Assert
Assert.NotNull(response.RefreshToken);
Mock.Get(manager).Verify(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique",
token.Start, token.End, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task SerializeRefreshToken_ReferenceRefreshTokenIsCorrectlyPersisted()
{
// Arrange
var token = new OpenIddictToken
{
End = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero),
Start = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero),
};
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", It.IsAny<CancellationToken>()))
instance.Setup(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique",
It.IsNotNull<string>(), It.IsNotNull<string>(),
token.Start, token.End, It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
@ -293,6 +724,14 @@ namespace OpenIddict.Tests
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.UseReferenceTokens();
builder.Configure(options =>
{
options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == token.Start.Value);
options.RefreshTokenLifetime = token.End.Value - token.Start.Value;
});
});
var client = new OpenIdConnectClient(server.CreateClient());
@ -309,7 +748,10 @@ namespace OpenIddict.Tests
// Assert
Assert.NotNull(response.RefreshToken);
Mock.Get(manager).Verify(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique",
It.IsNotNull<string>(), It.IsNotNull<string>(),
token.Start, token.End, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
@ -321,7 +763,10 @@ namespace OpenIddict.Tests
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", It.IsAny<CancellationToken>()))
instance.Setup(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique",
It.IsAny<DateTimeOffset?>(), It.IsAny<DateTimeOffset?>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
@ -376,7 +821,10 @@ namespace OpenIddict.Tests
var manager = CreateTokenManager(instance =>
{
instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique", It.IsAny<CancellationToken>()))
instance.Setup(mock => mock.CreateAsync(
OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique",
It.IsAny<DateTimeOffset?>(), It.IsAny<DateTimeOffset?>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
@ -405,7 +853,8 @@ namespace OpenIddict.Tests
GrantType = OpenIdConnectConstants.GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w",
Scope = OpenIdConnectConstants.Scopes.OfflineAccess
Scope = OpenIdConnectConstants.Scopes.OfflineAccess,
["attach-authorization"] = true
});
// Assert

54
test/OpenIddict.Tests/OpenIddictProviderTests.cs

@ -47,43 +47,46 @@ namespace OpenIddict.Tests
services.AddAuthentication();
services.AddOptions();
var instance = services.AddOpenIddict()
// Replace the default OpenIddict managers.
services.AddSingleton(CreateApplicationManager());
services.AddSingleton(CreateAuthorizationManager());
services.AddSingleton(CreateTokenManager());
services.AddOpenIddict(options =>
{
// Disable the transport security requirement during testing.
.DisableHttpsRequirement()
options.DisableHttpsRequirement();
// Enable the tested endpoints.
.EnableAuthorizationEndpoint(AuthorizationEndpoint)
.EnableIntrospectionEndpoint(IntrospectionEndpoint)
.EnableLogoutEndpoint(LogoutEndpoint)
.EnableRevocationEndpoint(RevocationEndpoint)
.EnableTokenEndpoint(TokenEndpoint)
.EnableUserinfoEndpoint(UserinfoEndpoint)
options.EnableAuthorizationEndpoint(AuthorizationEndpoint)
.EnableIntrospectionEndpoint(IntrospectionEndpoint)
.EnableLogoutEndpoint(LogoutEndpoint)
.EnableRevocationEndpoint(RevocationEndpoint)
.EnableTokenEndpoint(TokenEndpoint)
.EnableUserinfoEndpoint(UserinfoEndpoint);
// Enable the tested flows.
.AllowAuthorizationCodeFlow()
.AllowClientCredentialsFlow()
.AllowImplicitFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow()
options.AllowAuthorizationCodeFlow()
.AllowClientCredentialsFlow()
.AllowImplicitFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow();
// Register the X.509 certificate used to sign the identity tokens.
.AddSigningCertificate(
options.AddSigningCertificate(
assembly: typeof(OpenIddictProviderTests).GetTypeInfo().Assembly,
resource: "OpenIddict.Tests.Certificate.pfx",
password: "OpenIddict")
password: "OpenIddict");
// Note: overriding the default data protection provider is not necessary for the tests to pass,
// but is useful to ensure unnecessary keys are not persisted in testing environments, which also
// helps make the unit tests run faster, as no registry or disk access is required in this case.
.UseDataProtectionProvider(new EphemeralDataProtectionProvider());
options.UseDataProtectionProvider(new EphemeralDataProtectionProvider());
// Replace the default application/token managers.
services.AddSingleton(CreateApplicationManager());
services.AddSingleton(CreateTokenManager());
// Run the configuration delegate
// registered by the unit tests.
configuration?.Invoke(instance);
// Run the configuration delegate
// registered by the unit tests.
configuration?.Invoke(options);
});
});
builder.Configure(app =>
@ -145,7 +148,10 @@ namespace OpenIddict.Tests
ticket.SetScopes(request.GetScopes());
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70");
if (request.HasParameter("attach-authorization"))
{
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70");
}
return context.Authentication.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties);
}

Loading…
Cancel
Save