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