diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
index 19d62ec8..3e6be278 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
+++ b/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
/// The application to create.
/// The that can be used to abort the operation.
///
- /// A that can be used to monitor the asynchronous operation.
+ /// A that can be used to monitor the asynchronous operation, whose result returns the authorization.
///
- public virtual Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
+ public virtual Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
@@ -54,6 +55,38 @@ namespace OpenIddict.Core
return Store.CreateAsync(authorization, cancellationToken);
}
+ ///
+ /// Creates a new authorization.
+ ///
+ /// The subject associated with the authorization.
+ /// The client associated with the authorization.
+ /// The scopes associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation, whose result returns the authorization.
+ ///
+ public virtual Task CreateAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] IEnumerable 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);
+ }
+
///
/// Retrieves an authorization using its associated subject/client.
///
@@ -102,6 +135,45 @@ namespace OpenIddict.Core
return Store.GetIdAsync(authorization, cancellationToken);
}
+ ///
+ /// Revokes an authorization.
+ ///
+ /// The authorization to revoke.
+ /// The that can be used to abort the operation.
+ /// A that can be used to monitor the asynchronous operation.
+ 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);
+ }
+ }
+
+ ///
+ /// Updates an existing authorization.
+ ///
+ /// The authorization to update.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public virtual Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ if (authorization == null)
+ {
+ throw new ArgumentNullException(nameof(authorization));
+ }
+
+ return Store.UpdateAsync(authorization, cancellationToken);
+ }
+
///
/// Validates the authorization to ensure it's in a consistent state.
///
diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
index b1b95d1d..cd8db35b 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
@@ -65,17 +65,39 @@ namespace OpenIddict.Core
///
/// A that can be used to monitor the asynchronous operation, whose result returns the token.
///
- public virtual async Task CreateAsync([NotNull] string type, [NotNull] string subject, CancellationToken cancellationToken)
+ public virtual Task CreateAsync([NotNull] string type, [NotNull] string subject, 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, cancellationToken);
+ }
+
+ ///
+ /// Creates a new reference token, which is associated with a particular subject.
+ ///
+ /// The token type.
+ /// The subject associated with the token.
+ /// The hash of the crypto-secure random identifier associated with the token.
+ /// The ciphertext associated with the token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation, whose result returns the token.
+ ///
+ public virtual Task CreateAsync(
+ [NotNull] string type, [NotNull] string subject, [NotNull] string hash,
+ [NotNull] string ciphertext, 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 +105,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, cancellationToken);
+ }
+
+ ///
+ /// Retrieves the list of tokens corresponding to the specified authorization identifier.
+ ///
+ /// The authorization identifier associated with the tokens.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the specified authorization.
+ ///
+ public virtual Task FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken)
+ {
+ return Store.FindByAuthorizationIdAsync(identifier, cancellationToken);
+ }
+
+ ///
+ /// Retrieves the list of tokens corresponding to the specified hash.
+ ///
+ /// The hashed crypto-secure random identifier associated with the tokens.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the specified hash.
+ ///
+ public virtual Task FindByHashAsync(string hash, CancellationToken cancellationToken)
+ {
+ return Store.FindByHashAsync(hash, cancellationToken);
}
///
@@ -114,6 +169,63 @@ namespace OpenIddict.Core
return Store.FindBySubjectAsync(subject, cancellationToken);
}
+ ///
+ /// Retrieves the optional authorization identifier associated with a token.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorization identifier associated with the token.
+ ///
+ public virtual Task GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
+ {
+ if (token == null)
+ {
+ throw new ArgumentNullException(nameof(token));
+ }
+
+ return Store.GetAuthorizationIdAsync(token, cancellationToken);
+ }
+
+ ///
+ /// Retrieves the ciphertext associated with a token.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the ciphertext associated with the specified token.
+ ///
+ public virtual Task GetCiphertextAsync([NotNull] TToken token, CancellationToken cancellationToken)
+ {
+ if (token == null)
+ {
+ throw new ArgumentNullException(nameof(token));
+ }
+
+ return Store.GetCiphertextAsync(token, cancellationToken);
+ }
+
+ ///
+ /// Retrieves the hashed identifier associated with a token.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the hashed identifier associated with the specified token.
+ ///
+ public virtual Task GetHashAsync([NotNull] TToken token, CancellationToken cancellationToken)
+ {
+ if (token == null)
+ {
+ throw new ArgumentNullException(nameof(token));
+ }
+
+ return Store.GetHashAsync(token, cancellationToken);
+ }
+
///
/// Retrieves the unique identifier associated with a token.
///
@@ -133,20 +245,131 @@ namespace OpenIddict.Core
return Store.GetIdAsync(token, cancellationToken);
}
+ ///
+ /// Retrieves the status associated with a token.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the status associated with the specified token.
+ ///
+ public virtual Task GetStatusAsync([NotNull] TToken token, CancellationToken cancellationToken)
+ {
+ if (token == null)
+ {
+ throw new ArgumentNullException(nameof(token));
+ }
+
+ return Store.GetStatusAsync(token, cancellationToken);
+ }
+
+ ///
+ /// Determines whether a given token has already been redemeed.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ /// true if the token has already been redemeed, false otherwise.
+ public virtual async Task 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);
+ }
+
+ ///
+ /// Determines whether a given token has been revoked.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ /// true if the token has been revoked, false otherwise.
+ public virtual async Task 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);
+ }
+
+ ///
+ /// Determines whether a given token is valid.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ /// true if the token is valid, false otherwise.
+ public virtual async Task 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);
+ }
+
+ ///
+ /// Redeems a token.
+ ///
+ /// The token to redeem.
+ /// The that can be used to abort the operation.
+ /// A that can be used to monitor the asynchronous operation.
+ 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);
+ }
+ }
+
///
/// Revokes a token.
///
/// The token to revoke.
/// The that can be used to abort the operation.
/// A that can be used to monitor the asynchronous operation.
- 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);
+ }
}
///
diff --git a/src/OpenIddict.Core/OpenIddictConstants.cs b/src/OpenIddict.Core/OpenIddictConstants.cs
index ec3f7d4c..97b901b8 100644
--- a/src/OpenIddict.Core/OpenIddictConstants.cs
+++ b/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";
+ }
}
}
diff --git a/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs
index 5035e1a5..dbe35f14 100644
--- a/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs
+++ b/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
///
Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
+ ///
+ /// Creates a new authorization.
+ ///
+ /// The subject associated with the authorization.
+ /// The client associated with the authorization.
+ /// The scopes associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation, whose result returns the authorization.
+ ///
+ Task CreateAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] IEnumerable scopes, CancellationToken cancellationToken);
+
///
/// Retrieves an authorization using its unique identifier.
///
@@ -60,6 +75,17 @@ namespace OpenIddict.Core
///
Task GetIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
+ ///
+ /// Retrieves the status associated with an authorization.
+ ///
+ /// The authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the status associated with the specified authorization.
+ ///
+ Task GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
+
///
/// Retrieves the subject associated with an authorization.
///
@@ -70,5 +96,26 @@ namespace OpenIddict.Core
/// whose result returns the subject associated with the specified authorization.
///
Task GetSubjectAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
+
+ ///
+ /// Sets the status associated with an authorization.
+ ///
+ /// The authorization.
+ /// The status associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ Task SetStatusAsync([NotNull] TAuthorization authorization, [NotNull] string status, CancellationToken cancellationToken);
+
+ ///
+ /// Updates an existing authorization.
+ ///
+ /// The authorization to update.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
}
}
\ No newline at end of file
diff --git a/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs
index 4456f5bc..9840206c 100644
--- a/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs
+++ b/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs
@@ -37,6 +37,51 @@ namespace OpenIddict.Core
///
Task CreateAsync([NotNull] string type, [NotNull] string subject, CancellationToken cancellationToken);
+ ///
+ /// Creates a new reference token, which is associated with a particular subject.
+ ///
+ /// The token type.
+ /// The subject associated with the token.
+ /// The hash of the crypto-secure random identifier associated with the token.
+ /// The ciphertext associated with the token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation, whose result returns the token.
+ ///
+ Task CreateAsync(
+ [NotNull] string type, [NotNull] string subject, [NotNull] string hash,
+ [NotNull] string ciphertext, CancellationToken cancellationToken);
+
+ ///
+ /// Removes a token.
+ ///
+ /// The token to delete.
+ /// The that can be used to abort the operation.
+ /// A that can be used to monitor the asynchronous operation.
+ Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the list of tokens corresponding to the specified authorization identifier.
+ ///
+ /// The authorization identifier associated with the tokens.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the specified authorization.
+ ///
+ Task FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the list of tokens corresponding to the specified hash.
+ ///
+ /// The hashed crypto-secure random identifier associated with the tokens.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the specified hash.
+ ///
+ Task FindByHashAsync(string hash, CancellationToken cancellationToken);
+
///
/// Retrieves an token using its unique identifier.
///
@@ -59,6 +104,39 @@ namespace OpenIddict.Core
///
Task FindBySubjectAsync(string subject, CancellationToken cancellationToken);
+ ///
+ /// Retrieves the optional authorization identifier associated with a token.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorization identifier associated with the token.
+ ///
+ Task GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the ciphertext associated with a token.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the ciphertext associated with the specified token.
+ ///
+ Task GetCiphertextAsync([NotNull] TToken token, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the hashed identifier associated with a token.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the hashed identifier associated with the specified token.
+ ///
+ Task GetHashAsync([NotNull] TToken token, CancellationToken cancellationToken);
+
///
/// Retrieves the unique identifier associated with a token.
///
@@ -71,15 +149,15 @@ namespace OpenIddict.Core
Task GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken);
///
- /// Retrieves the token type associated with a token.
+ /// Retrieves the status associated with a token.
///
/// The token.
/// The that can be used to abort the operation.
///
/// A 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.
///
- Task GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken);
+ Task GetStatusAsync([NotNull] TToken token, CancellationToken cancellationToken);
///
/// Retrieves the subject associated with a token.
@@ -93,12 +171,15 @@ namespace OpenIddict.Core
Task GetSubjectAsync([NotNull] TToken token, CancellationToken cancellationToken);
///
- /// Revokes a token.
+ /// Retrieves the token type associated with a token.
///
- /// The token to revoke.
+ /// The token.
/// The that can be used to abort the operation.
- /// A that can be used to monitor the asynchronous operation.
- Task RevokeAsync([NotNull] TToken token, CancellationToken cancellationToken);
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the token type associated with the specified token.
+ ///
+ Task GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken);
///
/// Sets the authorization associated with a token.
@@ -122,6 +203,17 @@ namespace OpenIddict.Core
///
Task SetClientAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken);
+ ///
+ /// Sets the status associated with a token.
+ ///
+ /// The token.
+ /// The status associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ Task SetStatusAsync([NotNull] TToken token, [NotNull] string status, CancellationToken cancellationToken);
+
///
/// Updates an existing token.
///
diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs
index 93674ea1..ca82c718 100644
--- a/src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs
+++ b/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");
});
diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs
index dd2da734..87a99740 100644
--- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs
+++ b/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;
}
+ ///
+ /// Creates a new authorization.
+ ///
+ /// The subject associated with the authorization.
+ /// The client associated with the authorization.
+ /// The scopes associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation, whose result returns the authorization.
+ ///
+ public virtual async Task CreateAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] IEnumerable 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);
+ }
+
///
/// Retrieves an authorization using its associated subject/client.
///
@@ -160,6 +203,20 @@ namespace OpenIddict.EntityFrameworkCore
return Task.FromResult(ConvertIdentifierToString(authorization.Id));
}
+ ///
+ /// Retrieves the status associated with an authorization.
+ ///
+ /// The authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the status associated with the specified authorization.
+ ///
+ public virtual Task GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(authorization.Status);
+ }
+
///
/// Retrieves the subject associated with an authorization.
///
@@ -179,6 +236,44 @@ namespace OpenIddict.EntityFrameworkCore
return Task.FromResult(authorization.Subject);
}
+ ///
+ /// Sets the status associated with an authorization.
+ ///
+ /// The authorization.
+ /// The status associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public virtual Task SetStatusAsync([NotNull] TAuthorization authorization, [NotNull] string status, CancellationToken cancellationToken)
+ {
+ authorization.Status = status;
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Updates an existing authorization.
+ ///
+ /// The authorization to update.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public virtual async Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
+ {
+
+ Context.Attach(authorization);
+ Context.Update(authorization);
+
+ try
+ {
+ await Context.SaveChangesAsync(cancellationToken);
+ }
+
+ catch (DbUpdateConcurrencyException) { }
+ }
+
///
/// Converts the provided identifier to a strongly typed key object.
///
diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs
index b6b45092..d9a922a8 100644
--- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs
+++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs
@@ -125,7 +125,107 @@ namespace OpenIddict.EntityFrameworkCore
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
+ {
+ Subject = subject,
+ Type = type
+ };
+
+ return CreateAsync(token, cancellationToken);
+ }
+
+ ///
+ /// Creates a new reference token, which is associated with a particular subject.
+ ///
+ /// The token type.
+ /// The subject associated with the token.
+ /// The hash of the crypto-secure random identifier associated with the token.
+ /// The ciphertext associated with the token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation, whose result returns the token.
+ ///
+ public virtual Task CreateAsync(
+ [NotNull] string type, [NotNull] string subject, [NotNull] string hash,
+ [NotNull] string ciphertext, 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,
+ Hash = hash,
+ Subject = subject,
+ Type = type
+ };
+
+ return CreateAsync(token, cancellationToken);
+ }
+
+ ///
+ /// Removes a token.
+ ///
+ /// The token to delete.
+ /// The that can be used to abort the operation.
+ /// A that can be used to monitor the asynchronous operation.
+ 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) { }
+ }
+
+ ///
+ /// Retrieves the list of tokens corresponding to the specified authorization identifier.
+ ///
+ /// The authorization identifier associated with the tokens.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the specified authorization.
+ ///
+ public virtual Task FindByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken)
+ {
+ var key = ConvertIdentifierFromString(identifier);
+
+ return Tokens.Where(token => token.Authorization.Id.Equals(key)).ToArrayAsync(cancellationToken);
+ }
+
+ ///
+ /// Retrieves the list of tokens corresponding to the specified hash.
+ ///
+ /// The hashed crypto-secure random identifier associated with the tokens.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the specified hash.
+ ///
+ public virtual Task FindByHashAsync(string hash, CancellationToken cancellationToken)
+ {
+ return Tokens.SingleOrDefaultAsync(token => token.Hash == hash, cancellationToken);
}
///
@@ -158,6 +258,67 @@ namespace OpenIddict.EntityFrameworkCore
return Tokens.Where(token => token.Subject == subject).ToArrayAsync();
}
+ ///
+ /// Retrieves the optional authorization identifier associated with a token.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorization identifier associated with the token.
+ ///
+ public virtual async Task 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);
+ }
+
+ ///
+ /// Retrieves the ciphertext associated with a token.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the ciphertext associated with the specified token.
+ ///
+ public virtual Task GetCiphertextAsync([NotNull] TToken token, CancellationToken cancellationToken)
+ {
+ if (token == null)
+ {
+ throw new ArgumentNullException(nameof(token));
+ }
+
+ return Task.FromResult(token.Ciphertext);
+ }
+
+ ///
+ /// Retrieves the hashed identifier associated with a token.
+ ///
+ /// The token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the hashed identifier associated with the specified token.
+ ///
+ public virtual Task GetHashAsync([NotNull] TToken token, CancellationToken cancellationToken)
+ {
+ if (token == null)
+ {
+ throw new ArgumentNullException(nameof(token));
+ }
+
+ return Task.FromResult(token.Hash);
+ }
+
///
/// Retrieves the unique identifier associated with a token.
///
@@ -178,22 +339,22 @@ namespace OpenIddict.EntityFrameworkCore
}
///
- /// Retrieves the token type associated with a token.
+ /// Retrieves the status associated with a token.
///
/// The token.
/// The that can be used to abort the operation.
///
/// A 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.
///
- public virtual Task GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken)
+ public virtual Task GetStatusAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
- return Task.FromResult(token.Type);
+ return Task.FromResult(token.Status);
}
///
@@ -216,26 +377,22 @@ namespace OpenIddict.EntityFrameworkCore
}
///
- /// Revokes a token.
+ /// Retrieves the token type associated with a token.
///
- /// The token to revoke.
+ /// The token.
/// The that can be used to abort the operation.
- /// A that can be used to monitor the asynchronous operation.
- public virtual async Task RevokeAsync([NotNull] TToken token, CancellationToken cancellationToken)
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the token type associated with the specified token.
+ ///
+ public virtual Task 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);
}
///
@@ -324,6 +481,22 @@ namespace OpenIddict.EntityFrameworkCore
}
}
+ ///
+ /// Sets the status associated with a token.
+ ///
+ /// The token.
+ /// The status associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public virtual Task SetStatusAsync([NotNull] TToken token, [NotNull] string status, CancellationToken cancellationToken)
+ {
+ token.Status = status;
+
+ return Task.CompletedTask;
+ }
+
///
/// Updates an existing token.
///
diff --git a/src/OpenIddict.Models/OpenIddictAuthorization.cs b/src/OpenIddict.Models/OpenIddictAuthorization.cs
index 34a4be67..2da90c02 100644
--- a/src/OpenIddict.Models/OpenIddictAuthorization.cs
+++ b/src/OpenIddict.Models/OpenIddictAuthorization.cs
@@ -50,6 +50,11 @@ namespace OpenIddict.Models
///
public virtual string Scope { get; set; }
+ ///
+ /// Gets or sets the status of the current authorization.
+ ///
+ public virtual string Status { get; set; } = "valid";
+
///
/// Gets or sets the subject associated with the current authorization.
///
diff --git a/src/OpenIddict.Models/OpenIddictToken.cs b/src/OpenIddict.Models/OpenIddictToken.cs
index d4801084..ba6113c0 100644
--- a/src/OpenIddict.Models/OpenIddictToken.cs
+++ b/src/OpenIddict.Models/OpenIddictToken.cs
@@ -43,12 +43,31 @@ namespace OpenIddict.Models
///
public virtual TAuthorization Authorization { get; set; }
+ ///
+ /// Gets or sets the encrypted payload
+ /// of the current token, if applicable.
+ /// This property is only used for reference tokens.
+ ///
+ public virtual string Ciphertext { get; set; }
+
+ ///
+ /// Gets or sets the hashed identifier associated
+ /// with the current token, if applicable.
+ /// This property is only used for reference tokens.
+ ///
+ public virtual string Hash { get; set; }
+
///
/// Gets or sets the unique identifier
/// associated with the current token.
///
public virtual TKey Id { get; set; }
+ ///
+ /// Gets or sets the status of the current token.
+ ///
+ public virtual string Status { get; set; } = "valid";
+
///
/// Gets or sets the subject associated with the current token.
///
diff --git a/src/OpenIddict/OpenIddictExtensions.cs b/src/OpenIddict/OpenIddictExtensions.cs
index 0df50a88..d33d5b91 100644
--- a/src/OpenIddict/OpenIddictExtensions.cs
+++ b/src/OpenIddict/OpenIddictExtensions.cs
@@ -890,5 +890,26 @@ namespace Microsoft.AspNetCore.Builder
};
});
}
+
+ ///
+ /// 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.
+ ///
+ /// The services builder used by OpenIddict to register new services.
+ /// The .
+ public static OpenIddictBuilder UseReferenceTokens([NotNull] this OpenIddictBuilder builder)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ return builder.Configure(options => options.UseReferenceTokens = true);
+ }
}
}
\ No newline at end of file
diff --git a/src/OpenIddict/OpenIddictInitializer.cs b/src/OpenIddict/OpenIddictInitializer.cs
index 00e09f93..f01db70f 100644
--- a/src/OpenIddict/OpenIddictInitializer.cs
+++ b/src/OpenIddict/OpenIddictInitializer.cs
@@ -8,7 +8,10 @@ using System;
using System.ComponentModel;
using System.Linq;
using AspNet.Security.OpenIdConnect.Primitives;
+using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
@@ -23,13 +26,17 @@ namespace OpenIddict
public class OpenIddictInitializer : IPostConfigureOptions
{
private readonly IDistributedCache _cache;
+ private readonly IDataProtectionProvider _dataProtectionProvider;
///
/// Creates a new instance of the class.
///
- public OpenIddictInitializer([NotNull] IDistributedCache cache)
+ public OpenIddictInitializer(
+ [NotNull] IDistributedCache cache,
+ [NotNull] IDataProtectionProvider dataProtectionProvider)
{
_cache = cache;
+ _dataProtectionProvider = dataProtectionProvider;
}
///
@@ -50,6 +57,11 @@ namespace OpenIddict
throw new ArgumentException("The options instance name cannot be null or empty.", nameof(name));
}
+ if (options.RandomNumberGenerator == null)
+ {
+ throw new InvalidOperationException("A random number generator must be registered.");
+ }
+
// When no distributed cache has been registered in the options,
// try to resolve it from the dependency injection container.
if (options.Cache == null)
@@ -57,6 +69,52 @@ namespace OpenIddict
options.Cache = _cache;
}
+ // 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 = _dataProtectionProvider;
+ }
+
+ if (options.AccessTokenFormat == null)
+ {
+ var protector = options.DataProtectionProvider.CreateProtector(
+ nameof(OpenIdConnectServerHandler),
+ nameof(options.AccessTokenFormat),
+ nameof(options.UseReferenceTokens), name);
+
+ options.AccessTokenFormat = new TicketDataFormat(protector);
+ }
+
+ if (options.AuthorizationCodeFormat == null)
+ {
+ var protector = options.DataProtectionProvider.CreateProtector(
+ nameof(OpenIdConnectServerHandler),
+ nameof(options.AuthorizationCodeFormat),
+ nameof(options.UseReferenceTokens), name);
+
+ options.AuthorizationCodeFormat = new TicketDataFormat(protector);
+ }
+
+ if (options.RefreshTokenFormat == null)
+ {
+ var protector = options.DataProtectionProvider.CreateProtector(
+ nameof(OpenIdConnectServerHandler),
+ nameof(options.RefreshTokenFormat),
+ nameof(options.UseReferenceTokens), name);
+
+ options.RefreshTokenFormat = new TicketDataFormat(protector);
+ }
+ }
+
// Ensure at least one flow has been enabled.
if (options.GrantTypes.Count == 0)
{
@@ -88,6 +146,18 @@ namespace OpenIddict
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.");
+ }
+
if (options.AccessTokenHandler != null && options.SigningCredentials.Count == 0)
{
throw new InvalidOperationException(
diff --git a/src/OpenIddict/OpenIddictOptions.cs b/src/OpenIddict/OpenIddictOptions.cs
index 61aa03e2..96231b51 100644
--- a/src/OpenIddict/OpenIddictOptions.cs
+++ b/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;
@@ -54,11 +55,28 @@ namespace OpenIddict
///
public ISet GrantTypes { get; } = new HashSet(StringComparer.Ordinal);
+ ///
+ /// Gets or sets the random number generator used to generate crypto-secure identifiers.
+ ///
+ public RandomNumberGenerator RandomNumberGenerator { get; set; } = RandomNumberGenerator.Create();
+
///
/// 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.
///
public bool RequireClientIdentification { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether reference tokens should be used.
+ /// When set to true, 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.
+ ///
+ public bool UseReferenceTokens { get; set; }
}
}
diff --git a/src/OpenIddict/OpenIddictProvider.Exchange.cs b/src/OpenIddict/OpenIddictProvider.Exchange.cs
index a26d1816..82b6a3fe 100644
--- a/src/OpenIddict/OpenIddictProvider.Exchange.cs
+++ b/src/OpenIddict/OpenIddictProvider.Exchange.cs
@@ -11,6 +11,7 @@ using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
+using OpenIddict.Core;
namespace OpenIddict
{
@@ -197,56 +198,108 @@ namespace OpenIddict
{
var options = (OpenIddictOptions) context.Options;
- if (!options.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.SkipHandler();
- // 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.");
+ return;
+ }
+
+ 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 authorization code is no longer valid.");
+ context.Reject(
+ error: OpenIdConnectConstants.Errors.InvalidGrant,
+ description: "The specified 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 tokens = await Tokens.FindByAuthorizationIdAsync(key, context.HttpContext.RequestAborted);
+ for (var index = 0; index < tokens.Length; index++)
+ {
+ Logger.LogInformation("The compromised token '{Identifier}' was automatically revoked.",
+ await Tokens.GetIdAsync(tokens[index], context.HttpContext.RequestAborted));
+
+ await Tokens.RevokeAsync(tokens[index], context.HttpContext.RequestAborted);
+ }
}
- // 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 (context.Request.IsRefreshTokenGrantType())
+ else if (!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 authorization code 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 authorization code 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);
- }
+ // 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))
+ {
+ Logger.LogError("The token request was rejected because the refresh token was no longer valid.");
+
+ context.Reject(
+ error: OpenIdConnectConstants.Errors.InvalidGrant,
+ description: "The specified refresh token is no longer valid.");
+
+ return;
+ }
+
+ // 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);
}
}
diff --git a/src/OpenIddict/OpenIddictProvider.Introspection.cs b/src/OpenIddict/OpenIddictProvider.Introspection.cs
index 1350f2c4..cadf9130 100644
--- a/src/OpenIddict/OpenIddictProvider.Introspection.cs
+++ b/src/OpenIddict/OpenIddictProvider.Introspection.cs
@@ -116,13 +116,18 @@ namespace OpenIddict
return;
}
+ if (options.DisableTokenRevocation)
+ {
+ return;
+ }
+
// When the received ticket is revocable, ensure it is still valid.
- if (!options.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);
diff --git a/src/OpenIddict/OpenIddictProvider.Revocation.cs b/src/OpenIddict/OpenIddictProvider.Revocation.cs
index 49d0c392..fb7bd7c5 100644
--- a/src/OpenIddict/OpenIddictProvider.Revocation.cs
+++ b/src/OpenIddict/OpenIddictProvider.Revocation.cs
@@ -24,16 +24,28 @@ namespace OpenIddict
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
@@ -123,17 +135,31 @@ namespace OpenIddict
public override async Task HandleRevocationRequest([NotNull] HandleRevocationRequestContext context)
{
+ var options = (OpenIddictOptions) context.Options;
+
Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null.");
// 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;
}
@@ -145,7 +171,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);
diff --git a/src/OpenIddict/OpenIddictProvider.Serialization.cs b/src/OpenIddict/OpenIddictProvider.Serialization.cs
index 454f68d7..4ae3abbc 100644
--- a/src/OpenIddict/OpenIddictProvider.Serialization.cs
+++ b/src/OpenIddict/OpenIddictProvider.Serialization.cs
@@ -6,11 +6,16 @@
using System;
using System.Diagnostics;
+using System.Security.Cryptography;
+using System.Threading;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.Logging;
+using Microsoft.IdentityModel.Tokens;
using OpenIddict.Core;
namespace OpenIddict
@@ -18,108 +23,358 @@ namespace OpenIddict
public partial class OpenIddictProvider : OpenIdConnectServerProvider
where TApplication : class where TAuthorization : class where TScope : class where TToken : class
{
- public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context)
+ public override async Task DeserializeAccessToken([NotNull] DeserializeAccessTokenContext context)
{
var options = (OpenIddictOptions) context.Options;
+ if (!options.UseReferenceTokens)
+ {
+ return;
+ }
- Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId), "The client identifier shouldn't be null or empty.");
+ var ticket = await ReceiveTokenAsync(context.AccessToken, options, context.Request,
+ context.DataFormat, context.HttpContext.RequestAborted);
- if (!options.DisableTokenRevocation)
+ // If a valid ticket was returned by ReceiveTokenAsync(),
+ // force the OpenID Connect server middleware to use it.
+ if (ticket != null)
{
- // 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.Ticket = ticket;
+ context.HandleDeserialization();
+ }
+
+ // 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.Request,
+ context.DataFormat, context.HttpContext.RequestAborted);
+
+ // If a valid ticket was returned by ReceiveTokenAsync(),
+ // force the OpenID Connect server middleware to use it.
+ if (ticket != null)
+ {
+ context.Ticket = ticket;
+ context.HandleDeserialization();
+ }
+
+ // 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.Request,
+ context.DataFormat, context.HttpContext.RequestAborted);
+
+ // If a valid ticket was returned by ReceiveTokenAsync(),
+ // force the OpenID Connect server middleware to use it.
+ if (ticket != null)
+ {
+ context.Ticket = ticket;
+ context.HandleDeserialization();
+ }
+
+ // 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,
+ (OpenIddictOptions) context.Options, context.Request, context.DataFormat,
+ context.Ticket, context.HttpContext.RequestAborted);
+
+ // 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.HandleSerialization();
+ }
+
+ // Otherwise, let the OpenID Connect server middleware
+ // serialize the token using its default internal logic.
+ }
+
+ public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context)
+ {
+ var token = await CreateTokenAsync(OpenIdConnectConstants.TokenUsages.AuthorizationCode,
+ (OpenIddictOptions) context.Options, context.Request, context.DataFormat,
+ context.Ticket, context.HttpContext.RequestAborted);
+
+ // 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.HandleSerialization();
+ }
+
+ // 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,
+ (OpenIddictOptions) context.Options, context.Request, context.DataFormat,
+ context.Ticket, context.HttpContext.RequestAborted);
+
+ // If a reference token was returned by CreateTokenAsync(),
+ // force the OpenID Connect server middleware to use it.
+ if (!string.IsNullOrEmpty(token))
+ {
+ context.RefreshToken = token;
+ context.HandleSerialization();
+ }
+
+ // Otherwise, let the OpenID Connect server middleware
+ // serialize the token using its default internal logic.
+ }
+
+ private async Task CreateTokenAsync(
+ [NotNull] string type, [NotNull] OpenIddictOptions options,
+ [NotNull] OpenIdConnectRequest request,
+ [NotNull] ISecureDataFormat format,
+ [NotNull] AuthenticationTicket ticket, CancellationToken cancellationToken)
+ {
+ 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, cancellationToken);
+ }
+
+ // 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, cancellationToken);
+ }
+
+ 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, cancellationToken);
+ 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, cancellationToken);
+ 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, cancellationToken);
+
+ await Tokens.SetClientAsync(token, key, cancellationToken);
+ }
+
+ // 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), cancellationToken);
+ }
+
+ // 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(context.Request.ClientId, context.HttpContext.RequestAborted);
+ var application = await Applications.FindByClientIdAsync(request.ClientId, cancellationToken);
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, cancellationToken), request.GetScopes(), cancellationToken);
- // 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, cancellationToken);
+ ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, key);
+
+ await Tokens.SetAuthorizationAsync(token, key, cancellationToken);
}
}
+
+ 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 ReceiveTokenAsync(
+ [NotNull] string value, [NotNull] OpenIddictOptions options,
+ [NotNull] OpenIdConnectRequest request,
+ [NotNull] ISecureDataFormat format, CancellationToken cancellationToken)
{
- var options = (OpenIddictOptions) context.Options;
+ if (!options.UseReferenceTokens)
+ {
+ return null;
+ }
- if (!options.DisableTokenRevocation)
+ string hash;
+ try
{
- // 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))
+ // Compute the digest of the received token and use it
+ // to retrieve the reference token from the database.
+ using (var algorithm = SHA256.Create())
{
- throw new InvalidOperationException("The subject associated with the authentication ticket cannot be retrieved.");
+ hash = Convert.ToBase64String(algorithm.ComputeHash(Base64UrlEncoder.DecodeBytes(value)));
}
+ }
- // 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)
- {
- return;
- }
+ // Swallow format-related exceptions to ensure badly formed
+ // or tampered tokens don't cause an exception at this stage.
+ catch
+ {
+ return null;
+ }
- // 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.");
- }
+ // Retrieve the token entry from the database. If it
+ // cannot be found, assume the token is not valid.
+ var token = await Tokens.FindByHashAsync(hash, cancellationToken);
+ if (token == null)
+ {
+ Logger.LogInformation("The reference token corresponding to the '{Hash}' hashed " +
+ "identifier cannot be found in the database.", hash);
- // 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);
+ return null;
+ }
- // 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.");
- }
+ var identifier = await Tokens.GetIdAsync(token, cancellationToken);
+ 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.");
- await Tokens.SetClientAsync(token, await Applications.GetIdAsync(application, context.HttpContext.RequestAborted), context.HttpContext.RequestAborted);
- }
+ return null;
+ }
- // 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);
- }
+ // 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, cancellationToken);
+ 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, cancellationToken));
+
+ 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, cancellationToken));
}
+
+ 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;
}
}
}
\ No newline at end of file
diff --git a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs
index 6b1e416e..769cfcdc 100644
--- a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs
+++ b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs
@@ -614,6 +614,22 @@ namespace OpenIddict.Tests
Assert.IsType(options.AccessTokenHandler);
}
+ [Fact]
+ public void UseReferenceTokens_ReferenceTokensAreEnabled()
+ {
+ // Arrange
+ var services = CreateServices();
+ var builder = new OpenIddictBuilder(services);
+
+ // Act
+ builder.UseReferenceTokens();
+
+ var options = GetOptions(services);
+
+ // Assert
+ Assert.True(options.UseReferenceTokens);
+ }
+
private static IServiceCollection CreateServices()
{
var services = new ServiceCollection();
diff --git a/test/OpenIddict.Tests/OpenIddictInitializerTests.cs b/test/OpenIddict.Tests/OpenIddictInitializerTests.cs
index 6ce41c53..af5a7f44 100644
--- a/test/OpenIddict.Tests/OpenIddictInitializerTests.cs
+++ b/test/OpenIddict.Tests/OpenIddictInitializerTests.cs
@@ -16,6 +16,27 @@ namespace OpenIddict.Tests
{
public class OpenIddictInitializerTests
{
+ [Fact]
+ public async Task PostConfigure_ThrowsAnExceptionWhenRandomNumberGeneratorIsNull()
+ {
+ // Arrange
+ var server = CreateAuthorizationServer(builder =>
+ {
+ builder.Configure(options => options.RandomNumberGenerator = null);
+ });
+
+ var client = new OpenIdConnectClient(server.CreateClient());
+
+ // Act and assert
+ var exception = await Assert.ThrowsAsync(delegate
+ {
+ return client.GetAsync("/");
+ });
+
+ // Assert
+ Assert.Equal("A random number generator must be registered.", exception.Message);
+ }
+
[Fact]
public async Task PostConfigure_ThrowsAnExceptionWhenNoFlowIsEnabled()
{
@@ -111,6 +132,52 @@ namespace OpenIddict.Tests
Assert.Equal("The revocation endpoint cannot be enabled when token revocation is disabled.", exception.Message);
}
+ [Fact]
+ public async Task PostConfigure_ThrowsAnExceptionWhenUsingReferenceTokensWithTokenRevocationDisabled()
+ {
+ // Arrange
+ var server = CreateAuthorizationServer(builder =>
+ {
+ builder.EnableAuthorizationEndpoint("/connect/authorize")
+ .AllowImplicitFlow()
+ .DisableTokenRevocation()
+ .UseReferenceTokens();
+ });
+
+ var client = new OpenIdConnectClient(server.CreateClient());
+
+ // Act and assert
+ var exception = await Assert.ThrowsAsync(delegate
+ {
+ return client.GetAsync("/");
+ });
+
+ Assert.Equal("Reference tokens cannot be used when disabling token revocation.", exception.Message);
+ }
+
+ [Fact]
+ public async Task PostConfigure_ThrowsAnExceptionWhenUsingReferenceTokensIfAnAccessTokenHandlerIsSet()
+ {
+ // Arrange
+ var server = CreateAuthorizationServer(builder =>
+ {
+ builder.EnableAuthorizationEndpoint("/connect/authorize")
+ .AllowImplicitFlow()
+ .UseReferenceTokens()
+ .UseJsonWebTokens();
+ });
+
+ var client = new OpenIdConnectClient(server.CreateClient());
+
+ // Act and assert
+ var exception = await Assert.ThrowsAsync(delegate
+ {
+ return client.GetAsync("/");
+ });
+
+ Assert.Equal("Reference tokens cannot be used when configuring JWT as the access token format.", exception.Message);
+ }
+
[Fact]
public async Task PostConfigure_ThrowsAnExceptionWhenNoSigningKeyIsRegisteredIfAnAccessTokenHandlerIsSet()
{
diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs
index 33fedc91..fd1034eb 100644
--- a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs
+++ b/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;
@@ -457,7 +458,7 @@ namespace OpenIddict.Tests
}
[Fact]
- public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsExpired()
+ public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsUnknown()
{
// Arrange
var ticket = new AuthenticationTicket(
@@ -511,13 +512,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()), 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>();
+
+ 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()))
+ .ReturnsAsync(token);
+
+ instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny()))
+ .ReturnsAsync(true);
+ });
+
+ var server = CreateAuthorizationServer(builder =>
+ {
+ builder.Services.AddSingleton(CreateApplicationManager(instance =>
+ {
+ var application = new OpenIddictApplication();
+
+ instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny()))
+ .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()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny()), 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>();
+
+ 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()))
+ .ReturnsAsync(tokens[0]);
+
+ instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny()))
+ .ReturnsAsync(true);
+
+ instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()))
+ .ReturnsAsync(tokens);
+ });
+
+ var server = CreateAuthorizationServer(builder =>
+ {
+ builder.Services.AddSingleton(CreateApplicationManager(instance =>
+ {
+ var application = new OpenIddictApplication();
+
+ instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny()))
+ .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()))
+ .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()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny()), 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>();
+
+ 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()))
+ .ReturnsAsync(token);
+
+ instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny()))
+ .ReturnsAsync(false);
+
+ instance.Setup(mock => mock.IsValidAsync(token, It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ var server = CreateAuthorizationServer(builder =>
+ {
+ builder.Services.AddSingleton(CreateApplicationManager(instance =>
+ {
+ var application = new OpenIddictApplication();
+
+ instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny()))
+ .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()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsUnknown()
{
// Arrange
var ticket = new AuthenticationTicket(
@@ -568,13 +791,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()), 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>();
+
+ 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()))
+ .ReturnsAsync(token);
+
+ instance.Setup(mock => mock.IsValidAsync(token, It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ var server = CreateAuthorizationServer(builder =>
+ {
+ builder.Services.AddSingleton(CreateApplicationManager(instance =>
+ {
+ var application = new OpenIddictApplication();
+
+ instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny()))
+ .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()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task HandleTokenRequest_AuthorizationCodeIsAutomaticallyRedeemed()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
@@ -600,6 +886,9 @@ namespace OpenIddict.Tests
{
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()))
.ReturnsAsync(token);
+
+ instance.Setup(mock => mock.IsValidAsync(token, It.IsAny()))
+ .ReturnsAsync(true);
});
var server = CreateAuthorizationServer(builder =>
@@ -633,11 +922,11 @@ namespace OpenIddict.Tests
// Assert
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once());
- Mock.Get(manager).Verify(mock => mock.RevokeAsync(token, It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once());
}
[Fact]
- public async Task HandleTokenRequest_RefreshTokenIsAutomaticallyRevokedWhenSlidingExpirationIsEnabled()
+ public async Task HandleTokenRequest_RefreshTokenIsAutomaticallyRedeemedWhenSlidingExpirationIsEnabled()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
@@ -662,6 +951,12 @@ namespace OpenIddict.Tests
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()))
.ReturnsAsync(token);
+
+ instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny()))
+ .ReturnsAsync(false);
+
+ instance.Setup(mock => mock.IsValidAsync(token, It.IsAny()))
+ .ReturnsAsync(true);
});
var server = CreateAuthorizationServer(builder =>
@@ -693,7 +988,7 @@ namespace OpenIddict.Tests
// Assert
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once());
- Mock.Get(manager).Verify(mock => mock.RevokeAsync(token, It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once());
}
[Theory]
@@ -738,6 +1033,9 @@ namespace OpenIddict.Tests
{
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()))
.ReturnsAsync(token);
+
+ instance.Setup(mock => mock.IsValidAsync(token, It.IsAny()))
+ .ReturnsAsync(true);
});
var server = CreateAuthorizationServer(builder =>
diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs
index 75d15783..7401d32a 100644
--- a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs
+++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs
@@ -341,7 +341,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>();
+
+ format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"))
+ .Returns(ticket);
+
+ var manager = CreateTokenManager(instance =>
+ {
+ instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()))
+ .ReturnsAsync(value: null);
+ });
+
+ var server = CreateAuthorizationServer(builder =>
+ {
+ builder.Services.AddSingleton(CreateApplicationManager(instance =>
+ {
+ var application = new OpenIddictApplication();
+
+ instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny()))
+ .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential);
+
+ instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()))
+ .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()), 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>();
+
+ 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()))
+ .ReturnsAsync(token);
+
+ instance.Setup(mock => mock.IsValidAsync(token, It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ var server = CreateAuthorizationServer(builder =>
+ {
+ builder.Services.AddSingleton(CreateApplicationManager(instance =>
+ {
+ var application = new OpenIddictApplication();
+
+ instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny()))
+ .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential);
+
+ instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()))
+ .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()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task HandleIntrospectionRequest_RequestIsRejectedWhenAuthorizationCodeIsUnknown()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
@@ -405,7 +543,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>();
+
+ 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()))
+ .ReturnsAsync(token);
+
+ instance.Setup(mock => mock.IsValidAsync(token, It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ var server = CreateAuthorizationServer(builder =>
+ {
+ builder.Services.AddSingleton(CreateApplicationManager(instance =>
+ {
+ var application = new OpenIddictApplication();
+
+ instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny()))
+ .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential);
+
+ instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()))
+ .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()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task HandleIntrospectionRequest_RequestIsRejectedWhenRefreshTokenIsUnknown()
{
// Arrange
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
@@ -467,5 +675,75 @@ namespace OpenIddict.Tests
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), 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>();
+
+ 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()))
+ .ReturnsAsync(token);
+
+ instance.Setup(mock => mock.IsValidAsync(token, It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ var server = CreateAuthorizationServer(builder =>
+ {
+ builder.Services.AddSingleton(CreateApplicationManager(instance =>
+ {
+ var application = new OpenIddictApplication();
+
+ instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny()))
+ .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential);
+
+ instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()))
+ .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()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once());
+ }
}
}
diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs
index 557bdacb..4d95f3ec 100644
--- a/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs
+++ b/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;
@@ -21,10 +20,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();
@@ -35,13 +54,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]
@@ -218,7 +238,7 @@ namespace OpenIddict.Tests
}
[Fact]
- public async Task HandleRevocationRequest_RequestIsRejectedWhenTokenIsAnAccessToken()
+ public async Task HandleRevocationRequest_RequestIsRejectedWhenTokenIsAnAccessTokenIfReferenceTokensAreDisabled()
{
// Arrange
var ticket = new AuthenticationTicket(
@@ -249,13 +269,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(mock =>
@@ -289,7 +309,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()
.Verify(mock => mock.CanReadToken("SlAV32hkKG"), Times.Once());
@@ -299,7 +319,7 @@ namespace OpenIddict.Tests
}
[Fact]
- public async Task HandleRevocationRequest_TokenIsNotRevokedWhenItIsAlreadyInvalid()
+ public async Task HandleRevocationRequest_TokenIsNotRevokedWhenItIsUnknown()
{
// Arrange
var ticket = new AuthenticationTicket(
@@ -342,6 +362,55 @@ namespace OpenIddict.Tests
Mock.Get(manager).Verify(mock => mock.RevokeAsync(It.IsAny(), It.IsAny()), 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>();
+
+ 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()))
+ .ReturnsAsync(token);
+
+ instance.Setup(mock => mock.IsRevokedAsync(token, It.IsAny()))
+ .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()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.RevokeAsync(It.IsAny(), It.IsAny()), Times.Never());
+ }
+
[Fact]
public async Task HandleRevocationRequest_TokenIsSuccessfullyRevoked()
{
diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs
index f4eb0ce0..1f0d427a 100644
--- a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs
+++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs
@@ -1,4 +1,6 @@
-using System.Threading;
+using System.Collections;
+using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Client;
using AspNet.Security.OpenIdConnect.Primitives;
@@ -14,6 +16,190 @@ 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()), Times.Never());
+ }
+
+ [Fact]
+ public async Task SerializeAccessToken_ReferenceAccessTokenIsCorrectlyPersisted()
+ {
+ // Arrange
+ var token = new OpenIddictToken();
+
+ var manager = CreateTokenManager(instance =>
+ {
+ instance.Setup(mock => mock.CreateAsync(
+ OpenIdConnectConstants.TokenTypeHints.AccessToken, "Bob le Magnifique",
+ It.IsNotNull(), It.IsNotNull(), It.IsAny()))
+ .ReturnsAsync(token);
+
+ instance.Setup(mock => mock.GetIdAsync(token, It.IsAny()))
+ .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
+ });
+
+ var server = CreateAuthorizationServer(builder =>
+ {
+ 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
+ });
+
+ // Assert
+ Assert.NotNull(response.AccessToken);
+
+ Mock.Get(manager).Verify(mock => mock.CreateAsync(
+ OpenIdConnectConstants.TokenTypeHints.AccessToken, "Bob le Magnifique",
+ It.IsNotNull(), It.IsNotNull(), It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), 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(), It.IsNotNull(), It.IsAny()))
+ .ReturnsAsync(token);
+
+ instance.Setup(mock => mock.GetIdAsync(token, It.IsAny()))
+ .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
+
+ instance.Setup(mock => mock.SetClientAsync(token, "3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()))
+ .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()))
+ .ReturnsAsync(application);
+
+ instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny()))
+ .ReturnsAsync(OpenIddictConstants.ClientTypes.Public);
+
+ instance.Setup(mock => mock.GetIdAsync(application, It.IsAny()))
+ .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()), 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(), It.IsNotNull(), It.IsAny()))
+ .ReturnsAsync(token);
+
+ instance.Setup(mock => mock.GetIdAsync(token, It.IsAny()))
+ .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
+
+ instance.Setup(mock => mock.SetAuthorizationAsync(token, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny()))
+ .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()))
+ .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()), Times.Once());
+ }
+
[Fact]
public async Task SerializeAuthorizationCode_AuthorizationCodeIsNotPersistedWhenRevocationIsDisabled()
{
@@ -116,6 +302,65 @@ namespace OpenIddict.Tests
Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once());
}
+ [Fact]
+ public async Task SerializeAuthorizationCode_ReferenceAuthorizationCodeIsCorrectlyPersisted()
+ {
+ // Arrange
+ var token = new OpenIddictToken();
+
+ var manager = CreateTokenManager(instance =>
+ {
+ instance.Setup(mock => mock.CreateAsync(
+ OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique",
+ It.IsNotNull(), It.IsNotNull(), It.IsAny()))
+ .ReturnsAsync(token);
+
+ instance.Setup(mock => mock.GetIdAsync(token, It.IsAny()))
+ .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()))
+ .ReturnsAsync(application);
+
+ instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny()))
+ .ReturnsAsync(true);
+
+ instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()))
+ .ReturnsAsync(true);
+
+ instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny()))
+ .ReturnsAsync(OpenIddictConstants.ClientTypes.Public);
+ }));
+
+ builder.Services.AddSingleton(manager);
+
+ builder.UseReferenceTokens();
+ });
+
+ 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(), It.IsNotNull(), It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once());
+ }
+
[Fact]
public async Task SerializeAuthorizationCode_ClientApplicationIsAutomaticallyAttached()
{
@@ -235,6 +480,7 @@ namespace OpenIddict.Tests
ClientId = "Fabrikam",
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = OpenIdConnectConstants.ResponseTypes.Code,
+ ["attach-authorization"] = true
});
// Assert
@@ -243,6 +489,69 @@ namespace OpenIddict.Tests
Mock.Get(manager).Verify(mock => mock.SetAuthorizationAsync(token, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny()), 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()))
+ .ReturnsAsync(new OpenIddictAuthorization());
+ });
+
+ var server = CreateAuthorizationServer(builder =>
+ {
+ builder.Services.AddSingleton(CreateApplicationManager(instance =>
+ {
+ var application = new OpenIddictApplication();
+
+ instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny()))
+ .ReturnsAsync(true);
+
+ instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()))
+ .ReturnsAsync(true);
+
+ instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny()))
+ .ReturnsAsync(OpenIddictConstants.ClientTypes.Public);
+
+ instance.Setup(mock => mock.GetIdAsync(application, It.IsAny()))
+ .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
+ }));
+
+ builder.Services.AddSingleton(CreateTokenManager(instance =>
+ {
+ instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, "Bob le Magnifique", It.IsAny()))
+ .ReturnsAsync(token);
+
+ instance.Setup(mock => mock.GetIdAsync(token, It.IsAny()))
+ .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>(), It.IsAny()), Times.Once());
+ }
+
[Fact]
public async Task SerializeRefreshToken_RefreshTokenIsNotPersistedWhenRevocationIsDisabled()
{
@@ -313,6 +622,50 @@ namespace OpenIddict.Tests
Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once());
}
+ [Fact]
+ public async Task SerializeRefreshToken_ReferenceRefreshTokenIsCorrectlyPersisted()
+ {
+ // Arrange
+ var token = new OpenIddictToken();
+
+ var manager = CreateTokenManager(instance =>
+ {
+ instance.Setup(mock => mock.CreateAsync(
+ OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique",
+ It.IsNotNull(), It.IsNotNull(), It.IsAny()))
+ .ReturnsAsync(token);
+
+ instance.Setup(mock => mock.GetIdAsync(token, It.IsAny()))
+ .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
+ });
+
+ var server = CreateAuthorizationServer(builder =>
+ {
+ 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
+ });
+
+ // Assert
+ Assert.NotNull(response.RefreshToken);
+
+ Mock.Get(manager).Verify(mock => mock.CreateAsync(
+ OpenIdConnectConstants.TokenTypeHints.RefreshToken, "Bob le Magnifique",
+ It.IsNotNull(), It.IsNotNull(), It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once());
+ }
+
[Fact]
public async Task SerializeRefreshToken_ClientApplicationIsAutomaticallyAttached()
{
@@ -405,7 +758,8 @@ namespace OpenIddict.Tests
GrantType = OpenIdConnectConstants.GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w",
- Scope = OpenIdConnectConstants.Scopes.OfflineAccess
+ Scope = OpenIdConnectConstants.Scopes.OfflineAccess,
+ ["attach-authorization"] = true
});
// Assert
diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.cs
index ad04f052..e678f17c 100644
--- a/test/OpenIddict.Tests/OpenIddictProviderTests.cs
+++ b/test/OpenIddict.Tests/OpenIddictProviderTests.cs
@@ -146,7 +146,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.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties);
}