16 changed files with 2774 additions and 623 deletions
@ -0,0 +1,347 @@ |
|||||
|
/* |
||||
|
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
* See https://github.com/openiddict/openiddict-core for more information concerning
|
||||
|
* the license and the contributors participating to this project. |
||||
|
*/ |
||||
|
|
||||
|
using System; |
||||
|
using System.Diagnostics; |
||||
|
using System.Security.Cryptography; |
||||
|
using System.Threading.Tasks; |
||||
|
using AspNet.Security.OpenIdConnect.Extensions; |
||||
|
using AspNet.Security.OpenIdConnect.Primitives; |
||||
|
using AspNet.Security.OpenIdConnect.Server; |
||||
|
using JetBrains.Annotations; |
||||
|
using Microsoft.AspNetCore.Authentication; |
||||
|
using Microsoft.AspNetCore.Http; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.IdentityModel.Tokens; |
||||
|
using OpenIddict.Core; |
||||
|
|
||||
|
namespace OpenIddict |
||||
|
{ |
||||
|
public partial class OpenIddictProvider<TApplication, TAuthorization, TScope, TToken> : OpenIdConnectServerProvider |
||||
|
where TApplication : class where TAuthorization : class where TScope : class where TToken : class |
||||
|
{ |
||||
|
private async Task CreateAuthorizationAsync( |
||||
|
[NotNull] AuthenticationTicket ticket, [NotNull] OpenIddictOptions options, |
||||
|
[NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request) |
||||
|
{ |
||||
|
if (options.DisableTokenRevocation) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var descriptor = new OpenIddictAuthorizationDescriptor |
||||
|
{ |
||||
|
Status = OpenIddictConstants.Statuses.Valid, |
||||
|
Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject), |
||||
|
Type = OpenIddictConstants.AuthorizationTypes.AdHoc |
||||
|
}; |
||||
|
|
||||
|
foreach (var scope in request.GetScopes()) |
||||
|
{ |
||||
|
descriptor.Scopes.Add(scope); |
||||
|
} |
||||
|
|
||||
|
// If the client application is known, bind it to the authorization.
|
||||
|
if (!string.IsNullOrEmpty(request.ClientId)) |
||||
|
{ |
||||
|
var application = await Applications.FindByClientIdAsync(request.ClientId, context.RequestAborted); |
||||
|
if (application == null) |
||||
|
{ |
||||
|
throw new InvalidOperationException("The client application cannot be retrieved from the database."); |
||||
|
} |
||||
|
|
||||
|
descriptor.ApplicationId = await Applications.GetIdAsync(application, context.RequestAborted); |
||||
|
} |
||||
|
|
||||
|
var authorization = await Authorizations.CreateAsync(descriptor, context.RequestAborted); |
||||
|
if (authorization != null) |
||||
|
{ |
||||
|
var identifier = await Authorizations.GetIdAsync(authorization, context.RequestAborted); |
||||
|
|
||||
|
if (string.IsNullOrEmpty(request.ClientId)) |
||||
|
{ |
||||
|
Logger.LogInformation("An ad hoc authorization was automatically created and " + |
||||
|
"associated with an unknown application: {Identifier}.", identifier); |
||||
|
} |
||||
|
|
||||
|
else |
||||
|
{ |
||||
|
Logger.LogInformation("An ad hoc authorization was automatically created and " + |
||||
|
"associated with the '{ClientId}' application: {Identifier}.", |
||||
|
request.ClientId, identifier); |
||||
|
} |
||||
|
|
||||
|
// Attach the unique identifier of the ad hoc authorization to the authentication ticket
|
||||
|
// so that it is attached to all the derived tokens, allowing batched revocations support.
|
||||
|
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, identifier); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task<string> CreateTokenAsync( |
||||
|
[NotNull] string type, [NotNull] AuthenticationTicket ticket, |
||||
|
[NotNull] OpenIddictOptions options, [NotNull] HttpContext context, |
||||
|
[NotNull] OpenIdConnectRequest request, |
||||
|
[NotNull] ISecureDataFormat<AuthenticationTicket> format) |
||||
|
{ |
||||
|
Debug.Assert(!(options.DisableTokenRevocation && options.UseReferenceTokens), |
||||
|
"Token revocation cannot be disabled when using reference tokens."); |
||||
|
|
||||
|
Debug.Assert(type == OpenIdConnectConstants.TokenUsages.AccessToken || |
||||
|
type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || |
||||
|
type == OpenIdConnectConstants.TokenUsages.RefreshToken, |
||||
|
"Only authorization codes, access and refresh tokens should be created using this method."); |
||||
|
|
||||
|
// When sliding expiration is disabled, the expiration date of generated refresh tokens is fixed
|
||||
|
// and must exactly match the expiration date of the refresh token used in the token request.
|
||||
|
if (request.IsTokenRequest() && request.IsRefreshTokenGrantType() && |
||||
|
!options.UseSlidingExpiration && type == OpenIdConnectConstants.TokenUsages.RefreshToken) |
||||
|
{ |
||||
|
var properties = request.GetProperty<AuthenticationTicket>( |
||||
|
OpenIddictConstants.Properties.AuthenticationTicket)?.Properties; |
||||
|
Debug.Assert(properties != null, "The authentication properties shouldn't be null."); |
||||
|
|
||||
|
ticket.Properties.ExpiresUtc = properties.ExpiresUtc; |
||||
|
} |
||||
|
|
||||
|
if (options.DisableTokenRevocation) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var descriptor = new OpenIddictTokenDescriptor |
||||
|
{ |
||||
|
AuthorizationId = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId), |
||||
|
CreationDate = ticket.Properties.IssuedUtc, |
||||
|
ExpirationDate = ticket.Properties.ExpiresUtc, |
||||
|
Status = OpenIddictConstants.Statuses.Valid, |
||||
|
Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject), |
||||
|
Type = type |
||||
|
}; |
||||
|
|
||||
|
string result = null; |
||||
|
|
||||
|
// When reference tokens are enabled or when the token is an authorization code or a
|
||||
|
// refresh token, remove the unnecessary properties from the authentication ticket.
|
||||
|
if (options.UseReferenceTokens || |
||||
|
(type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || |
||||
|
type == OpenIdConnectConstants.TokenUsages.RefreshToken)) |
||||
|
{ |
||||
|
ticket.Properties.IssuedUtc = ticket.Properties.ExpiresUtc = null; |
||||
|
ticket.RemoveProperty(OpenIddictConstants.Properties.AuthorizationId) |
||||
|
.RemoveProperty(OpenIdConnectConstants.Properties.TokenId); |
||||
|
} |
||||
|
|
||||
|
// If reference tokens are enabled, create a new entry for
|
||||
|
// authorization codes, refresh tokens and access tokens.
|
||||
|
if (options.UseReferenceTokens) |
||||
|
{ |
||||
|
// Note: the data format is automatically replaced at startup time to ensure
|
||||
|
// that encrypted tokens stored in the database cannot be considered as
|
||||
|
// valid tokens if the developer decides to disable reference tokens support.
|
||||
|
descriptor.Ciphertext = format.Protect(ticket); |
||||
|
|
||||
|
// Generate a new crypto-secure random identifier that will be
|
||||
|
// substituted to the ciphertext returned by the data format.
|
||||
|
var bytes = new byte[256 / 8]; |
||||
|
options.RandomNumberGenerator.GetBytes(bytes); |
||||
|
result = Base64UrlEncoder.Encode(bytes); |
||||
|
|
||||
|
// Compute the digest of the generated identifier and use
|
||||
|
// it as the hashed identifier of the reference token.
|
||||
|
// Doing that prevents token identifiers stolen from
|
||||
|
// the database from being used as valid reference tokens.
|
||||
|
using (var algorithm = SHA256.Create()) |
||||
|
{ |
||||
|
descriptor.Hash = Convert.ToBase64String(algorithm.ComputeHash(bytes)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Otherwise, only create a token metadata entry for authorization codes and refresh tokens.
|
||||
|
else if (type != OpenIdConnectConstants.TokenUsages.AuthorizationCode && |
||||
|
type != OpenIdConnectConstants.TokenUsages.RefreshToken) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// If the client application is known, associate it with the token.
|
||||
|
if (!string.IsNullOrEmpty(request.ClientId)) |
||||
|
{ |
||||
|
var application = await Applications.FindByClientIdAsync(request.ClientId, context.RequestAborted); |
||||
|
if (application == null) |
||||
|
{ |
||||
|
throw new InvalidOperationException("The client application cannot be retrieved from the database."); |
||||
|
} |
||||
|
|
||||
|
descriptor.ApplicationId = await Applications.GetIdAsync(application, context.RequestAborted); |
||||
|
} |
||||
|
|
||||
|
// If a null value was returned by CreateAsync(), return immediately.
|
||||
|
var token = await Tokens.CreateAsync(descriptor, context.RequestAborted); |
||||
|
if (token == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// Throw an exception if the token identifier can't be resolved.
|
||||
|
var identifier = await Tokens.GetIdAsync(token, context.RequestAborted); |
||||
|
if (string.IsNullOrEmpty(identifier)) |
||||
|
{ |
||||
|
throw new InvalidOperationException("The unique key associated with a refresh token cannot be null or empty."); |
||||
|
} |
||||
|
|
||||
|
// Restore the token identifier using the unique
|
||||
|
// identifier attached with the database entry.
|
||||
|
ticket.SetTokenId(identifier); |
||||
|
|
||||
|
// Dynamically set the creation and expiration dates.
|
||||
|
ticket.Properties.IssuedUtc = await Tokens.GetCreationDateAsync(token, context.RequestAborted); |
||||
|
ticket.Properties.ExpiresUtc = await Tokens.GetExpirationDateAsync(token, context.RequestAborted); |
||||
|
|
||||
|
// Restore the authorization identifier using the identifier attached with the database entry.
|
||||
|
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, |
||||
|
await Tokens.GetAuthorizationIdAsync(token, context.RequestAborted)); |
||||
|
|
||||
|
if (!string.IsNullOrEmpty(result)) |
||||
|
{ |
||||
|
Logger.LogTrace("A new reference token was successfully generated and persisted " + |
||||
|
"in the database: {Token} ; {Claims} ; {Properties}.", |
||||
|
result, ticket.Principal.Claims, ticket.Properties.Items); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private async Task<AuthenticationTicket> ReceiveTokenAsync( |
||||
|
[NotNull] string value, [NotNull] OpenIddictOptions options, |
||||
|
[NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request, |
||||
|
[NotNull] ISecureDataFormat<AuthenticationTicket> format) |
||||
|
{ |
||||
|
if (!options.UseReferenceTokens) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
string hash; |
||||
|
try |
||||
|
{ |
||||
|
// Compute the digest of the received token and use it
|
||||
|
// to retrieve the reference token from the database.
|
||||
|
using (var algorithm = SHA256.Create()) |
||||
|
{ |
||||
|
hash = Convert.ToBase64String(algorithm.ComputeHash(Base64UrlEncoder.DecodeBytes(value))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Swallow format-related exceptions to ensure badly formed
|
||||
|
// or tampered tokens don't cause an exception at this stage.
|
||||
|
catch |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// Retrieve the token entry from the database. If it
|
||||
|
// cannot be found, assume the token is not valid.
|
||||
|
var token = await Tokens.FindByHashAsync(hash, context.RequestAborted); |
||||
|
if (token == null) |
||||
|
{ |
||||
|
Logger.LogInformation("The reference token corresponding to the '{Hash}' hashed " + |
||||
|
"identifier cannot be found in the database.", hash); |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var identifier = await Tokens.GetIdAsync(token, context.RequestAborted); |
||||
|
if (string.IsNullOrEmpty(identifier)) |
||||
|
{ |
||||
|
Logger.LogWarning("The identifier associated with the received token cannot be retrieved. " + |
||||
|
"This may indicate that the token entry is corrupted."); |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// Extract the encrypted payload from the token. If it's null or empty,
|
||||
|
// assume the token is not a reference token and consider it as invalid.
|
||||
|
var ciphertext = await Tokens.GetCiphertextAsync(token, context.RequestAborted); |
||||
|
if (string.IsNullOrEmpty(ciphertext)) |
||||
|
{ |
||||
|
Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be retrieved. " + |
||||
|
"This may indicate that the token is not a reference token.", identifier); |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var ticket = format.Unprotect(ciphertext); |
||||
|
if (ticket == null) |
||||
|
{ |
||||
|
Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be decrypted. " + |
||||
|
"This may indicate that the token entry is corrupted or tampered.", |
||||
|
await Tokens.GetIdAsync(token, context.RequestAborted)); |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// Restore the token identifier using the unique
|
||||
|
// identifier attached with the database entry.
|
||||
|
ticket.SetTokenId(identifier); |
||||
|
|
||||
|
// Dynamically set the creation and expiration dates.
|
||||
|
ticket.Properties.IssuedUtc = await Tokens.GetCreationDateAsync(token, context.RequestAborted); |
||||
|
ticket.Properties.ExpiresUtc = await Tokens.GetExpirationDateAsync(token, context.RequestAborted); |
||||
|
|
||||
|
// Restore the authorization identifier using the identifier attached with the database entry.
|
||||
|
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, |
||||
|
await Tokens.GetAuthorizationIdAsync(token, context.RequestAborted)); |
||||
|
|
||||
|
Logger.LogTrace("The reference token '{Identifier}' was successfully retrieved " + |
||||
|
"from the database and decrypted: {Claims} ; {Properties}.", |
||||
|
identifier, ticket.Principal.Claims, ticket.Properties.Items); |
||||
|
|
||||
|
return ticket; |
||||
|
} |
||||
|
|
||||
|
private async Task RevokeAuthorizationAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context) |
||||
|
{ |
||||
|
var identifier = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); |
||||
|
if (string.IsNullOrEmpty(identifier)) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var authorization = await Authorizations.FindByIdAsync(identifier, context.RequestAborted); |
||||
|
if (authorization == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await Authorizations.RevokeAsync(authorization, context.RequestAborted); |
||||
|
|
||||
|
Logger.LogInformation("The authorization '{Identifier}' was automatically revoked.", identifier); |
||||
|
} |
||||
|
|
||||
|
private async Task RevokeTokensAsync([NotNull] AuthenticationTicket ticket, [NotNull] HttpContext context) |
||||
|
{ |
||||
|
var identifier = ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); |
||||
|
if (string.IsNullOrEmpty(identifier)) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
foreach (var token in await Tokens.FindByAuthorizationIdAsync(identifier, context.RequestAborted)) |
||||
|
{ |
||||
|
// Don't overwrite the status of the token used in the token request.
|
||||
|
if (string.Equals(ticket.GetTokenId(), await Tokens.GetIdAsync(token, context.RequestAborted))) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
await Tokens.RevokeAsync(token, context.RequestAborted); |
||||
|
|
||||
|
Logger.LogInformation("The token '{Identifier}' was automatically revoked.", |
||||
|
await Tokens.GetIdAsync(token, context.RequestAborted)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,142 @@ |
|||||
|
/* |
||||
|
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
* See https://github.com/openiddict/openiddict-core for more information concerning
|
||||
|
* the license and the contributors participating to this project. |
||||
|
*/ |
||||
|
|
||||
|
using System.Diagnostics; |
||||
|
using System.Threading.Tasks; |
||||
|
using AspNet.Security.OpenIdConnect.Extensions; |
||||
|
using AspNet.Security.OpenIdConnect.Primitives; |
||||
|
using AspNet.Security.OpenIdConnect.Server; |
||||
|
using JetBrains.Annotations; |
||||
|
using Microsoft.AspNetCore.Authentication; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using OpenIddict.Core; |
||||
|
|
||||
|
namespace OpenIddict |
||||
|
{ |
||||
|
public partial class OpenIddictProvider<TApplication, TAuthorization, TScope, TToken> : OpenIdConnectServerProvider |
||||
|
where TApplication : class where TAuthorization : class where TScope : class where TToken : class |
||||
|
{ |
||||
|
public override async Task ProcessSigninResponse([NotNull] ProcessSigninResponseContext context) |
||||
|
{ |
||||
|
var options = (OpenIddictOptions) context.Options; |
||||
|
|
||||
|
if (context.Request.IsTokenRequest() && (context.Request.IsAuthorizationCodeGrantType() || |
||||
|
context.Request.IsRefreshTokenGrantType())) |
||||
|
{ |
||||
|
// Note: when handling a grant_type=authorization_code or refresh_token request,
|
||||
|
// the OpenID Connect server middleware allows creating authentication tickets
|
||||
|
// that are completely disconnected from the original code or refresh token ticket.
|
||||
|
// This scenario is deliberately not supported in OpenIddict and all the tickets
|
||||
|
// must be linked. To ensure the properties are preserved from an authorization code
|
||||
|
// or a refresh token to the new ticket, they are manually restored if necessary.
|
||||
|
|
||||
|
// Retrieve the original authentication ticket from the request properties.
|
||||
|
var ticket = context.Request.GetProperty<AuthenticationTicket>( |
||||
|
OpenIddictConstants.Properties.AuthenticationTicket); |
||||
|
Debug.Assert(ticket != null, "The authentication ticket shouldn't be null."); |
||||
|
|
||||
|
// If the properties instances of the two authentication tickets differ,
|
||||
|
// restore the missing properties in the new authentication ticket.
|
||||
|
if (!ReferenceEquals(ticket.Properties, context.Ticket.Properties)) |
||||
|
{ |
||||
|
foreach (var property in ticket.Properties.Items) |
||||
|
{ |
||||
|
// Don't override the properties that have been
|
||||
|
// manually set on the new authentication ticket.
|
||||
|
if (context.Ticket.HasProperty(property.Key)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
context.Ticket.AddProperty(property.Key, property.Value); |
||||
|
} |
||||
|
|
||||
|
// Always include the "openid" scope when the developer doesn't explicitly call SetScopes.
|
||||
|
// Note: the application is allowed to specify a different "scopes": in this case,
|
||||
|
// don't replace the "scopes" property stored in the authentication ticket.
|
||||
|
if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OpenId) && !context.Ticket.HasScope()) |
||||
|
{ |
||||
|
context.Ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId); |
||||
|
} |
||||
|
|
||||
|
context.IncludeIdentityToken = context.Ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId); |
||||
|
} |
||||
|
|
||||
|
// Always include a refresh token for grant_type=refresh_token requests if
|
||||
|
// rolling tokens are enabled and if the offline_access scope was specified.
|
||||
|
context.IncludeRefreshToken = context.Request.IsRefreshTokenGrantType() && options.UseRollingTokens && |
||||
|
context.Ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess); |
||||
|
|
||||
|
// If token revocation was explicitly disabled,
|
||||
|
// none of the following security routines apply.
|
||||
|
if (options.DisableTokenRevocation) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Extract the token identifier from the authentication ticket.
|
||||
|
var identifier = context.Ticket.GetTokenId(); |
||||
|
Debug.Assert(!string.IsNullOrEmpty(identifier), |
||||
|
"The authentication ticket should contain a ticket identifier."); |
||||
|
|
||||
|
// If rolling tokens are enabled or if the request is a grant_type=authorization_code request,
|
||||
|
// mark the authorization code or the refresh token as redeemed to prevent future reuses.
|
||||
|
// See https://tools.ietf.org/html/rfc6749#section-6 for more information.
|
||||
|
if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType()) |
||||
|
{ |
||||
|
var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); |
||||
|
if (token != null) |
||||
|
{ |
||||
|
await Tokens.RedeemAsync(token, context.HttpContext.RequestAborted); |
||||
|
|
||||
|
Logger.LogInformation("The token '{Identifier}' was automatically marked as redeemed.", identifier); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// When rolling tokens are enabled, revoke all the previously issued tokens associated
|
||||
|
// with the authorization if the request is a grant_type=refresh_token request.
|
||||
|
if (options.UseRollingTokens && context.Request.IsRefreshTokenGrantType()) |
||||
|
{ |
||||
|
await RevokeTokensAsync(context.Ticket, context.HttpContext); |
||||
|
} |
||||
|
|
||||
|
// When rolling tokens are disabled, extend the expiration date
|
||||
|
// of the existing token instead of returning a new refresh token
|
||||
|
// with a new expiration date if sliding expiration was not disabled.
|
||||
|
else if (options.UseSlidingExpiration && context.Request.IsRefreshTokenGrantType()) |
||||
|
{ |
||||
|
var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); |
||||
|
if (token != null) |
||||
|
{ |
||||
|
// Compute the new expiration date of the refresh token.
|
||||
|
var date = context.Options.SystemClock.UtcNow + |
||||
|
(context.Ticket.GetRefreshTokenLifetime() ?? |
||||
|
context.Options.RefreshTokenLifetime); |
||||
|
|
||||
|
await Tokens.ExtendAsync(token, date, context.HttpContext.RequestAborted); |
||||
|
|
||||
|
Logger.LogInformation("The expiration date of the refresh token '{Identifier}' " + |
||||
|
"was automatically updated: {Date}.", identifier, date); |
||||
|
|
||||
|
context.IncludeRefreshToken = false; |
||||
|
} |
||||
|
|
||||
|
// If the refresh token entry could not be
|
||||
|
// found in the database, generate a new one.
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// If no authorization was explicitly attached to the authentication ticket,
|
||||
|
// create an ad hoc authorization if an authorization code or a refresh token
|
||||
|
// is going to be returned to the client application as part of the response.
|
||||
|
if (!context.Ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId) && |
||||
|
(context.IncludeAuthorizationCode || context.IncludeRefreshToken)) |
||||
|
{ |
||||
|
await CreateAuthorizationAsync(context.Ticket, options, context.HttpContext, context.Request); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,721 @@ |
|||||
|
/* |
||||
|
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
* See https://github.com/openiddict/openiddict-core for more information concerning
|
||||
|
* the license and the contributors participating to this project. |
||||
|
*/ |
||||
|
|
||||
|
using System; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using AspNet.Security.OpenIdConnect.Client; |
||||
|
using AspNet.Security.OpenIdConnect.Extensions; |
||||
|
using AspNet.Security.OpenIdConnect.Primitives; |
||||
|
using AspNet.Security.OpenIdConnect.Server; |
||||
|
using Microsoft.AspNetCore.Authentication; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Moq; |
||||
|
using OpenIddict.Core; |
||||
|
using OpenIddict.Models; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace OpenIddict.Tests |
||||
|
{ |
||||
|
public partial class OpenIddictProviderTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public async Task ProcessSigninResponse_AuthenticationPropertiesAreAutomaticallyRestored() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); |
||||
|
|
||||
|
var ticket = new AuthenticationTicket( |
||||
|
new ClaimsPrincipal(identity), |
||||
|
new AuthenticationProperties(), |
||||
|
OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
|
||||
|
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); |
||||
|
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); |
||||
|
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); |
||||
|
ticket.SetProperty("custom_property_in_original_ticket", "original_value"); |
||||
|
|
||||
|
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>(); |
||||
|
|
||||
|
format.Setup(mock => mock.Protect(It.IsAny<AuthenticationTicket>())) |
||||
|
.Returns("8xLOxBtZp8"); |
||||
|
|
||||
|
format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) |
||||
|
.Returns(ticket); |
||||
|
|
||||
|
var token = new OpenIddictToken(); |
||||
|
|
||||
|
var manager = CreateTokenManager(instance => |
||||
|
{ |
||||
|
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(token); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(false); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(true); |
||||
|
}); |
||||
|
|
||||
|
var server = CreateAuthorizationServer(builder => |
||||
|
{ |
||||
|
builder.Services.AddSingleton(manager); |
||||
|
|
||||
|
builder.UseRollingTokens(); |
||||
|
|
||||
|
builder.Configure(options => options.RefreshTokenFormat = format.Object); |
||||
|
}); |
||||
|
|
||||
|
var client = new OpenIdConnectClient(server.CreateClient()); |
||||
|
|
||||
|
// Act
|
||||
|
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest |
||||
|
{ |
||||
|
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, |
||||
|
RefreshToken = "8xLOxBtZp8", |
||||
|
["do-not-flow-original-properties"] = true |
||||
|
}); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.NotNull(response.IdToken); |
||||
|
Assert.NotNull(response.RefreshToken); |
||||
|
|
||||
|
format.Verify(mock => mock.Protect( |
||||
|
It.Is<AuthenticationTicket>(value => |
||||
|
value.Properties.Items["custom_property_in_original_ticket"] == "original_value" && |
||||
|
value.Properties.Items["custom_property_in_new_ticket"] == "new_value"))); |
||||
|
} |
||||
|
[Fact] |
||||
|
public async Task ProcessSigninResponse_RefreshTokenIsAlwaysIssuedWhenRollingTokensAreEnabled() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); |
||||
|
|
||||
|
var ticket = new AuthenticationTicket( |
||||
|
new ClaimsPrincipal(identity), |
||||
|
new AuthenticationProperties(), |
||||
|
OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
|
||||
|
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); |
||||
|
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); |
||||
|
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); |
||||
|
|
||||
|
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>(); |
||||
|
|
||||
|
format.Setup(mock => mock.Protect(It.IsAny<AuthenticationTicket>())) |
||||
|
.Returns("8xLOxBtZp8"); |
||||
|
|
||||
|
format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) |
||||
|
.Returns(ticket); |
||||
|
|
||||
|
var token = new OpenIddictToken(); |
||||
|
|
||||
|
var manager = CreateTokenManager(instance => |
||||
|
{ |
||||
|
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(token); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(false); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(true); |
||||
|
}); |
||||
|
|
||||
|
var server = CreateAuthorizationServer(builder => |
||||
|
{ |
||||
|
builder.Services.AddSingleton(manager); |
||||
|
|
||||
|
builder.UseRollingTokens(); |
||||
|
|
||||
|
builder.Configure(options => options.RefreshTokenFormat = format.Object); |
||||
|
}); |
||||
|
|
||||
|
var client = new OpenIdConnectClient(server.CreateClient()); |
||||
|
|
||||
|
// Act
|
||||
|
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest |
||||
|
{ |
||||
|
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, |
||||
|
RefreshToken = "8xLOxBtZp8" |
||||
|
}); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.NotNull(response.RefreshToken); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ProcessSigninResponse_RefreshTokenIsNotIssuedWhenRollingTokensAreDisabled() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); |
||||
|
|
||||
|
var ticket = new AuthenticationTicket( |
||||
|
new ClaimsPrincipal(identity), |
||||
|
new AuthenticationProperties(), |
||||
|
OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
|
||||
|
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); |
||||
|
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); |
||||
|
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); |
||||
|
|
||||
|
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>(); |
||||
|
|
||||
|
format.Setup(mock => mock.Protect(It.IsAny<AuthenticationTicket>())) |
||||
|
.Returns("8xLOxBtZp8"); |
||||
|
|
||||
|
format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) |
||||
|
.Returns(ticket); |
||||
|
|
||||
|
var token = new OpenIddictToken(); |
||||
|
|
||||
|
var manager = CreateTokenManager(instance => |
||||
|
{ |
||||
|
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(token); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(false); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(true); |
||||
|
}); |
||||
|
|
||||
|
var server = CreateAuthorizationServer(builder => |
||||
|
{ |
||||
|
builder.Services.AddSingleton(manager); |
||||
|
|
||||
|
builder.Configure(options => options.RefreshTokenFormat = format.Object); |
||||
|
}); |
||||
|
|
||||
|
var client = new OpenIdConnectClient(server.CreateClient()); |
||||
|
|
||||
|
// Act
|
||||
|
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest |
||||
|
{ |
||||
|
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, |
||||
|
RefreshToken = "8xLOxBtZp8" |
||||
|
}); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Null(response.RefreshToken); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ProcessSigninResponse_AuthorizationCodeIsAutomaticallyRedeemed() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); |
||||
|
|
||||
|
var ticket = new AuthenticationTicket( |
||||
|
new ClaimsPrincipal(identity), |
||||
|
new AuthenticationProperties(), |
||||
|
OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
|
||||
|
ticket.SetPresenters("Fabrikam"); |
||||
|
ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); |
||||
|
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); |
||||
|
|
||||
|
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>(); |
||||
|
|
||||
|
format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) |
||||
|
.Returns(ticket); |
||||
|
|
||||
|
var token = new OpenIddictToken(); |
||||
|
|
||||
|
var manager = CreateTokenManager(instance => |
||||
|
{ |
||||
|
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(token); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(true); |
||||
|
}); |
||||
|
|
||||
|
var server = CreateAuthorizationServer(builder => |
||||
|
{ |
||||
|
builder.Services.AddSingleton(CreateApplicationManager(instance => |
||||
|
{ |
||||
|
var application = new OpenIddictApplication(); |
||||
|
|
||||
|
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(application); |
||||
|
|
||||
|
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(OpenIddictConstants.ClientTypes.Public); |
||||
|
})); |
||||
|
|
||||
|
builder.Services.AddSingleton(manager); |
||||
|
|
||||
|
builder.Configure(options => options.AuthorizationCodeFormat = format.Object); |
||||
|
}); |
||||
|
|
||||
|
var client = new OpenIdConnectClient(server.CreateClient()); |
||||
|
|
||||
|
// Act
|
||||
|
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest |
||||
|
{ |
||||
|
ClientId = "Fabrikam", |
||||
|
Code = "SplxlOBeZQQYbYS6WxSbIA", |
||||
|
GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, |
||||
|
RedirectUri = "http://www.fabrikam.com/path" |
||||
|
}); |
||||
|
|
||||
|
// Assert
|
||||
|
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Exactly(2)); |
||||
|
Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ProcessSigninResponse_RefreshTokenIsAutomaticallyRedeemedWhenRollingTokensAreEnabled() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); |
||||
|
|
||||
|
var ticket = new AuthenticationTicket( |
||||
|
new ClaimsPrincipal(identity), |
||||
|
new AuthenticationProperties(), |
||||
|
OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
|
||||
|
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); |
||||
|
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); |
||||
|
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); |
||||
|
|
||||
|
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>(); |
||||
|
|
||||
|
format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) |
||||
|
.Returns(ticket); |
||||
|
|
||||
|
var token = new OpenIddictToken(); |
||||
|
|
||||
|
var manager = CreateTokenManager(instance => |
||||
|
{ |
||||
|
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(token); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(false); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(true); |
||||
|
}); |
||||
|
|
||||
|
var server = CreateAuthorizationServer(builder => |
||||
|
{ |
||||
|
builder.Services.AddSingleton(manager); |
||||
|
|
||||
|
builder.UseRollingTokens(); |
||||
|
|
||||
|
builder.Configure(options => options.RefreshTokenFormat = format.Object); |
||||
|
}); |
||||
|
|
||||
|
var client = new OpenIdConnectClient(server.CreateClient()); |
||||
|
|
||||
|
// Act
|
||||
|
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest |
||||
|
{ |
||||
|
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, |
||||
|
RefreshToken = "8xLOxBtZp8" |
||||
|
}); |
||||
|
|
||||
|
// Assert
|
||||
|
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Exactly(2)); |
||||
|
Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ProcessSigninResponse_RefreshTokenIsNotRedeemedWhenRollingTokensAreDisabled() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); |
||||
|
|
||||
|
var ticket = new AuthenticationTicket( |
||||
|
new ClaimsPrincipal(identity), |
||||
|
new AuthenticationProperties(), |
||||
|
OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
|
||||
|
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); |
||||
|
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); |
||||
|
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); |
||||
|
|
||||
|
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>(); |
||||
|
|
||||
|
format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) |
||||
|
.Returns(ticket); |
||||
|
|
||||
|
var token = new OpenIddictToken(); |
||||
|
|
||||
|
var manager = CreateTokenManager(instance => |
||||
|
{ |
||||
|
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(token); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(false); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(true); |
||||
|
}); |
||||
|
|
||||
|
var server = CreateAuthorizationServer(builder => |
||||
|
{ |
||||
|
builder.Services.AddSingleton(manager); |
||||
|
|
||||
|
builder.Configure(options => options.RefreshTokenFormat = format.Object); |
||||
|
}); |
||||
|
|
||||
|
var client = new OpenIdConnectClient(server.CreateClient()); |
||||
|
|
||||
|
// Act
|
||||
|
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest |
||||
|
{ |
||||
|
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, |
||||
|
RefreshToken = "8xLOxBtZp8" |
||||
|
}); |
||||
|
|
||||
|
// Assert
|
||||
|
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Exactly(2)); |
||||
|
Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Never()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ProcessSigninResponse_PreviousTokensAreAutomaticallyRevokedWhenRollingTokensAreEnabled() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); |
||||
|
|
||||
|
var ticket = new AuthenticationTicket( |
||||
|
new ClaimsPrincipal(identity), |
||||
|
new AuthenticationProperties(), |
||||
|
OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
|
||||
|
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); |
||||
|
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); |
||||
|
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); |
||||
|
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); |
||||
|
|
||||
|
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>(); |
||||
|
|
||||
|
format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) |
||||
|
.Returns(ticket); |
||||
|
|
||||
|
var tokens = new[] |
||||
|
{ |
||||
|
new OpenIddictToken(), |
||||
|
new OpenIddictToken(), |
||||
|
new OpenIddictToken() |
||||
|
}; |
||||
|
|
||||
|
var manager = CreateTokenManager(instance => |
||||
|
{ |
||||
|
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(tokens[0]); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(false); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsValidAsync(tokens[0], It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(true); |
||||
|
|
||||
|
instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(tokens); |
||||
|
}); |
||||
|
|
||||
|
var server = CreateAuthorizationServer(builder => |
||||
|
{ |
||||
|
builder.Services.AddSingleton(manager); |
||||
|
|
||||
|
builder.UseRollingTokens(); |
||||
|
|
||||
|
builder.Configure(options => options.RefreshTokenFormat = format.Object); |
||||
|
}); |
||||
|
|
||||
|
var client = new OpenIdConnectClient(server.CreateClient()); |
||||
|
|
||||
|
// Act
|
||||
|
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest |
||||
|
{ |
||||
|
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, |
||||
|
RefreshToken = "8xLOxBtZp8" |
||||
|
}); |
||||
|
|
||||
|
// Assert
|
||||
|
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Exactly(2)); |
||||
|
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ProcessSigninResponse_PreviousTokensAreNotRevokedWhenRollingTokensAreDisabled() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); |
||||
|
|
||||
|
var ticket = new AuthenticationTicket( |
||||
|
new ClaimsPrincipal(identity), |
||||
|
new AuthenticationProperties(), |
||||
|
OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
|
||||
|
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); |
||||
|
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); |
||||
|
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); |
||||
|
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); |
||||
|
|
||||
|
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>(); |
||||
|
|
||||
|
format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) |
||||
|
.Returns(ticket); |
||||
|
|
||||
|
var tokens = new[] |
||||
|
{ |
||||
|
new OpenIddictToken(), |
||||
|
new OpenIddictToken(), |
||||
|
new OpenIddictToken() |
||||
|
}; |
||||
|
|
||||
|
var manager = CreateTokenManager(instance => |
||||
|
{ |
||||
|
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(tokens[0]); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(false); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsValidAsync(tokens[0], It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(true); |
||||
|
|
||||
|
instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(tokens); |
||||
|
}); |
||||
|
|
||||
|
var server = CreateAuthorizationServer(builder => |
||||
|
{ |
||||
|
builder.Services.AddSingleton(manager); |
||||
|
|
||||
|
builder.Configure(options => options.RefreshTokenFormat = format.Object); |
||||
|
}); |
||||
|
|
||||
|
var client = new OpenIdConnectClient(server.CreateClient()); |
||||
|
|
||||
|
// Act
|
||||
|
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest |
||||
|
{ |
||||
|
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, |
||||
|
RefreshToken = "8xLOxBtZp8" |
||||
|
}); |
||||
|
|
||||
|
// Assert
|
||||
|
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Exactly(2)); |
||||
|
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Never()); |
||||
|
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Never()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ProcessSigninResponse_ExtendsLifetimeWhenRollingTokensAreDisabledAndSlidingExpirationEnabled() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var ticket = new AuthenticationTicket( |
||||
|
new ClaimsPrincipal(), |
||||
|
new AuthenticationProperties(), |
||||
|
OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
|
||||
|
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); |
||||
|
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); |
||||
|
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); |
||||
|
|
||||
|
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>(); |
||||
|
|
||||
|
format.Setup(mock => mock.Protect(It.IsAny<AuthenticationTicket>())) |
||||
|
.Returns("8xLOxBtZp8"); |
||||
|
|
||||
|
format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) |
||||
|
.Returns(ticket); |
||||
|
|
||||
|
var token = new OpenIddictToken(); |
||||
|
|
||||
|
var manager = CreateTokenManager(instance => |
||||
|
{ |
||||
|
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(token); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(false); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(true); |
||||
|
}); |
||||
|
|
||||
|
var server = CreateAuthorizationServer(builder => |
||||
|
{ |
||||
|
builder.Services.AddSingleton(manager); |
||||
|
|
||||
|
builder.Configure(options => |
||||
|
{ |
||||
|
options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == |
||||
|
new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); |
||||
|
options.RefreshTokenLifetime = TimeSpan.FromDays(10); |
||||
|
options.RefreshTokenFormat = format.Object; |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
var client = new OpenIdConnectClient(server.CreateClient()); |
||||
|
|
||||
|
// Act
|
||||
|
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest |
||||
|
{ |
||||
|
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, |
||||
|
RefreshToken = "8xLOxBtZp8" |
||||
|
}); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Null(response.RefreshToken); |
||||
|
|
||||
|
Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, |
||||
|
new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), |
||||
|
It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ProcessSigninResponse_DoesNotExtendLifetimeWhenSlidingExpirationIsDisabled() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var ticket = new AuthenticationTicket( |
||||
|
new ClaimsPrincipal(), |
||||
|
new AuthenticationProperties(), |
||||
|
OpenIdConnectServerDefaults.AuthenticationScheme); |
||||
|
|
||||
|
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); |
||||
|
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); |
||||
|
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); |
||||
|
|
||||
|
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>(); |
||||
|
|
||||
|
format.Setup(mock => mock.Protect(It.IsAny<AuthenticationTicket>())) |
||||
|
.Returns("8xLOxBtZp8"); |
||||
|
|
||||
|
format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) |
||||
|
.Returns(ticket); |
||||
|
|
||||
|
var token = new OpenIddictToken(); |
||||
|
|
||||
|
var manager = CreateTokenManager(instance => |
||||
|
{ |
||||
|
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(token); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(false); |
||||
|
|
||||
|
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(true); |
||||
|
}); |
||||
|
|
||||
|
var server = CreateAuthorizationServer(builder => |
||||
|
{ |
||||
|
builder.Services.AddSingleton(manager); |
||||
|
|
||||
|
builder.DisableSlidingExpiration(); |
||||
|
|
||||
|
builder.Configure(options => |
||||
|
{ |
||||
|
options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == |
||||
|
new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); |
||||
|
options.RefreshTokenLifetime = TimeSpan.FromDays(10); |
||||
|
options.RefreshTokenFormat = format.Object; |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
var client = new OpenIdConnectClient(server.CreateClient()); |
||||
|
|
||||
|
// Act
|
||||
|
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest |
||||
|
{ |
||||
|
GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, |
||||
|
RefreshToken = "8xLOxBtZp8" |
||||
|
}); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Null(response.RefreshToken); |
||||
|
|
||||
|
Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, |
||||
|
new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), |
||||
|
It.IsAny<CancellationToken>()), Times.Never()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ProcessSigninResponse_AdHocAuthorizationIsAutomaticallyCreated() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var token = new OpenIddictToken(); |
||||
|
|
||||
|
var manager = CreateAuthorizationManager(instance => |
||||
|
{ |
||||
|
instance.Setup(mock => mock.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(new OpenIddictAuthorization()); |
||||
|
}); |
||||
|
|
||||
|
var server = CreateAuthorizationServer(builder => |
||||
|
{ |
||||
|
builder.Services.AddSingleton(CreateApplicationManager(instance => |
||||
|
{ |
||||
|
var application = new OpenIddictApplication(); |
||||
|
|
||||
|
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(application); |
||||
|
|
||||
|
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(true); |
||||
|
|
||||
|
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(OpenIddictConstants.ClientTypes.Public); |
||||
|
|
||||
|
instance.Setup(mock => mock.GetIdAsync(application, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); |
||||
|
})); |
||||
|
|
||||
|
builder.Services.AddSingleton(CreateTokenManager(instance => |
||||
|
{ |
||||
|
instance.Setup(mock => mock.CreateAsync(It.IsAny<OpenIddictTokenDescriptor>(), It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync(token); |
||||
|
|
||||
|
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) |
||||
|
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); |
||||
|
})); |
||||
|
|
||||
|
builder.Services.AddSingleton(manager); |
||||
|
}); |
||||
|
|
||||
|
var client = new OpenIdConnectClient(server.CreateClient()); |
||||
|
|
||||
|
// Act
|
||||
|
var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest |
||||
|
{ |
||||
|
ClientId = "Fabrikam", |
||||
|
RedirectUri = "http://www.fabrikam.com/path", |
||||
|
ResponseType = OpenIdConnectConstants.ResponseTypes.Code, |
||||
|
}); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.NotNull(response.Code); |
||||
|
|
||||
|
Mock.Get(manager).Verify(mock => mock.CreateAsync( |
||||
|
It.Is<OpenIddictAuthorizationDescriptor>(descriptor => |
||||
|
descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && |
||||
|
descriptor.Subject == "Bob le Magnifique" && |
||||
|
descriptor.Type == OpenIddictConstants.AuthorizationTypes.AdHoc), |
||||
|
It.IsAny<CancellationToken>()), Times.Once()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue