diff --git a/build/dependencies.props b/build/dependencies.props index cdefb81b..559afa3e 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -2,7 +2,7 @@ 2.0.0-* - 2.0.0-* + 2.0.0-rc1-final 2.0.0 4.4.0 3.0.0 diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index 82ed13ed..36208f6f 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -207,6 +207,69 @@ namespace OpenIddict.Core return Store.GetIdAsync(authorization, 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. + /// + public virtual Task GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return Store.GetStatusAsync(authorization, cancellationToken); + } + + /// + /// Determines whether a given authorization has been revoked. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// true if the authorization has been revoked, false otherwise. + public virtual async Task IsRevokedAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + var status = await Store.GetStatusAsync(authorization, cancellationToken); + if (string.IsNullOrEmpty(status)) + { + return false; + } + + return string.Equals(status, OpenIddictConstants.Statuses.Revoked, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Determines whether a given authorization is valid. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// true if the authorization is valid, false otherwise. + public virtual async Task IsValidAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + var status = await Store.GetStatusAsync(authorization, cancellationToken); + if (string.IsNullOrEmpty(status)) + { + return false; + } + + return string.Equals(status, OpenIddictConstants.Statuses.Valid, StringComparison.OrdinalIgnoreCase); + } + /// /// Executes the specified query. /// @@ -301,7 +364,8 @@ namespace OpenIddict.Core var descriptor = new OpenIddictAuthorizationDescriptor { Status = await Store.GetStatusAsync(authorization, cancellationToken), - Subject = await Store.GetSubjectAsync(authorization, cancellationToken) + Subject = await Store.GetSubjectAsync(authorization, cancellationToken), + Type = await Store.GetTypeAsync(authorization, cancellationToken) }; await ValidateAsync(descriptor, cancellationToken); diff --git a/src/OpenIddict.Core/OpenIddictConstants.cs b/src/OpenIddict.Core/OpenIddictConstants.cs index dce4eb79..88149927 100644 --- a/src/OpenIddict.Core/OpenIddictConstants.cs +++ b/src/OpenIddict.Core/OpenIddictConstants.cs @@ -39,8 +39,8 @@ namespace OpenIddict.Core public static class Properties { + public const string AuthenticationTicket = ".authentication_ticket"; public const string AuthorizationId = ".authorization_id"; - public const string TokenId = ".token_id"; } public static class Separators diff --git a/src/OpenIddict/OpenIddictExtensions.cs b/src/OpenIddict/OpenIddictExtensions.cs index 9960ae52..1b40612f 100644 --- a/src/OpenIddict/OpenIddictExtensions.cs +++ b/src/OpenIddict/OpenIddictExtensions.cs @@ -546,8 +546,8 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Disables sliding expiration. When using this option, a single refresh token - /// is issued with a fixed expiration date: when it expires, a complete + /// Disables sliding expiration. When using this option, refresh tokens + /// are issued with a fixed expiration date: when it expires, a complete /// authorization flow must be started to retrieve a new refresh token. /// /// The services builder used by OpenIddict to register new services. @@ -972,10 +972,8 @@ namespace Microsoft.Extensions.DependencyInjection /// /// Configures OpenIddict to use rolling refresh tokens. When this option is enabled, - /// a new refresh token is issued for each refresh token request and the previous one - /// is automatically revoked (when disabled, no new refresh token is issued and the - /// lifetime of the original refresh token is increased by updating the database entry). - /// Note: this option cannot be used when manually disabling sliding expiration. + /// a new refresh token is always issued for each refresh token request (and the previous + /// one is automatically revoked unless token revocation was explicitly disabled). /// /// The services builder used by OpenIddict to register new services. /// The . diff --git a/src/OpenIddict/OpenIddictInitializer.cs b/src/OpenIddict/OpenIddictInitializer.cs index 16a52332..e672da53 100644 --- a/src/OpenIddict/OpenIddictInitializer.cs +++ b/src/OpenIddict/OpenIddictInitializer.cs @@ -158,14 +158,10 @@ namespace OpenIddict "Reference tokens cannot be used when configuring JWT as the access token format."); } - if (options.UseRollingTokens && options.DisableTokenRevocation) + if (options.UseSlidingExpiration && options.DisableTokenRevocation && !options.UseRollingTokens) { - throw new InvalidOperationException("Rolling tokens cannot be used when disabling token expiration."); - } - - if (options.UseRollingTokens && !options.UseSlidingExpiration) - { - throw new InvalidOperationException("Rolling tokens cannot be used without enabling sliding expiration."); + throw new InvalidOperationException("Sliding expiration must be disabled when turning off " + + "token revocation if rolling tokens are not used."); } if (options.AccessTokenHandler != null && options.SigningCredentials.Count == 0) diff --git a/src/OpenIddict/OpenIddictOptions.cs b/src/OpenIddict/OpenIddictOptions.cs index 42bfca1c..ca1265eb 100644 --- a/src/OpenIddict/OpenIddictOptions.cs +++ b/src/OpenIddict/OpenIddictOptions.cs @@ -105,7 +105,8 @@ namespace OpenIddict /// When disabled, no new token is issued and the refresh token lifetime is /// dynamically managed by updating the token entry in the database. /// When this option is enabled, a new refresh token is issued for each - /// refresh token request and the previous one is automatically revoked. + /// refresh token request (and the previous one is automatically revoked + /// unless token revocation was explicitly disabled in the options). /// public bool UseRollingTokens { get; set; } } diff --git a/src/OpenIddict/OpenIddictProvider.Exchange.cs b/src/OpenIddict/OpenIddictProvider.Exchange.cs index 8df9b3cf..d52a2500 100644 --- a/src/OpenIddict/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict/OpenIddictProvider.Exchange.cs @@ -202,6 +202,12 @@ namespace OpenIddict { var options = (OpenIddictOptions) context.Options; + if (context.Ticket != null) + { + // Store the authentication ticket as a request property so it can be later retrieved, if necessary. + context.Request.SetProperty(OpenIddictConstants.Properties.AuthenticationTicket, context.Ticket); + } + if (options.DisableTokenRevocation || (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType())) { @@ -215,127 +221,58 @@ namespace OpenIddict Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null."); // Extract the token identifier from the authentication ticket. - var identifier = context.Ticket.GetProperty(OpenIdConnectConstants.Properties.TokenId); - Debug.Assert(!string.IsNullOrEmpty(identifier), "The authentication ticket should contain a ticket identifier."); + var identifier = context.Ticket.GetTokenId(); + Debug.Assert(!string.IsNullOrEmpty(identifier), + "The authentication ticket should contain a ticket identifier."); - // Store the original authorization code/refresh token so it can be later retrieved. - context.Request.SetProperty(OpenIddictConstants.Properties.TokenId, identifier); - - if (context.Request.IsAuthorizationCodeGrantType()) + // Retrieve the authorization code/refresh token from the database and ensure it is still valid. + var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); + if (token == null) { - // Retrieve the authorization code from the database and ensure it is still valid. - var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); - if (token == null) - { - Logger.LogError("The token request was rejected because the authorization " + - "code '{Identifier}' was not found in the database.", identifier); - - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The specified authorization code is no longer valid."); - - return; - } - - // If the authorization code is already marked as redeemed, this may indicate that the authorization - // code was compromised. In this case, revoke the authorization and all the associated tokens. - // See https://tools.ietf.org/html/rfc6749#section-10.5 for more information. - if (await Tokens.IsRedeemedAsync(token, context.HttpContext.RequestAborted)) - { - var key = context.Ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); - if (!string.IsNullOrEmpty(key)) - { - var authorization = await Authorizations.FindByIdAsync(key, context.HttpContext.RequestAborted); - if (authorization != null) - { - Logger.LogInformation("The authorization '{Identifier}' was automatically revoked.", key); - - await Authorizations.RevokeAsync(authorization, context.HttpContext.RequestAborted); - } - - var tokens = await Tokens.FindByAuthorizationIdAsync(key, context.HttpContext.RequestAborted); - for (var index = 0; index < tokens.Length; index++) - { - Logger.LogInformation("The compromised token '{Identifier}' was automatically revoked.", - await Tokens.GetIdAsync(tokens[index], context.HttpContext.RequestAborted)); - - await Tokens.RevokeAsync(tokens[index], context.HttpContext.RequestAborted); - } - } - - Logger.LogError("The token request was rejected because the authorization code " + - "'{Identifier}' has already been redeemed.", identifier); + Logger.LogError("The token request was rejected because the authorization code " + + "or refresh token '{Identifier}' was not found in the database.", identifier); - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The specified authorization code has already been redeemed."); - - return; - } - - else if (!await Tokens.IsValidAsync(token, context.HttpContext.RequestAborted)) - { - Logger.LogError("The token request was rejected because the authorization code " + - "'{Identifier}' was no longer valid.", identifier); - - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The specified authorization code is no longer valid."); - - return; - } + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: context.Request.IsAuthorizationCodeGrantType() ? + "The specified authorization code is no longer valid." : + "The specified refresh token is no longer valid."); - // Mark the authorization code as redeemed to prevent token reuse. - await Tokens.RedeemAsync(token, context.HttpContext.RequestAborted); + return; } - else + // If the authorization code/refresh token is already marked as redeemed, this may indicate that + // it was compromised. In this case, revoke the authorization and all the associated tokens. + // See https://tools.ietf.org/html/rfc6749#section-10.5 for more information. + if (await Tokens.IsRedeemedAsync(token, context.HttpContext.RequestAborted)) { - // Retrieve the token from the database and ensure it is still valid. - var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); - if (token == null) - { - Logger.LogError("The token request was rejected because the refresh token " + - "'{Identifier}' was not found in the database.", identifier); - - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The specified refresh token is no longer valid."); - - return; - } - - else if (await Tokens.IsRedeemedAsync(token, context.HttpContext.RequestAborted)) - { - Logger.LogError("The token request was rejected because the refresh token " + - "'{Identifier}' has already been redeemed.", identifier); + await RevokeAuthorizationAsync(context.Ticket, context.HttpContext); + await RevokeTokensAsync(context.Ticket, context.HttpContext); - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The specified refresh token has already been redeemed."); + Logger.LogError("The token request was rejected because the authorization code " + + "or refresh token '{Identifier}' has already been redeemed.", identifier); - return; - } + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: context.Request.IsAuthorizationCodeGrantType() ? + "The specified authorization code has already been redeemed." : + "The specified refresh token has already been redeemed."); - else if (!await Tokens.IsValidAsync(token, context.HttpContext.RequestAborted)) - { - Logger.LogError("The token request was rejected because the refresh token " + - "'{Identifier}' was no longer valid.", identifier); + return; + } - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The specified refresh token is no longer valid."); + else if (!await Tokens.IsValidAsync(token, context.HttpContext.RequestAborted)) + { + Logger.LogError("The token request was rejected because the authorization code " + + "or refresh token '{Identifier}' was no longer valid.", identifier); - return; - } + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: context.Request.IsAuthorizationCodeGrantType() ? + "The specified authorization code is no longer valid." : + "The specified refresh token is no longer valid."); - // When rolling tokens are enabled, immediately - // redeem the refresh token to prevent future reuse. - // See https://tools.ietf.org/html/rfc6749#section-6. - if (options.UseRollingTokens) - { - await Tokens.RedeemAsync(token, context.HttpContext.RequestAborted); - } + return; } // Invoke the rest of the pipeline to allow diff --git a/src/OpenIddict/OpenIddictProvider.Helpers.cs b/src/OpenIddict/OpenIddictProvider.Helpers.cs new file mode 100644 index 00000000..f951490e --- /dev/null +++ b/src/OpenIddict/OpenIddictProvider.Helpers.cs @@ -0,0 +1,347 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using AspNet.Security.OpenIdConnect.Primitives; +using AspNet.Security.OpenIdConnect.Server; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Core; + +namespace OpenIddict +{ + public partial class OpenIddictProvider : OpenIdConnectServerProvider + where TApplication : class where TAuthorization : class where TScope : class where TToken : class + { + private async Task CreateAuthorizationAsync( + [NotNull] AuthenticationTicket ticket, [NotNull] OpenIddictOptions options, + [NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request) + { + if (options.DisableTokenRevocation) + { + return; + } + + var descriptor = new OpenIddictAuthorizationDescriptor + { + Status = OpenIddictConstants.Statuses.Valid, + Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject), + Type = OpenIddictConstants.AuthorizationTypes.AdHoc + }; + + foreach (var scope in request.GetScopes()) + { + descriptor.Scopes.Add(scope); + } + + // If the client application is known, bind it to the authorization. + if (!string.IsNullOrEmpty(request.ClientId)) + { + var application = await Applications.FindByClientIdAsync(request.ClientId, context.RequestAborted); + if (application == null) + { + throw new InvalidOperationException("The client application cannot be retrieved from the database."); + } + + descriptor.ApplicationId = await Applications.GetIdAsync(application, context.RequestAborted); + } + + var authorization = await Authorizations.CreateAsync(descriptor, context.RequestAborted); + if (authorization != null) + { + var identifier = await Authorizations.GetIdAsync(authorization, context.RequestAborted); + + if (string.IsNullOrEmpty(request.ClientId)) + { + Logger.LogInformation("An ad hoc authorization was automatically created and " + + "associated with an unknown application: {Identifier}.", identifier); + } + + else + { + Logger.LogInformation("An ad hoc authorization was automatically created and " + + "associated with the '{ClientId}' application: {Identifier}.", + request.ClientId, identifier); + } + + // Attach the unique identifier of the ad hoc authorization to the authentication ticket + // so that it is attached to all the derived tokens, allowing batched revocations support. + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, identifier); + } + } + + private async Task CreateTokenAsync( + [NotNull] string type, [NotNull] AuthenticationTicket ticket, + [NotNull] OpenIddictOptions options, [NotNull] HttpContext context, + [NotNull] OpenIdConnectRequest request, + [NotNull] ISecureDataFormat format) + { + Debug.Assert(!(options.DisableTokenRevocation && options.UseReferenceTokens), + "Token revocation cannot be disabled when using reference tokens."); + + Debug.Assert(type == OpenIdConnectConstants.TokenUsages.AccessToken || + type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || + type == OpenIdConnectConstants.TokenUsages.RefreshToken, + "Only authorization codes, access and refresh tokens should be created using this method."); + + // When sliding expiration is disabled, the expiration date of generated refresh tokens is fixed + // and must exactly match the expiration date of the refresh token used in the token request. + if (request.IsTokenRequest() && request.IsRefreshTokenGrantType() && + !options.UseSlidingExpiration && type == OpenIdConnectConstants.TokenUsages.RefreshToken) + { + var properties = request.GetProperty( + OpenIddictConstants.Properties.AuthenticationTicket)?.Properties; + Debug.Assert(properties != null, "The authentication properties shouldn't be null."); + + ticket.Properties.ExpiresUtc = properties.ExpiresUtc; + } + + if (options.DisableTokenRevocation) + { + return null; + } + + var descriptor = new OpenIddictTokenDescriptor + { + AuthorizationId = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId), + CreationDate = ticket.Properties.IssuedUtc, + ExpirationDate = ticket.Properties.ExpiresUtc, + Status = OpenIddictConstants.Statuses.Valid, + Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject), + Type = type + }; + + string result = null; + + // When reference tokens are enabled or when the token is an authorization code or a + // refresh token, remove the unnecessary properties from the authentication ticket. + if (options.UseReferenceTokens || + (type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || + type == OpenIdConnectConstants.TokenUsages.RefreshToken)) + { + ticket.Properties.IssuedUtc = ticket.Properties.ExpiresUtc = null; + ticket.RemoveProperty(OpenIddictConstants.Properties.AuthorizationId) + .RemoveProperty(OpenIdConnectConstants.Properties.TokenId); + } + + // If reference tokens are enabled, create a new entry for + // authorization codes, refresh tokens and access tokens. + if (options.UseReferenceTokens) + { + // Note: the data format is automatically replaced at startup time to ensure + // that encrypted tokens stored in the database cannot be considered as + // valid tokens if the developer decides to disable reference tokens support. + descriptor.Ciphertext = format.Protect(ticket); + + // Generate a new crypto-secure random identifier that will be + // substituted to the ciphertext returned by the data format. + var bytes = new byte[256 / 8]; + options.RandomNumberGenerator.GetBytes(bytes); + result = Base64UrlEncoder.Encode(bytes); + + // Compute the digest of the generated identifier and use + // it as the hashed identifier of the reference token. + // Doing that prevents token identifiers stolen from + // the database from being used as valid reference tokens. + using (var algorithm = SHA256.Create()) + { + descriptor.Hash = Convert.ToBase64String(algorithm.ComputeHash(bytes)); + } + } + + // Otherwise, only create a token metadata entry for authorization codes and refresh tokens. + else if (type != OpenIdConnectConstants.TokenUsages.AuthorizationCode && + type != OpenIdConnectConstants.TokenUsages.RefreshToken) + { + return null; + } + + // If the client application is known, associate it with the token. + if (!string.IsNullOrEmpty(request.ClientId)) + { + var application = await Applications.FindByClientIdAsync(request.ClientId, context.RequestAborted); + if (application == null) + { + throw new InvalidOperationException("The client application cannot be retrieved from the database."); + } + + descriptor.ApplicationId = await Applications.GetIdAsync(application, context.RequestAborted); + } + + // If a null value was returned by CreateAsync(), return immediately. + var token = await Tokens.CreateAsync(descriptor, context.RequestAborted); + if (token == null) + { + return null; + } + + // Throw an exception if the token identifier can't be resolved. + var identifier = await Tokens.GetIdAsync(token, context.RequestAborted); + if (string.IsNullOrEmpty(identifier)) + { + throw new InvalidOperationException("The unique key associated with a refresh token cannot be null or empty."); + } + + // Restore the token identifier using the unique + // identifier attached with the database entry. + ticket.SetTokenId(identifier); + + // Dynamically set the creation and expiration dates. + ticket.Properties.IssuedUtc = await Tokens.GetCreationDateAsync(token, context.RequestAborted); + ticket.Properties.ExpiresUtc = await Tokens.GetExpirationDateAsync(token, context.RequestAborted); + + // Restore the authorization identifier using the identifier attached with the database entry. + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, + await Tokens.GetAuthorizationIdAsync(token, context.RequestAborted)); + + if (!string.IsNullOrEmpty(result)) + { + Logger.LogTrace("A new reference token was successfully generated and persisted " + + "in the database: {Token} ; {Claims} ; {Properties}.", + result, ticket.Principal.Claims, ticket.Properties.Items); + } + + return result; + } + + private async Task ReceiveTokenAsync( + [NotNull] string value, [NotNull] OpenIddictOptions options, + [NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request, + [NotNull] ISecureDataFormat format) + { + if (!options.UseReferenceTokens) + { + return null; + } + + string hash; + try + { + // Compute the digest of the received token and use it + // to retrieve the reference token from the database. + using (var algorithm = SHA256.Create()) + { + hash = Convert.ToBase64String(algorithm.ComputeHash(Base64UrlEncoder.DecodeBytes(value))); + } + } + + // Swallow format-related exceptions to ensure badly formed + // or tampered tokens don't cause an exception at this stage. + catch + { + return null; + } + + // Retrieve the token entry from the database. If it + // cannot be found, assume the token is not valid. + var token = await Tokens.FindByHashAsync(hash, context.RequestAborted); + if (token == null) + { + Logger.LogInformation("The reference token corresponding to the '{Hash}' hashed " + + "identifier cannot be found in the database.", hash); + + return null; + } + + var identifier = await Tokens.GetIdAsync(token, context.RequestAborted); + if (string.IsNullOrEmpty(identifier)) + { + Logger.LogWarning("The identifier associated with the received token cannot be retrieved. " + + "This may indicate that the token entry is corrupted."); + + return null; + } + + // Extract the encrypted payload from the token. If it's null or empty, + // assume the token is not a reference token and consider it as invalid. + var ciphertext = await Tokens.GetCiphertextAsync(token, context.RequestAborted); + if (string.IsNullOrEmpty(ciphertext)) + { + Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be retrieved. " + + "This may indicate that the token is not a reference token.", identifier); + + return null; + } + + var ticket = format.Unprotect(ciphertext); + if (ticket == null) + { + Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be decrypted. " + + "This may indicate that the token entry is corrupted or tampered.", + await Tokens.GetIdAsync(token, context.RequestAborted)); + + return null; + } + + // Restore the token identifier using the unique + // identifier attached with the database entry. + ticket.SetTokenId(identifier); + + // Dynamically set the creation and expiration dates. + ticket.Properties.IssuedUtc = await Tokens.GetCreationDateAsync(token, context.RequestAborted); + ticket.Properties.ExpiresUtc = await Tokens.GetExpirationDateAsync(token, context.RequestAborted); + + // Restore the authorization identifier using the identifier attached with the database entry. + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, + await Tokens.GetAuthorizationIdAsync(token, context.RequestAborted)); + + Logger.LogTrace("The reference token '{Identifier}' was successfully retrieved " + + "from the database and decrypted: {Claims} ; {Properties}.", + identifier, ticket.Principal.Claims, ticket.Properties.Items); + + return ticket; + } + + private async Task RevokeAuthorizationAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context) + { + var identifier = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + var authorization = await Authorizations.FindByIdAsync(identifier, context.RequestAborted); + if (authorization == null) + { + return; + } + + await Authorizations.RevokeAsync(authorization, context.RequestAborted); + + Logger.LogInformation("The authorization '{Identifier}' was automatically revoked.", identifier); + } + + private async Task RevokeTokensAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context) + { + var identifier = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + foreach (var token in await Tokens.FindByAuthorizationIdAsync(identifier, context.RequestAborted)) + { + // Don't overwrite the status of the token used in the token request. + if (string.Equals(ticket.GetTokenId(), await Tokens.GetIdAsync(token, context.RequestAborted))) + { + continue; + } + + await Tokens.RevokeAsync(token, context.RequestAborted); + + Logger.LogInformation("The token '{Identifier}' was automatically revoked.", + await Tokens.GetIdAsync(token, context.RequestAborted)); + } + } + } +} \ No newline at end of file diff --git a/src/OpenIddict/OpenIddictProvider.Serialization.cs b/src/OpenIddict/OpenIddictProvider.Serialization.cs index f2bb4dee..29ca9166 100644 --- a/src/OpenIddict/OpenIddictProvider.Serialization.cs +++ b/src/OpenIddict/OpenIddictProvider.Serialization.cs @@ -4,19 +4,11 @@ * the license and the contributors participating to this project. */ -using System; using System.Diagnostics; -using System.Security.Cryptography; using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; -using OpenIddict.Core; namespace OpenIddict { @@ -116,36 +108,11 @@ namespace OpenIddict public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) { - var options = (OpenIddictOptions) context.Options; - Debug.Assert(context.Request.IsTokenRequest(), "The request should be a token request."); - // When rolling tokens are disabled, extend the expiration date associated with the - // existing token instead of returning a new refresh token with a new expiration date. - if (options.UseSlidingExpiration && !options.UseRollingTokens && context.Request.IsRefreshTokenGrantType()) - { - var identifier = context.Request.GetProperty(OpenIddictConstants.Properties.TokenId); - - var entry = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); - if (entry != null) - { - Logger.LogInformation("The expiration date of the '{Identifier}' token was automatically updated: {Date}.", - identifier, context.Ticket.Properties.ExpiresUtc); - - await Tokens.ExtendAsync(entry, context.Ticket.Properties.ExpiresUtc, context.HttpContext.RequestAborted); - - context.RefreshToken = null; - context.HandleSerialization(); - - return; - } - - // If the refresh token entry could not be - // found in the database, generate a new one. - } - var token = await CreateTokenAsync( - OpenIdConnectConstants.TokenUsages.RefreshToken, context.Ticket, options, + OpenIdConnectConstants.TokenUsages.RefreshToken, + context.Ticket, (OpenIddictOptions) context.Options, context.HttpContext, context.Request, context.DataFormat); // If a reference token was returned by CreateTokenAsync(), @@ -159,258 +126,5 @@ namespace OpenIddict // Otherwise, let the OpenID Connect server middleware // serialize the token using its default internal logic. } - - private async Task CreateTokenAsync( - [NotNull] string type, [NotNull] AuthenticationTicket ticket, - [NotNull] OpenIddictOptions options, [NotNull] HttpContext context, - [NotNull] OpenIdConnectRequest request, - [NotNull] ISecureDataFormat format) - { - Debug.Assert(!(options.DisableTokenRevocation && options.UseReferenceTokens), - "Token revocation cannot be disabled when using reference tokens."); - - Debug.Assert(!(options.DisableTokenRevocation && options.UseRollingTokens), - "Token revocation cannot be disabled when using rolling tokens."); - - Debug.Assert(type != OpenIdConnectConstants.TokenUsages.IdToken, - "Identity tokens shouldn't be stored in the database."); - - if (options.DisableTokenRevocation) - { - return null; - } - - var descriptor = new OpenIddictTokenDescriptor - { - CreationDate = ticket.Properties.IssuedUtc, - ExpirationDate = ticket.Properties.ExpiresUtc, - Status = OpenIddictConstants.Statuses.Valid, - Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject), - Type = type - }; - - string result = null; - - // When reference tokens are enabled or when the token is an authorization code or a - // refresh token, remove the unnecessary properties from the authentication ticket. - if (options.UseReferenceTokens || - (type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || - type == OpenIdConnectConstants.TokenUsages.RefreshToken)) - { - ticket.Properties.IssuedUtc = ticket.Properties.ExpiresUtc = null; - ticket.RemoveProperty(OpenIdConnectConstants.Properties.TokenId); - } - - // If reference tokens are enabled, create a new entry for - // authorization codes, refresh tokens and access tokens. - if (options.UseReferenceTokens) - { - // Note: the data format is automatically replaced at startup time to ensure - // that encrypted tokens stored in the database cannot be considered as - // valid tokens if the developer decides to disable reference tokens support. - descriptor.Ciphertext = format.Protect(ticket); - - // Generate a new crypto-secure random identifier that will be - // substituted to the ciphertext returned by the data format. - var bytes = new byte[256 / 8]; - options.RandomNumberGenerator.GetBytes(bytes); - result = Base64UrlEncoder.Encode(bytes); - - // Compute the digest of the generated identifier and use - // it as the hashed identifier of the reference token. - // Doing that prevents token identifiers stolen from - // the database from being used as valid reference tokens. - using (var algorithm = SHA256.Create()) - { - descriptor.Hash = Convert.ToBase64String(algorithm.ComputeHash(bytes)); - } - } - - // Otherwise, only create a token metadata entry for authorization codes and refresh tokens. - else if (type != OpenIdConnectConstants.TokenUsages.AuthorizationCode && - type != OpenIdConnectConstants.TokenUsages.RefreshToken) - { - return null; - } - - // If the client application is known, associate it with the token. - if (!string.IsNullOrEmpty(request.ClientId)) - { - var application = await Applications.FindByClientIdAsync(request.ClientId, context.RequestAborted); - if (application == null) - { - throw new InvalidOperationException("The client application cannot be retrieved from the database."); - } - - descriptor.ApplicationId = await Applications.GetIdAsync(application, context.RequestAborted); - } - - // If an authorization identifier was specified, bind it to the token. - if (ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId)) - { - descriptor.AuthorizationId = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); - } - - // Otherwise, create an ad hoc authorization if the token is an authorization code. - else if (type == OpenIdConnectConstants.TokenUsages.AuthorizationCode) - { - Debug.Assert(!string.IsNullOrEmpty(descriptor.ApplicationId), "The client identifier shouldn't be null."); - - var authorization = await CreateAuthorizationAsync(descriptor, context, request); - if (authorization != null) - { - descriptor.AuthorizationId = await Authorizations.GetIdAsync(authorization, context.RequestAborted); - - Logger.LogInformation("An ad hoc authorization was automatically created and " + - "associated with the '{ClientId}' application: {Identifier}.", - request.ClientId, descriptor.AuthorizationId); - } - } - - // If a null value was returned by CreateAsync(), return immediately. - var token = await Tokens.CreateAsync(descriptor, context.RequestAborted); - if (token == null) - { - return null; - } - - // Throw an exception if the token identifier can't be resolved. - var identifier = await Tokens.GetIdAsync(token, context.RequestAborted); - if (string.IsNullOrEmpty(identifier)) - { - throw new InvalidOperationException("The unique key associated with a refresh token cannot be null or empty."); - } - - // Restore the token identifier using the unique - // identifier attached with the database entry. - ticket.SetTokenId(identifier); - - // Dynamically set the creation and expiration dates. - ticket.Properties.IssuedUtc = await Tokens.GetCreationDateAsync(token, context.RequestAborted); - ticket.Properties.ExpiresUtc = await Tokens.GetExpirationDateAsync(token, context.RequestAborted); - - ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, descriptor.AuthorizationId); - - if (!string.IsNullOrEmpty(result)) - { - Logger.LogTrace("A new reference token was successfully generated and persisted " + - "in the database: {Token} ; {Claims} ; {Properties}.", - result, ticket.Principal.Claims, ticket.Properties.Items); - } - - return result; - } - - private async Task ReceiveTokenAsync( - [NotNull] string value, [NotNull] OpenIddictOptions options, - [NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request, - [NotNull] ISecureDataFormat format) - { - if (!options.UseReferenceTokens) - { - return null; - } - - string hash; - try - { - // Compute the digest of the received token and use it - // to retrieve the reference token from the database. - using (var algorithm = SHA256.Create()) - { - hash = Convert.ToBase64String(algorithm.ComputeHash(Base64UrlEncoder.DecodeBytes(value))); - } - } - - // Swallow format-related exceptions to ensure badly formed - // or tampered tokens don't cause an exception at this stage. - catch - { - return null; - } - - // Retrieve the token entry from the database. If it - // cannot be found, assume the token is not valid. - var token = await Tokens.FindByHashAsync(hash, context.RequestAborted); - if (token == null) - { - Logger.LogInformation("The reference token corresponding to the '{Hash}' hashed " + - "identifier cannot be found in the database.", hash); - - return null; - } - - var identifier = await Tokens.GetIdAsync(token, context.RequestAborted); - if (string.IsNullOrEmpty(identifier)) - { - Logger.LogWarning("The identifier associated with the received token cannot be retrieved. " + - "This may indicate that the token entry is corrupted."); - - return null; - } - - // Extract the encrypted payload from the token. If it's null or empty, - // assume the token is not a reference token and consider it as invalid. - var ciphertext = await Tokens.GetCiphertextAsync(token, context.RequestAborted); - if (string.IsNullOrEmpty(ciphertext)) - { - Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be retrieved. " + - "This may indicate that the token is not a reference token.", identifier); - - return null; - } - - var ticket = format.Unprotect(ciphertext); - if (ticket == null) - { - Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be decrypted. " + - "This may indicate that the token entry is corrupted or tampered.", - await Tokens.GetIdAsync(token, context.RequestAborted)); - - return null; - } - - // Restore the token identifier using the unique - // identifier attached with the database entry. - ticket.SetTokenId(identifier); - - // Dynamically set the creation and expiration dates. - ticket.Properties.IssuedUtc = await Tokens.GetCreationDateAsync(token, context.RequestAborted); - ticket.Properties.ExpiresUtc = await Tokens.GetExpirationDateAsync(token, context.RequestAborted); - - // If the authorization identifier cannot be found in the ticket properties, - // try to restore it using the identifier associated with the database entry. - if (!ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId)) - { - ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, - await Tokens.GetAuthorizationIdAsync(token, context.RequestAborted)); - } - - Logger.LogTrace("The reference token '{Identifier}' was successfully retrieved " + - "from the database and decrypted: {Claims} ; {Properties}.", - identifier, ticket.Principal.Claims, ticket.Properties.Items); - - return ticket; - } - - private Task CreateAuthorizationAsync( - [NotNull] OpenIddictTokenDescriptor token, - [NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request) - { - var descriptor = new OpenIddictAuthorizationDescriptor - { - ApplicationId = token.ApplicationId, - Status = OpenIddictConstants.Statuses.Valid, - Subject = token.Subject, - Type = OpenIddictConstants.AuthorizationTypes.AdHoc - }; - - foreach (var scope in request.GetScopes()) - { - descriptor.Scopes.Add(scope); - } - - return Authorizations.CreateAsync(descriptor, context.RequestAborted); - } } } \ No newline at end of file diff --git a/src/OpenIddict/OpenIddictProvider.Signin.cs b/src/OpenIddict/OpenIddictProvider.Signin.cs new file mode 100644 index 00000000..ff7fcb8a --- /dev/null +++ b/src/OpenIddict/OpenIddictProvider.Signin.cs @@ -0,0 +1,142 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Diagnostics; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using AspNet.Security.OpenIdConnect.Primitives; +using AspNet.Security.OpenIdConnect.Server; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using OpenIddict.Core; + +namespace OpenIddict +{ + public partial class OpenIddictProvider : OpenIdConnectServerProvider + where TApplication : class where TAuthorization : class where TScope : class where TToken : class + { + public override async Task ProcessSigninResponse([NotNull] ProcessSigninResponseContext context) + { + var options = (OpenIddictOptions) context.Options; + + if (context.Request.IsTokenRequest() && (context.Request.IsAuthorizationCodeGrantType() || + context.Request.IsRefreshTokenGrantType())) + { + // Note: when handling a grant_type=authorization_code or refresh_token request, + // the OpenID Connect server middleware allows creating authentication tickets + // that are completely disconnected from the original code or refresh token ticket. + // This scenario is deliberately not supported in OpenIddict and all the tickets + // must be linked. To ensure the properties are preserved from an authorization code + // or a refresh token to the new ticket, they are manually restored if necessary. + + // Retrieve the original authentication ticket from the request properties. + var ticket = context.Request.GetProperty( + OpenIddictConstants.Properties.AuthenticationTicket); + Debug.Assert(ticket != null, "The authentication ticket shouldn't be null."); + + // If the properties instances of the two authentication tickets differ, + // restore the missing properties in the new authentication ticket. + if (!ReferenceEquals(ticket.Properties, context.Ticket.Properties)) + { + foreach (var property in ticket.Properties.Items) + { + // Don't override the properties that have been + // manually set on the new authentication ticket. + if (context.Ticket.HasProperty(property.Key)) + { + continue; + } + + context.Ticket.AddProperty(property.Key, property.Value); + } + + // Always include the "openid" scope when the developer doesn't explicitly call SetScopes. + // Note: the application is allowed to specify a different "scopes": in this case, + // don't replace the "scopes" property stored in the authentication ticket. + if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OpenId) && !context.Ticket.HasScope()) + { + context.Ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId); + } + + context.IncludeIdentityToken = context.Ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId); + } + + // Always include a refresh token for grant_type=refresh_token requests if + // rolling tokens are enabled and if the offline_access scope was specified. + context.IncludeRefreshToken = context.Request.IsRefreshTokenGrantType() && options.UseRollingTokens && + context.Ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess); + + // If token revocation was explicitly disabled, + // none of the following security routines apply. + if (options.DisableTokenRevocation) + { + return; + } + + // Extract the token identifier from the authentication ticket. + var identifier = context.Ticket.GetTokenId(); + Debug.Assert(!string.IsNullOrEmpty(identifier), + "The authentication ticket should contain a ticket identifier."); + + // If rolling tokens are enabled or if the request is a grant_type=authorization_code request, + // mark the authorization code or the refresh token as redeemed to prevent future reuses. + // See https://tools.ietf.org/html/rfc6749#section-6 for more information. + if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType()) + { + var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); + if (token != null) + { + await Tokens.RedeemAsync(token, context.HttpContext.RequestAborted); + + Logger.LogInformation("The token '{Identifier}' was automatically marked as redeemed.", identifier); + } + } + + // When rolling tokens are enabled, revoke all the previously issued tokens associated + // with the authorization if the request is a grant_type=refresh_token request. + if (options.UseRollingTokens && context.Request.IsRefreshTokenGrantType()) + { + await RevokeTokensAsync(context.Ticket, context.HttpContext); + } + + // When rolling tokens are disabled, extend the expiration date + // of the existing token instead of returning a new refresh token + // with a new expiration date if sliding expiration was not disabled. + else if (options.UseSlidingExpiration && context.Request.IsRefreshTokenGrantType()) + { + var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); + if (token != null) + { + // Compute the new expiration date of the refresh token. + var date = context.Options.SystemClock.UtcNow + + (context.Ticket.GetRefreshTokenLifetime() ?? + context.Options.RefreshTokenLifetime); + + await Tokens.ExtendAsync(token, date, context.HttpContext.RequestAborted); + + Logger.LogInformation("The expiration date of the refresh token '{Identifier}' " + + "was automatically updated: {Date}.", identifier, date); + + context.IncludeRefreshToken = false; + } + + // If the refresh token entry could not be + // found in the database, generate a new one. + } + } + + // If no authorization was explicitly attached to the authentication ticket, + // create an ad hoc authorization if an authorization code or a refresh token + // is going to be returned to the client application as part of the response. + if (!context.Ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId) && + (context.IncludeAuthorizationCode || context.IncludeRefreshToken)) + { + await CreateAuthorizationAsync(context.Ticket, options, context.HttpContext, context.Request); + } + } + } +} \ No newline at end of file diff --git a/test/OpenIddict.Tests/OpenIddictInitializerTests.cs b/test/OpenIddict.Tests/OpenIddictInitializerTests.cs index e86dd3f1..e5c65b60 100644 --- a/test/OpenIddict.Tests/OpenIddictInitializerTests.cs +++ b/test/OpenIddict.Tests/OpenIddictInitializerTests.cs @@ -162,38 +162,14 @@ namespace OpenIddict.Tests } [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenUsingRollingTokensWithTokenRevocationDisabled() + public async Task PostConfigure_ThrowsAnExceptionWhenUsingSlidingExpirationWithoutRollingTokensAndWithTokenRevocationDisabled() { // Arrange var server = CreateAuthorizationServer(builder => { builder.EnableAuthorizationEndpoint("/connect/authorize") .AllowImplicitFlow() - .DisableTokenRevocation() - .UseRollingTokens(); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - Assert.Equal("Rolling tokens cannot be used when disabling token expiration.", exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenUsingRollingTokensWithSlidingExpirationDisabled() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.EnableAuthorizationEndpoint("/connect/authorize") - .AllowImplicitFlow() - .UseRollingTokens() - .Configure(options => options.UseSlidingExpiration = false); + .DisableTokenRevocation(); }); var client = new OpenIdConnectClient(server.CreateClient()); @@ -204,7 +180,8 @@ namespace OpenIddict.Tests return client.GetAsync("/"); }); - Assert.Equal("Rolling tokens cannot be used without enabling sliding expiration.", exception.Message); + Assert.Equal("Sliding expiration must be disabled when turning off " + + "token revocation if rolling tokens are not used.", exception.Message); } [Fact] diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs index c327f00d..9b5b2d16 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs @@ -436,6 +436,7 @@ namespace OpenIddict.Tests builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); builder.DisableTokenRevocation(); + builder.DisableSlidingExpiration(); }); var client = new OpenIdConnectClient(server.CreateClient()); @@ -487,6 +488,7 @@ namespace OpenIddict.Tests builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); builder.DisableTokenRevocation(); + builder.DisableSlidingExpiration(); }); var client = new OpenIdConnectClient(server.CreateClient()); @@ -562,6 +564,63 @@ namespace OpenIddict.Tests Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); } + [Fact] + public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsUnknown() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", 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.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()); + } + [Fact] public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsAlreadyRedeemed() { @@ -629,7 +688,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task HandleTokenRequest_RevokesTokensWhenAuthorizationCodeIsAlreadyRedeemed() + public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsAlreadyRedeemed() { // Arrange var ticket = new AuthenticationTicket( @@ -637,33 +696,23 @@ namespace OpenIddict.Tests new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme); - ticket.SetPresenters("Fabrikam"); - ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); var format = new Mock>(); - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) .Returns(ticket); - var tokens = new[] - { - new OpenIddictToken(), - new OpenIddictToken(), - new OpenIddictToken() - }; + var token = new OpenIddictToken(); var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(tokens[0]); + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); - instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) .ReturnsAsync(true); - - instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(tokens); }); var server = CreateAuthorizationServer(builder => @@ -679,17 +728,9 @@ namespace OpenIddict.Tests .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); - builder.Services.AddSingleton(CreateAuthorizationManager(instance => - { - var authorization = new OpenIddictAuthorization(); - - instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - })); - builder.Services.AddSingleton(manager); - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); + builder.Configure(options => options.RefreshTokenFormat = format.Object); }); var client = new OpenIdConnectClient(server.CreateClient()); @@ -697,25 +738,20 @@ namespace OpenIddict.Tests // Act var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" }); // Assert Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription); + Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), 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()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny()), Times.Once()); } [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsInvalid() + public async Task HandleTokenRequest_RevokesAuthorizationWhenAuthorizationCodeIsAlreadyRedeemed() { // Arrange var ticket = new AuthenticationTicket( @@ -726,24 +762,19 @@ namespace OpenIddict.Tests ticket.SetPresenters("Fabrikam"); ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); var format = new Mock>(); format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) .Returns(ticket); - var token = new OpenIddictToken(); + var authorization = new OpenIddictAuthorization(); - var manager = CreateTokenManager(instance => + var manager = CreateAuthorizationManager(instance => { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(false); + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(authorization); }); var server = CreateAuthorizationServer(builder => @@ -759,6 +790,17 @@ namespace OpenIddict.Tests .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); + builder.Services.AddSingleton(CreateTokenManager(instance => + { + var token = new OpenIddictToken(); + + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(true); + })); + builder.Services.AddSingleton(manager); builder.Configure(options => options.AuthorizationCodeFormat = format.Object); @@ -777,15 +819,14 @@ namespace OpenIddict.Tests // Assert Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); + Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), 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()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.RevokeAsync(authorization, It.IsAny()), Times.Once()); } [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsUnknown() + public async Task HandleTokenRequest_RevokesAuthorizationWhenRefreshTokenIsAlreadyRedeemed() { // Arrange var ticket = new AuthenticationTicket( @@ -793,18 +834,21 @@ namespace OpenIddict.Tests new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme); - ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); var format = new Mock>(); format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) .Returns(ticket); - var manager = CreateTokenManager(instance => + var authorization = new OpenIddictAuthorization(); + + var manager = CreateAuthorizationManager(instance => { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(value: null); + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(authorization); }); var server = CreateAuthorizationServer(builder => @@ -820,6 +864,17 @@ namespace OpenIddict.Tests .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); + builder.Services.AddSingleton(CreateTokenManager(instance => + { + var token = new OpenIddictToken(); + + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(true); + })); + builder.Services.AddSingleton(manager); builder.Configure(options => options.RefreshTokenFormat = format.Object); @@ -836,13 +891,14 @@ namespace OpenIddict.Tests // Assert Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); + Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.RevokeAsync(authorization, It.IsAny()), Times.Once()); } [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsAlreadyRedeemed() + public async Task HandleTokenRequest_RevokesTokensWhenAuthorizationCodeIsAlreadyRedeemed() { // Arrange var ticket = new AuthenticationTicket( @@ -850,23 +906,33 @@ namespace OpenIddict.Tests new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme); - ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetPresenters("Fabrikam"); + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); var format = new Mock>(); - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) .Returns(ticket); - var token = new OpenIddictToken(); + var tokens = new[] + { + new OpenIddictToken(), + new OpenIddictToken(), + new OpenIddictToken() + }; var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(tokens[0]); - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + 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 => @@ -882,9 +948,17 @@ namespace OpenIddict.Tests .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); + builder.Services.AddSingleton(CreateAuthorizationManager(instance => + { + var authorization = new OpenIddictAuthorization(); + + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(authorization); + })); + builder.Services.AddSingleton(manager); - builder.Configure(options => options.RefreshTokenFormat = format.Object); + builder.Configure(options => options.AuthorizationCodeFormat = format.Object); }); var client = new OpenIdConnectClient(server.CreateClient()); @@ -892,20 +966,25 @@ namespace OpenIddict.Tests // Act var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { - GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, + RedirectUri = "http://www.fabrikam.com/path" }); // Assert Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription); + Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny()), Times.Once()); + 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_RequestIsRejectedWhenRefreshTokenIsInvalid() + public async Task HandleTokenRequest_RevokesTokensWhenRefreshTokenIsAlreadyRedeemed() { // Arrange var ticket = new AuthenticationTicket( @@ -913,26 +992,32 @@ namespace OpenIddict.Tests new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme); - ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); var format = new Mock>(); format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) .Returns(ticket); - var token = new OpenIddictToken(); + var tokens = new[] + { + new OpenIddictToken(), + new OpenIddictToken(), + new OpenIddictToken() + }; var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(tokens[0]); - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); + instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) + .ReturnsAsync(true); - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(false); + instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(tokens); }); var server = CreateAuthorizationServer(builder => @@ -948,6 +1033,14 @@ namespace OpenIddict.Tests .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); + builder.Services.AddSingleton(CreateAuthorizationManager(instance => + { + var authorization = new OpenIddictAuthorization(); + + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(authorization); + })); + builder.Services.AddSingleton(manager); builder.Configure(options => options.RefreshTokenFormat = format.Object); @@ -964,21 +1057,21 @@ namespace OpenIddict.Tests // Assert Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); + Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); + 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_AuthorizationCodeIsAutomaticallyRedeemed() + public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsInvalid() { // Arrange - var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), + new ClaimsPrincipal(), new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme); @@ -998,8 +1091,11 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); + .ReturnsAsync(false); }); var server = CreateAuthorizationServer(builder => @@ -1032,19 +1128,20 @@ namespace OpenIddict.Tests }); // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, 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_RefreshTokenIsAutomaticallyRedeemedWhenRollingTokensAreEnabled() + public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsInvalid() { // Arrange - var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), + new ClaimsPrincipal(), new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme); @@ -1067,7 +1164,7 @@ namespace OpenIddict.Tests .ReturnsAsync(false); instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); + .ReturnsAsync(false); }); var server = CreateAuthorizationServer(builder => @@ -1085,8 +1182,6 @@ namespace OpenIddict.Tests builder.Services.AddSingleton(manager); - builder.UseRollingTokens(); - builder.Configure(options => options.RefreshTokenFormat = format.Object); }); @@ -1100,8 +1195,11 @@ namespace OpenIddict.Tests }); // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); } [Theory] diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs index 29a366a6..bbcf8c67 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs @@ -272,6 +272,7 @@ namespace OpenIddict.Tests builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); builder.DisableTokenRevocation(); + builder.DisableSlidingExpiration(); }); var client = new OpenIdConnectClient(server.CreateClient()); @@ -328,6 +329,7 @@ namespace OpenIddict.Tests builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); builder.DisableTokenRevocation(); + builder.DisableSlidingExpiration(); }); var client = new OpenIdConnectClient(server.CreateClient()); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs index 5282db38..d44e48cf 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs @@ -5,10 +5,13 @@ */ using System; +using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Client; +using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Primitives; +using AspNet.Security.OpenIdConnect.Server; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -21,6 +24,1191 @@ namespace OpenIddict.Tests { public partial class OpenIddictProviderTests { + [Fact] + public async Task DeserializeAccessToken_ReturnsNullForMalformedReferenceToken() + { + // Arrange + var format = new Mock>(); + var manager = CreateTokenManager(); + + 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.UseReferenceTokens(); + + builder.Configure(options => options.AccessTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Never()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Never()); + } + + [Fact] + public async Task DeserializeAccessToken_AccessTokenIsNotRetrievedFromDatabaseWhenReferenceTokensAreDisabled() + { + // 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(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.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", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken + }); + + // Assert + Assert.True((bool) response[OpenIdConnectConstants.Claims.Active]); + + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByHashAsync(It.IsAny(), It.IsAny()), Times.Never()); + } + + [Fact] + public async Task DeserializeAccessToken_ReturnsNullForMissingTokenIdentifier() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, 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.UseReferenceTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); + } + + [Fact] + public async Task DeserializeAccessToken_ReturnsNullForMissingTokenCiphertext() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.GetCiphertextAsync(token, 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.UseReferenceTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); + } + + [Fact] + public async Task DeserializeAccessToken_ReturnsNullForInvalidTokenCiphertext() + { + // Arrange + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(value: null); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.GetCiphertextAsync(token, It.IsAny())) + .ReturnsAsync("2YotnFZFEjr1zCsicMWpAA"); + }); + + 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.UseReferenceTokens(); + + builder.Configure(options => options.AccessTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); + } + + [Fact] + public async Task DeserializeAccessToken_ReturnsExpectedToken() + { + // 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.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.GetCiphertextAsync(token, It.IsAny())) + .ReturnsAsync("2YotnFZFEjr1zCsicMWpAA"); + + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny())) + .ReturnsAsync(new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero)); + + instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) + .ReturnsAsync(new DateTimeOffset(2017, 01, 10, 00, 00, 00, TimeSpan.Zero)); + }); + + 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.UseReferenceTokens(); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + + options.AccessTokenFormat = format.Object; + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken + }); + + // Assert + Assert.True((bool) response[OpenIdConnectConstants.Claims.Active]); + Assert.Equal("3E228451-1555-46F7-A471-951EFBA23A56", response[OpenIdConnectConstants.Claims.JwtId]); + Assert.Equal(1483228800, (long) response[OpenIdConnectConstants.Claims.IssuedAt]); + Assert.Equal(1484006400, (long) response[OpenIdConnectConstants.Claims.ExpiresAt]); + + Mock.Get(manager).Verify(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); + } + + [Fact] + public async Task DeserializeAuthorizationCode_ReturnsNullForMalformedReferenceToken() + { + // Arrange + var format = new Mock>(); + var manager = CreateTokenManager(); + + 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.UseReferenceTokens(); + + 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", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AuthorizationCode + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Never()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Never()); + } + + [Fact] + public async Task DeserializeAuthorizationCode_AuthorizationCodeIsNotRetrievedFromDatabaseWhenReferenceTokensAreDisabled() + { + // 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(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.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", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AuthorizationCode + }); + + // Assert + Assert.True((bool) response[OpenIdConnectConstants.Claims.Active]); + + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByHashAsync(It.IsAny(), It.IsAny()), Times.Never()); + } + + [Fact] + public async Task DeserializeAuthorizationCode_ReturnsNullForMissingTokenIdentifier() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, 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.UseReferenceTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AuthorizationCode + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); + } + + [Fact] + public async Task DeserializeAuthorizationCode_ReturnsNullForMissingTokenCiphertext() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.GetCiphertextAsync(token, 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.UseReferenceTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AuthorizationCode + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); + } + + [Fact] + public async Task DeserializeAuthorizationCode_ReturnsNullForInvalidTokenCiphertext() + { + // Arrange + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(value: null); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.GetCiphertextAsync(token, It.IsAny())) + .ReturnsAsync("2YotnFZFEjr1zCsicMWpAA"); + }); + + 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.UseReferenceTokens(); + + 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 = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AuthorizationCode + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); + } + + [Fact] + public async Task DeserializeAuthorizationCode_ReturnsExpectedToken() + { + // 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.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.GetCiphertextAsync(token, It.IsAny())) + .ReturnsAsync("2YotnFZFEjr1zCsicMWpAA"); + + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny())) + .ReturnsAsync(new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero)); + + instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) + .ReturnsAsync(new DateTimeOffset(2017, 01, 10, 00, 00, 00, TimeSpan.Zero)); + }); + + 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.UseReferenceTokens(); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + + options.AuthorizationCodeFormat = format.Object; + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AuthorizationCode + }); + + // Assert + Assert.True((bool) response[OpenIdConnectConstants.Claims.Active]); + Assert.Equal("3E228451-1555-46F7-A471-951EFBA23A56", response[OpenIdConnectConstants.Claims.JwtId]); + Assert.Equal(1483228800, (long) response[OpenIdConnectConstants.Claims.IssuedAt]); + Assert.Equal(1484006400, (long) response[OpenIdConnectConstants.Claims.ExpiresAt]); + + Mock.Get(manager).Verify(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); + } + + [Fact] + public async Task DeserializeRefreshToken_ReturnsNullForMalformedReferenceToken() + { + // Arrange + var format = new Mock>(); + var manager = CreateTokenManager(); + + 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.UseReferenceTokens(); + + 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", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Never()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Never()); + } + + [Fact] + public async Task DeserializeRefreshToken_RefreshTokenIsNotRetrievedFromDatabaseWhenReferenceTokensAreDisabled() + { + // 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(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.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", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken + }); + + // Assert + Assert.True((bool) response[OpenIdConnectConstants.Claims.Active]); + + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByHashAsync(It.IsAny(), It.IsAny()), Times.Never()); + } + + [Fact] + public async Task DeserializeRefreshToken_ReturnsNullForMissingTokenIdentifier() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, 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.UseReferenceTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); + } + + [Fact] + public async Task DeserializeRefreshToken_ReturnsNullForMissingTokenCiphertext() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.GetCiphertextAsync(token, 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.UseReferenceTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); + } + + [Fact] + public async Task DeserializeRefreshToken_ReturnsNullForInvalidTokenCiphertext() + { + // Arrange + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(value: null); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.GetCiphertextAsync(token, It.IsAny())) + .ReturnsAsync("2YotnFZFEjr1zCsicMWpAA"); + }); + + 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.UseReferenceTokens(); + + 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 = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); + } + + [Fact] + public async Task DeserializeRefreshToken_ReturnsExpectedToken() + { + // 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.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.GetCiphertextAsync(token, It.IsAny())) + .ReturnsAsync("2YotnFZFEjr1zCsicMWpAA"); + + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny())) + .ReturnsAsync(new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero)); + + instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) + .ReturnsAsync(new DateTimeOffset(2017, 01, 10, 00, 00, 00, TimeSpan.Zero)); + }); + + 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.UseReferenceTokens(); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + + options.RefreshTokenFormat = format.Object; + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken + }); + + // Assert + Assert.True((bool) response[OpenIdConnectConstants.Claims.Active]); + Assert.Equal("3E228451-1555-46F7-A471-951EFBA23A56", response[OpenIdConnectConstants.Claims.JwtId]); + Assert.Equal(1483228800, (long) response[OpenIdConnectConstants.Claims.IssuedAt]); + Assert.Equal(1484006400, (long) response[OpenIdConnectConstants.Claims.ExpiresAt]); + + Mock.Get(manager).Verify(mock => mock.FindByHashAsync("jTwUlKz7IT5tRnmMnxYW26OdS28cPG2rM04zQr0ez70=", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); + } + [Fact] public async Task SerializeAccessToken_AccessTokenIsNotPersistedWhenReferenceTokensAreDisabled() { @@ -30,8 +1218,6 @@ namespace OpenIddict.Tests var server = CreateAuthorizationServer(builder => { builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); }); var client = new OpenIdConnectClient(server.CreateClient()); @@ -249,6 +1435,7 @@ namespace OpenIddict.Tests builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); builder.DisableTokenRevocation(); + builder.DisableSlidingExpiration(); }); var client = new OpenIdConnectClient(server.CreateClient()); @@ -538,99 +1725,53 @@ namespace OpenIddict.Tests } [Fact] - public async Task SerializeAuthorizationCode_AdHocAuthorizationIsAutomaticallyCreated() + public async Task SerializeRefreshToken_ExpirationDateIsFixedWhenSlidingExpirationIsDisabled() { // 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.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(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); - })); - - builder.Services.AddSingleton(manager); - }); + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); - var client = new OpenIdConnectClient(server.CreateClient()); + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIdConnectConstants.ResponseTypes.Code, - }); + var format = new Mock>(); - // Assert - Assert.NotNull(response.Code); + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.Is(descriptor => - descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && - descriptor.Subject == "Bob le Magnifique" && - descriptor.Type == OpenIddictConstants.AuthorizationTypes.AdHoc), - It.IsAny()), Times.Once()); - } + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); - [Fact] - public async Task SerializeRefreshToken_ExtendsLifetimeWhenRollingTokensAreDisabled() - { - // Arrange - var token = new OpenIddictToken - { - CreationDate = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero), - ExpirationDate = new DateTimeOffset(2017, 01, 10, 00, 00, 00, TimeSpan.Zero) - }; + var token = new OpenIddictToken(); var manager = CreateTokenManager(instance => { - instance.Setup(mock => mock.FindByHashAsync("d80c119138b3aaeefce94093032c0204c547dc27cc5fe97f32933becd48b7bf5", It.IsAny())) + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) .ReturnsAsync(token); - 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(true); }); var server = CreateAuthorizationServer(builder => { builder.Services.AddSingleton(manager); - builder.UseReferenceTokens(); + builder.DisableSlidingExpiration(); + builder.UseRollingTokens(); builder.Configure(options => { options.SystemClock = Mock.Of(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); options.RefreshTokenLifetime = TimeSpan.FromDays(10); + options.RefreshTokenFormat = format.Object; }); }); @@ -640,14 +1781,14 @@ namespace OpenIddict.Tests var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, - RefreshToken = "3E228451-1555-46F7-A471-951EFBA23A56" + RefreshToken = "8xLOxBtZp8" }); // Assert - Assert.Null(response.RefreshToken); + Assert.NotNull(response.RefreshToken); Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, - new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), + new DateTimeOffset(2017, 01, 10, 00, 00, 00, TimeSpan.Zero), It.IsAny()), Times.Never()); } @@ -664,6 +1805,7 @@ namespace OpenIddict.Tests builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); builder.DisableTokenRevocation(); + builder.DisableSlidingExpiration(); }); var client = new OpenIdConnectClient(server.CreateClient()); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs new file mode 100644 index 00000000..99871277 --- /dev/null +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs @@ -0,0 +1,721 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Client; +using AspNet.Security.OpenIdConnect.Extensions; +using AspNet.Security.OpenIdConnect.Primitives; +using AspNet.Security.OpenIdConnect.Server; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using OpenIddict.Core; +using OpenIddict.Models; +using Xunit; + +namespace OpenIddict.Tests +{ + public partial class OpenIddictProviderTests + { + [Fact] + public async Task ProcessSigninResponse_AuthenticationPropertiesAreAutomaticallyRestored() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + ticket.SetProperty("custom_property_in_original_ticket", "original_value"); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .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 => + { + builder.Services.AddSingleton(manager); + + builder.UseRollingTokens(); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8", + ["do-not-flow-original-properties"] = true + }); + + // Assert + Assert.NotNull(response.IdToken); + Assert.NotNull(response.RefreshToken); + + format.Verify(mock => mock.Protect( + It.Is(value => + value.Properties.Items["custom_property_in_original_ticket"] == "original_value" && + value.Properties.Items["custom_property_in_new_ticket"] == "new_value"))); + } + [Fact] + public async Task ProcessSigninResponse_RefreshTokenIsAlwaysIssuedWhenRollingTokensAreEnabled() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .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 => + { + builder.Services.AddSingleton(manager); + + builder.UseRollingTokens(); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.NotNull(response.RefreshToken); + } + + [Fact] + public async Task ProcessSigninResponse_RefreshTokenIsNotIssuedWhenRollingTokensAreDisabled() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .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 => + { + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + } + + [Fact] + public async Task ProcessSigninResponse_AuthorizationCodeIsAutomaticallyRedeemed() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetPresenters("Fabrikam"); + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); + + var format = new Mock>(); + + 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.IsValidAsync(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 + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Exactly(2)); + Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSigninResponse_RefreshTokenIsAutomaticallyRedeemedWhenRollingTokensAreEnabled() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + 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.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.UseRollingTokens(); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); + Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSigninResponse_RefreshTokenIsNotRedeemedWhenRollingTokensAreDisabled() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + 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.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(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(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); + Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Never()); + } + + [Fact] + public async Task ProcessSigninResponse_PreviousTokensAreAutomaticallyRevokedWhenRollingTokensAreEnabled() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var tokens = new[] + { + new OpenIddictToken(), + new OpenIddictToken(), + new OpenIddictToken() + }; + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(tokens[0]); + + instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(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(manager); + + builder.UseRollingTokens(); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); + 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 ProcessSigninResponse_PreviousTokensAreNotRevokedWhenRollingTokensAreDisabled() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var tokens = new[] + { + new OpenIddictToken(), + new OpenIddictToken(), + new OpenIddictToken() + }; + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(tokens[0]); + + instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(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(manager); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); + Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Never()); + Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny()), Times.Never()); + } + + [Fact] + public async Task ProcessSigninResponse_ExtendsLifetimeWhenRollingTokensAreDisabledAndSlidingExpirationEnabled() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .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 => + { + builder.Services.AddSingleton(manager); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + options.RefreshTokenLifetime = TimeSpan.FromDays(10); + options.RefreshTokenFormat = format.Object; + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, + new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSigninResponse_DoesNotExtendLifetimeWhenSlidingExpirationIsDisabled() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .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 => + { + builder.Services.AddSingleton(manager); + + builder.DisableSlidingExpiration(); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + options.RefreshTokenLifetime = TimeSpan.FromDays(10); + options.RefreshTokenFormat = format.Object; + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, + new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), + It.IsAny()), Times.Never()); + } + + [Fact] + public async Task ProcessSigninResponse_AdHocAuthorizationIsAutomaticallyCreated() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateAuthorizationManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) + .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.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(It.IsAny(), 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( + It.Is(descriptor => + descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && + descriptor.Subject == "Bob le Magnifique" && + descriptor.Type == OpenIddictConstants.AuthorizationTypes.AdHoc), + It.IsAny()), Times.Once()); + } + } +} diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.cs index 136fe8fd..6cd6df63 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.cs @@ -158,6 +158,14 @@ namespace OpenIddict.Tests ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70"); } + if (request.HasParameter("do-not-flow-original-properties")) + { + var properties = new AuthenticationProperties(); + properties.SetProperty("custom_property_in_new_ticket", "new_value"); + + return context.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, properties); + } + return context.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties); } @@ -184,46 +192,50 @@ namespace OpenIddict.Tests return new TestServer(builder); } - private static OpenIddictApplicationManager CreateApplicationManager(Action>> setup = null) + private static OpenIddictApplicationManager CreateApplicationManager( + Action>> configuration = null) { var manager = new Mock>( Mock.Of>(), Mock.Of>>()); - setup?.Invoke(manager); + configuration?.Invoke(manager); return manager.Object; } - private static OpenIddictAuthorizationManager CreateAuthorizationManager(Action>> setup = null) + private static OpenIddictAuthorizationManager CreateAuthorizationManager( + Action>> configuration = null) { var manager = new Mock>( Mock.Of>(), Mock.Of>>()); - setup?.Invoke(manager); + configuration?.Invoke(manager); return manager.Object; } - private static OpenIddictScopeManager CreateScopeManager(Action>> setup = null) + private static OpenIddictScopeManager CreateScopeManager( + Action>> configuration = null) { var manager = new Mock>( Mock.Of>(), Mock.Of>>()); - setup?.Invoke(manager); + configuration?.Invoke(manager); return manager.Object; } - private static OpenIddictTokenManager CreateTokenManager(Action>> setup = null) + private static OpenIddictTokenManager CreateTokenManager( + Action>> configuration = null) { var manager = new Mock>( Mock.Of>(), Mock.Of>>()); - setup?.Invoke(manager); + configuration?.Invoke(manager); return manager.Object; }