From d7589c229e6ffb6d246ddcafeeb65d8f14fed002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Tue, 17 Oct 2017 18:45:21 +0200 Subject: [PATCH] Allow custom properties marked as public to be returned as authorization/logout/token response parameters --- src/OpenIddict.Core/OpenIddictConstants.cs | 8 + src/OpenIddict/OpenIddictProvider.Helpers.cs | 118 ++ src/OpenIddict/OpenIddictProvider.Signin.cs | 141 -- src/OpenIddict/OpenIddictProvider.cs | 170 +++ .../OpenIddictProviderTests.Signin.cs | 1012 -------------- .../OpenIddictProviderTests.cs | 1240 ++++++++++++++++- 6 files changed, 1525 insertions(+), 1164 deletions(-) delete mode 100644 src/OpenIddict/OpenIddictProvider.Signin.cs delete mode 100644 test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs diff --git a/src/OpenIddict.Core/OpenIddictConstants.cs b/src/OpenIddict.Core/OpenIddictConstants.cs index 88149927..976d2198 100644 --- a/src/OpenIddict.Core/OpenIddictConstants.cs +++ b/src/OpenIddict.Core/OpenIddictConstants.cs @@ -43,6 +43,14 @@ namespace OpenIddict.Core public const string AuthorizationId = ".authorization_id"; } + public static class PropertyTypes + { + public const string Boolean = "#public_boolean"; + public const string Integer = "#public_integer"; + public const string Json = "#public_json"; + public const string String = "#public_string"; + } + public static class Separators { public const string Space = " "; diff --git a/src/OpenIddict/OpenIddictProvider.Helpers.cs b/src/OpenIddict/OpenIddictProvider.Helpers.cs index c7613c06..1be8b1ec 100644 --- a/src/OpenIddict/OpenIddictProvider.Helpers.cs +++ b/src/OpenIddict/OpenIddictProvider.Helpers.cs @@ -5,7 +5,10 @@ */ using System; +using System.Collections; +using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Security.Cryptography; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Extensions; @@ -16,6 +19,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json.Linq; using OpenIddict.Core; namespace OpenIddict @@ -446,5 +450,119 @@ namespace OpenIddict return false; } } + + private IEnumerable<(string property, string parameter, OpenIdConnectParameter value)> GetParameters( + OpenIdConnectRequest request, AuthenticationProperties properties) + { + Debug.Assert(properties != null, "The authentication properties shouldn't be null."); + + Debug.Assert(request != null, "The request shouldn't be null."); + Debug.Assert(request.IsAuthorizationRequest() || request.IsLogoutRequest() || request.IsTokenRequest(), + "The request should be an authorization, logout or token request."); + + foreach (var property in properties.Items) + { + if (string.IsNullOrEmpty(property.Key)) + { + continue; + } + + if (string.IsNullOrEmpty(property.Value)) + { + continue; + } + + if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Boolean)) + { + var name = property.Key.Substring( + /* index: */ 0, + /* length: */ property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.Boolean)); + + bool value; + + try + { + value = bool.Parse(property.Value); + } + + catch (Exception exception) + { + Logger.LogWarning(exception, "An error occurred while parsing the public property " + + "'{Name}' from the authentication ticket.", name); + + continue; + } + + yield return (property.Key, name, value); + } + + else if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Integer)) + { + var name = property.Key.Substring( + /* index: */ 0, + /* length: */ property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.Integer)); + + long value; + + try + { + value = long.Parse(property.Value, CultureInfo.InvariantCulture); + } + + catch (Exception exception) + { + Logger.LogWarning(exception, "An error occurred while parsing the public property " + + "'{Name}' from the authentication ticket.", name); + + continue; + } + + yield return (property.Key, name, value); + } + + else if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Json)) + { + var name = property.Key.Substring( + /* index: */ 0, + /* length: */ property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.Json)); + + if (request.IsAuthorizationRequest() || request.IsLogoutRequest()) + { + Logger.LogWarning("The JSON property '{Name}' was excluded as it was not " + + "compatible with the OpenID Connect response type.", name); + + continue; + } + + JToken value; + + try + { + value = JToken.Parse(property.Value); + } + + catch (Exception exception) + { + Logger.LogWarning(exception, "An error occurred while deserializing the public JSON " + + "property '{Name}' from the authentication ticket.", name); + + continue; + } + + yield return (property.Key, name, value); + } + + else if (property.Key.EndsWith(OpenIddictConstants.PropertyTypes.String)) + { + var name = property.Key.Substring( + /* index: */ 0, + /* length: */ property.Key.LastIndexOf(OpenIddictConstants.PropertyTypes.String)); + + yield return (property.Key, name, property.Value); + } + + continue; + } + } } } \ No newline at end of file diff --git a/src/OpenIddict/OpenIddictProvider.Signin.cs b/src/OpenIddict/OpenIddictProvider.Signin.cs deleted file mode 100644 index ef04be79..00000000 --- a/src/OpenIddict/OpenIddictProvider.Signin.cs +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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 OpenIddict.Core; - -namespace OpenIddict -{ - public partial class OpenIddictProvider : OpenIdConnectServerProvider - where TApplication : class where TAuthorization : class where TScope : class where TToken : class - { - public override async Task ProcessSigninResponse([NotNull] ProcessSigninResponseContext context) - { - var options = (OpenIddictOptions) context.Options; - - if (context.Request.IsTokenRequest() && (context.Request.IsAuthorizationCodeGrantType() || - context.Request.IsRefreshTokenGrantType())) - { - // Note: when handling a grant_type=authorization_code or refresh_token request, - // the OpenID Connect server middleware allows creating authentication tickets - // that are completely disconnected from the original code or refresh token ticket. - // This scenario is deliberately not supported in OpenIddict and all the tickets - // must be linked. To ensure the properties are flowed from the authorization code - // or the refresh token to the new ticket, they are manually restored if necessary. - - // Retrieve the original authentication ticket from the request properties. - var ticket = context.Request.GetProperty( - OpenIddictConstants.Properties.AuthenticationTicket); - Debug.Assert(ticket != null, "The authentication ticket shouldn't be null."); - - // If the properties instances of the two authentication tickets differ, - // restore the missing properties in the new authentication ticket. - if (!ReferenceEquals(ticket.Properties, context.Ticket.Properties)) - { - foreach (var property in ticket.Properties.Items) - { - // Don't override the properties that have been - // manually set on the new authentication ticket. - if (context.Ticket.HasProperty(property.Key)) - { - continue; - } - - context.Ticket.AddProperty(property.Key, property.Value); - } - - // Always include the "openid" scope when the developer doesn't explicitly call SetScopes. - // Note: the application is allowed to specify a different "scopes": in this case, - // don't replace the "scopes" property stored in the authentication ticket. - if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OpenId) && !context.Ticket.HasScope()) - { - context.Ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId); - } - - context.IncludeIdentityToken = context.Ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId); - } - - context.IncludeRefreshToken = context.Ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess); - - // Always include a refresh token for grant_type=refresh_token requests if - // rolling tokens are enabled and if the offline_access scope was specified. - if (context.Request.IsRefreshTokenGrantType()) - { - context.IncludeRefreshToken &= options.UseRollingTokens; - } - - // If token revocation was explicitly disabled, - // none of the following security routines apply. - if (options.DisableTokenRevocation) - { - return; - } - - // 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()) - { - if (!await TryRedeemTokenAsync(context.Ticket, context.HttpContext)) - { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The specified authorization code is no longer valid."); - - return; - } - } - - // 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()) - { - if (!await TryRevokeTokensAsync(context.Ticket, context.HttpContext)) - { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The specified refresh token is no longer valid."); - - return; - } - } - - // 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()) - { - if (!await TryExtendTokenAsync(context.Ticket, context.HttpContext, options)) - { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The specified refresh token is no longer valid."); - - return; - } - - // Prevent the OpenID Connect server from returning a new refresh token. - context.IncludeRefreshToken = false; - } - } - - // If no authorization was explicitly attached to the authentication ticket, - // create an ad hoc authorization if an authorization code or a refresh token - // is going to be returned to the client application as part of the response. - if (!context.Ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId) && - (context.IncludeAuthorizationCode || context.IncludeRefreshToken)) - { - await CreateAuthorizationAsync(context.Ticket, options, context.HttpContext, context.Request); - } - } - } -} \ No newline at end of file diff --git a/src/OpenIddict/OpenIddictProvider.cs b/src/OpenIddict/OpenIddictProvider.cs index 3e17d5ab..8aee01ec 100644 --- a/src/OpenIddict/OpenIddictProvider.cs +++ b/src/OpenIddict/OpenIddictProvider.cs @@ -5,8 +5,14 @@ */ using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +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; @@ -60,5 +66,169 @@ namespace OpenIddict /// Gets the tokens manager. /// public OpenIddictTokenManager Tokens { get; } + + public override Task ProcessChallengeResponse([NotNull] ProcessChallengeResponseContext context) + { + Debug.Assert(context.Request.IsAuthorizationRequest() || + context.Request.IsTokenRequest(), + "The request should be an authorization or token request."); + + // Add the custom properties that are marked as public + // as authorization or token response properties. + var parameters = GetParameters(context.Request, context.Properties); + foreach (var (property, parameter, value) in parameters) + { + context.Response.AddParameter(parameter, value); + } + + return Task.CompletedTask; + } + + public override async Task ProcessSigninResponse([NotNull] ProcessSigninResponseContext context) + { + var options = (OpenIddictOptions) context.Options; + + Debug.Assert(context.Request.IsAuthorizationRequest() || + context.Request.IsTokenRequest(), + "The request should be an authorization or token request."); + + 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 flowed from the authorization code + // or the refresh token to the new ticket, they are manually restored if necessary. + if (!context.Ticket.Properties.HasProperty(OpenIdConnectConstants.Properties.TokenId)) + { + // Retrieve the original authentication ticket from the request properties. + var ticket = context.Request.GetProperty( + OpenIddictConstants.Properties.AuthenticationTicket); + Debug.Assert(ticket != null, "The authentication ticket shouldn't be null."); + + 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); + } + + context.IncludeRefreshToken = context.Ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess); + + // Always include a refresh token for grant_type=refresh_token requests if + // rolling tokens are enabled and if the offline_access scope was specified. + if (context.Request.IsRefreshTokenGrantType()) + { + context.IncludeRefreshToken &= options.UseRollingTokens; + } + + // If token revocation was explicitly disabled, + // none of the following security routines apply. + if (options.DisableTokenRevocation) + { + return; + } + + // 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()) + { + if (!await TryRedeemTokenAsync(context.Ticket, context.HttpContext)) + { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The specified authorization code is no longer valid."); + + return; + } + } + + // 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()) + { + if (!await TryRevokeTokensAsync(context.Ticket, context.HttpContext)) + { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The specified refresh token is no longer valid."); + + return; + } + } + + // 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()) + { + if (!await TryExtendTokenAsync(context.Ticket, context.HttpContext, options)) + { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The specified refresh token is no longer valid."); + + return; + } + + // Prevent the OpenID Connect server from returning a new refresh token. + context.IncludeRefreshToken = false; + } + } + + // 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); + } + + // Add the custom properties that are marked as public as authorization or + // token response properties and remove them from the authentication ticket + // so they are not persisted in the authorization code/access/refresh token. + // Note: make sure the foreach statement iterates on a copy of the ticket + // as the property collection is modified when the property is removed. + var parameters = GetParameters(context.Request, context.Ticket.Properties); + foreach (var (property, parameter, value) in parameters.ToArray()) + { + context.Response.AddParameter(parameter, value); + context.Ticket.RemoveProperty(property); + } + } + + public override Task ProcessSignoutResponse([NotNull] ProcessSignoutResponseContext context) + { + Debug.Assert(context.Request.IsLogoutRequest(), "The request should be a logout request."); + + // Add the custom properties that are marked as public as logout response properties. + var parameters = GetParameters(context.Request, context.Properties); + foreach (var (property, parameter, value) in parameters) + { + context.Response.AddParameter(parameter, value); + } + + return Task.CompletedTask; + } } } \ No newline at end of file diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs deleted file mode 100644 index f7788d64..00000000 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Signin.cs +++ /dev/null @@ -1,1012 +0,0 @@ -/* - * 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.Collections.Immutable; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Client; -using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Primitives; -using AspNet.Security.OpenIdConnect.Server; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using OpenIddict.Core; -using OpenIddict.Models; -using Xunit; - -namespace OpenIddict.Tests -{ - public partial class OpenIddictProviderTests - { - [Fact] - public async Task ProcessSigninResponse_AuthenticationPropertiesAreAutomaticallyRestored() - { - // Arrange - var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIdConnectServerDefaults.AuthenticationScheme); - - ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); - ticket.SetProperty("custom_property_in_original_ticket", "original_value"); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseRollingTokens(); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8", - ["do-not-flow-original-properties"] = true - }); - - // Assert - Assert.NotNull(response.IdToken); - Assert.NotNull(response.RefreshToken); - - format.Verify(mock => mock.Protect( - It.Is(value => - value.Properties.Items["custom_property_in_original_ticket"] == "original_value" && - value.Properties.Items["custom_property_in_new_ticket"] == "new_value"))); - } - - [Fact] - public async Task ProcessSigninResponse_RefreshTokenIsIssuedForAuthorizationCodeRequestsWhenRollingTokensAreEnabled() - { - // 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); - ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); - })); - - builder.Services.AddSingleton(manager); - - builder.UseRollingTokens(); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.NotNull(response.RefreshToken); - } - - [Fact] - public async Task ProcessSigninResponse_RefreshTokenIsAlwaysIssuedWhenRollingTokensAreEnabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIdConnectServerDefaults.AuthenticationScheme); - - ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseRollingTokens(); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.NotNull(response.RefreshToken); - } - - [Fact] - public async Task ProcessSigninResponse_RefreshTokenIsNotIssuedWhenRollingTokensAreDisabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIdConnectServerDefaults.AuthenticationScheme); - - ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Null(response.RefreshToken); - } - - [Fact] - public async Task ProcessSigninResponse_AuthorizationCodeIsAutomaticallyRedeemed() - { - // Arrange - var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIdConnectServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Exactly(2)); - Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_ReturnsErrorResponseWhenRedeemingAuthorizationCodeFails() - { - // Arrange - var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIdConnectServerDefaults.AuthenticationScheme); - - ticket.SetPresenters("Fabrikam"); - ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.RedeemAsync(token, It.IsAny())) - .ThrowsAsync(new Exception()); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); - })); - - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.AuthorizationCodeFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Exactly(2)); - Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_RefreshTokenIsAutomaticallyRedeemedWhenRollingTokensAreEnabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIdConnectServerDefaults.AuthenticationScheme); - - ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseRollingTokens(); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.NotNull(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); - Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_ReturnsErrorResponseWhenRedeemingRefreshTokenFails() - { - // Arrange - var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIdConnectServerDefaults.AuthenticationScheme); - - ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.RedeemAsync(token, It.IsAny())) - .ThrowsAsync(new Exception()); - }); - - 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.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); - Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_RefreshTokenIsNotRedeemedWhenRollingTokensAreDisabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIdConnectServerDefaults.AuthenticationScheme); - - ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Null(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); - Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Never()); - } - - [Fact] - public async Task ProcessSigninResponse_PreviousTokensAreAutomaticallyRevokedWhenRollingTokensAreEnabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIdConnectServerDefaults.AuthenticationScheme); - - ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); - ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var tokens = ImmutableArray.Create( - new OpenIddictToken(), - new OpenIddictToken(), - new OpenIddictToken()); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(tokens[0]); - - instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(tokens[0], It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(tokens); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.UseRollingTokens(); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.NotNull(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_PreviousTokensAreNotRevokedWhenRollingTokensAreDisabled() - { - // Arrange - var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); - - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIdConnectServerDefaults.AuthenticationScheme); - - ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); - ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - var format = new Mock>(); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var tokens = ImmutableArray.Create( - new OpenIddictToken(), - new OpenIddictToken(), - new OpenIddictToken()); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(tokens[0]); - - instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(tokens[0], It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(tokens); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => options.RefreshTokenFormat = format.Object); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Null(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Never()); - Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny()), Times.Never()); - } - - [Fact] - public async Task ProcessSigninResponse_ExtendsLifetimeWhenRollingTokensAreDisabledAndSlidingExpirationEnabled() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIdConnectServerDefaults.AuthenticationScheme); - - ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == - new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); - options.RefreshTokenLifetime = TimeSpan.FromDays(10); - options.RefreshTokenFormat = format.Object; - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Null(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, - new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_DoesNotExtendLifetimeWhenSlidingExpirationIsDisabled() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIdConnectServerDefaults.AuthenticationScheme); - - ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.DisableSlidingExpiration(); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == - new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); - options.RefreshTokenLifetime = TimeSpan.FromDays(10); - options.RefreshTokenFormat = format.Object; - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Null(response.RefreshToken); - - Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, - new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), - It.IsAny()), Times.Never()); - } - - [Fact] - public async Task ProcessSigninResponse_ReturnsErrorResponseWhenExtendingLifetimeOfExistingTokenFailed() - { - // Arrange - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(), - new AuthenticationProperties(), - OpenIdConnectServerDefaults.AuthenticationScheme); - - ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); - - var format = new Mock>(); - - format.Setup(mock => mock.Protect(It.IsAny())) - .Returns("8xLOxBtZp8"); - - format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) - .Returns(ticket); - - var token = new OpenIddictToken(); - - var manager = CreateTokenManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) - .ReturnsAsync(false); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.ExtendAsync(token, It.IsAny(), It.IsAny())) - .ThrowsAsync(new Exception()); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(manager); - - builder.Configure(options => - { - options.SystemClock = Mock.Of(mock => mock.UtcNow == - new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); - options.RefreshTokenLifetime = TimeSpan.FromDays(10); - options.RefreshTokenFormat = format.Object; - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest - { - GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); - - Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, - new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), - It.IsAny()), Times.Once()); - } - - [Fact] - public async Task ProcessSigninResponse_AdHocAuthorizationIsAutomaticallyCreated() - { - // Arrange - var token = new OpenIddictToken(); - - var manager = CreateAuthorizationManager(instance => - { - instance.Setup(mock => mock.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - }); - - var server = CreateAuthorizationServer(builder => - { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); - - instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) - .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); - })); - - builder.Services.AddSingleton(CreateTokenManager(instance => - { - instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(token); - - instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); - })); - - builder.Services.AddSingleton(manager); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act - var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest - { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIdConnectConstants.ResponseTypes.Code, - }); - - // Assert - Assert.NotNull(response.Code); - - Mock.Get(manager).Verify(mock => mock.CreateAsync( - It.Is(descriptor => - descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && - descriptor.Subject == "Bob le Magnifique" && - descriptor.Type == OpenIddictConstants.AuthorizationTypes.AdHoc), - It.IsAny()), Times.Once()); - } - } -} diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.cs index 6cd6df63..3c2935e6 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.cs @@ -5,9 +5,13 @@ */ using System; +using System.Collections.Immutable; +using System.Linq; using System.Reflection; 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; @@ -24,8 +28,10 @@ using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using Moq; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OpenIddict.Core; using OpenIddict.Models; +using Xunit; namespace OpenIddict.Tests { @@ -39,6 +45,1195 @@ namespace OpenIddict.Tests public const string TokenEndpoint = "/connect/token"; public const string UserinfoEndpoint = "/connect/userinfo"; + [Fact] + public async Task ProcessChallengeResponse_CustomPublicParametersAreAddedToAuthorizationResponse() + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + }); + + 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, + ["attach-public-parameters"] = true, + ["deny-authorization"] = true + }); + + // Assert + Assert.NotEmpty(response.Error); + Assert.NotEmpty(response.ErrorDescription); + Assert.True((bool) response["custom_boolean_parameter"]); + Assert.Equal(42, (long) response["custom_integer_parameter"]); + Assert.Equal("value", (string) response["custom_string_parameter"]); + } + + [Fact] + public async Task ProcessChallengeResponse_CustomPublicParametersAreAddedToTokenResponse() + { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w", + Scope = OpenIdConnectConstants.Scopes.OfflineAccess, + ["attach-public-parameters"] = true, + ["deny-authorization"] = true + }); + + // Assert + Assert.NotEmpty(response.Error); + Assert.NotEmpty(response.ErrorDescription); + Assert.True((bool) response["custom_boolean_parameter"]); + Assert.Equal(42, (long) response["custom_integer_parameter"]); + Assert.Equal(new JArray(1, 2, 3), (JArray) response["custom_json_array_parameter"]); + Assert.Equal(JObject.FromObject(new { Property = "value" }), (JObject) response["custom_json_object_parameter"]); + Assert.Equal("value", (string) response["custom_string_parameter"]); + } + + [Fact] + public async Task ProcessSigninResponse_AuthenticationPropertiesAreAutomaticallyRestored() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + ticket.SetProperty("custom_property_in_original_ticket", "original_value"); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.UseRollingTokens(); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8", + ["do-not-flow-original-properties"] = true + }); + + // Assert + Assert.NotNull(response.IdToken); + Assert.NotNull(response.RefreshToken); + + format.Verify(mock => mock.Protect( + It.Is(value => + value.Properties.Items["custom_property_in_original_ticket"] == "original_value" && + value.Properties.Items["custom_property_in_new_ticket"] == "new_value"))); + } + + [Fact] + public async Task ProcessSigninResponse_RefreshTokenIsIssuedForAuthorizationCodeRequestsWhenRollingTokensAreEnabled() + { + // 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); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + builder.Services.AddSingleton(manager); + + builder.UseRollingTokens(); + + builder.Configure(options => options.AuthorizationCodeFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, + RedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Assert.NotNull(response.RefreshToken); + } + + [Fact] + public async Task ProcessSigninResponse_RefreshTokenIsAlwaysIssuedWhenRollingTokensAreEnabled() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.UseRollingTokens(); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.NotNull(response.RefreshToken); + } + + [Fact] + public async Task ProcessSigninResponse_RefreshTokenIsNotIssuedWhenRollingTokensAreDisabled() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + } + + [Fact] + public async Task ProcessSigninResponse_AuthorizationCodeIsAutomaticallyRedeemed() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetPresenters("Fabrikam"); + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.AuthorizationCodeFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, + RedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Exactly(2)); + Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSigninResponse_ReturnsErrorResponseWhenRedeemingAuthorizationCodeFails() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetPresenters("Fabrikam"); + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.RedeemAsync(token, It.IsAny())) + .ThrowsAsync(new Exception()); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.AuthorizationCodeFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, + RedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Exactly(2)); + Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSigninResponse_RefreshTokenIsAutomaticallyRedeemedWhenRollingTokensAreEnabled() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.UseRollingTokens(); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.NotNull(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); + Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSigninResponse_ReturnsErrorResponseWhenRedeemingRefreshTokenFails() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.RedeemAsync(token, It.IsAny())) + .ThrowsAsync(new Exception()); + }); + + 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.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); + Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSigninResponse_RefreshTokenIsNotRedeemedWhenRollingTokensAreDisabled() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); + Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Never()); + } + + [Fact] + public async Task ProcessSigninResponse_PreviousTokensAreAutomaticallyRevokedWhenRollingTokensAreEnabled() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var tokens = ImmutableArray.Create( + new OpenIddictToken(), + new OpenIddictToken(), + new OpenIddictToken()); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(tokens[0]); + + instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(tokens[0], It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(tokens); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.UseRollingTokens(); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.NotNull(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); + Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSigninResponse_PreviousTokensAreNotRevokedWhenRollingTokensAreDisabled() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var tokens = ImmutableArray.Create( + new OpenIddictToken(), + new OpenIddictToken(), + new OpenIddictToken()); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(tokens[0]); + + instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(tokens[0], It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(tokens); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Exactly(2)); + Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Never()); + Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny()), Times.Never()); + } + + [Fact] + public async Task ProcessSigninResponse_ExtendsLifetimeWhenRollingTokensAreDisabledAndSlidingExpirationEnabled() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + options.RefreshTokenLifetime = TimeSpan.FromDays(10); + options.RefreshTokenFormat = format.Object; + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, + new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSigninResponse_DoesNotExtendLifetimeWhenSlidingExpirationIsDisabled() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.DisableSlidingExpiration(); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + options.RefreshTokenLifetime = TimeSpan.FromDays(10); + options.RefreshTokenFormat = format.Object; + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, + new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), + It.IsAny()), Times.Never()); + } + + [Fact] + public async Task ProcessSigninResponse_ReturnsErrorResponseWhenExtendingLifetimeOfExistingTokenFailed() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.ExtendAsync(token, It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception()); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + options.RefreshTokenLifetime = TimeSpan.FromDays(10); + options.RefreshTokenFormat = format.Object; + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, + new DateTimeOffset(2017, 01, 15, 00, 00, 00, TimeSpan.Zero), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSigninResponse_AdHocAuthorizationIsAutomaticallyCreated() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateAuthorizationManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) + .ReturnsAsync(new OpenIddictAuthorization()); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + + instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + })); + + builder.Services.AddSingleton(CreateTokenManager(instance => + { + instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + })); + + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = OpenIdConnectConstants.ResponseTypes.Code, + }); + + // Assert + Assert.NotNull(response.Code); + + Mock.Get(manager).Verify(mock => mock.CreateAsync( + It.Is(descriptor => + descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && + descriptor.Subject == "Bob le Magnifique" && + descriptor.Type == OpenIddictConstants.AuthorizationTypes.AdHoc), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSigninResponse_CustomPublicParametersAreAddedToAuthorizationResponse() + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + }); + + 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, + ["attach-public-parameters"] = true + }); + + // Assert + Assert.True((bool) response["custom_boolean_parameter"]); + Assert.Equal(42, (long) response["custom_integer_parameter"]); + Assert.False(response.HasParameter("custom_json_array_parameter")); + Assert.False(response.HasParameter("custom_json_object_parameter")); + Assert.Equal("value", (string) response["custom_string_parameter"]); + } + + [Fact] + public async Task ProcessSigninResponse_CustomPublicParametersAreAddedToTokenResponse() + { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w", + Scope = OpenIdConnectConstants.Scopes.OfflineAccess, + ["attach-public-parameters"] = true + }); + + // Assert + Assert.True((bool) response["custom_boolean_parameter"]); + Assert.Equal(42, (long) response["custom_integer_parameter"]); + Assert.Equal(new JArray(1, 2, 3), (JArray) response["custom_json_array_parameter"]); + Assert.Equal(JObject.FromObject(new { Property = "value" }), (JObject) response["custom_json_object_parameter"]); + Assert.Equal("value", (string) response["custom_string_parameter"]); + } + + [Fact] + public async Task ProcessSigninResponse_CustomPublicParametersAreRemovedFromTicket() + { + // Arrange + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + var server = CreateAuthorizationServer(builder => + { + builder.Configure(options => options.AccessTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w", + Scope = OpenIdConnectConstants.Scopes.OfflineAccess, + ["attach-public-parameters"] = true + }); + + // Assert + Assert.NotNull(response.AccessToken); + + format.Verify(mock => mock.Protect( + It.Is(ticket => + !ticket.Properties.Items.Any(property => property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Boolean)) && + !ticket.Properties.Items.Any(property => property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Integer)) && + !ticket.Properties.Items.Any(property => property.Key.EndsWith(OpenIddictConstants.PropertyTypes.Json)) && + !ticket.Properties.Items.Any(property => property.Key.EndsWith(OpenIddictConstants.PropertyTypes.String))))); + } + + [Fact] + public async Task ProcessSignoutResponse_CustomPublicParametersAreAddedToLogoutResponse() + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + instance.Setup(mock => mock.ValidatePostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny())) + .ReturnsAsync(true); + })); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest + { + PostLogoutRedirectUri = "http://www.fabrikam.com/path", + State = "af0ifjsldkj", + ["attach-public-parameters"] = true + }); + + // Assert + Assert.True((bool) response["custom_boolean_parameter"]); + Assert.Equal(42, (long) response["custom_integer_parameter"]); + Assert.False(response.HasParameter("custom_json_array_parameter")); + Assert.False(response.HasParameter("custom_json_object_parameter")); + Assert.Equal("value", (string) response["custom_string_parameter"]); + } + private static TestServer CreateAuthorizationServer(Action configuration = null) { var builder = new WebHostBuilder(); @@ -141,21 +1336,44 @@ namespace OpenIddict.Tests app.Run(context => { var request = context.GetOpenIdConnectRequest(); - if (request.IsAuthorizationRequest() || request.IsTokenRequest()) + if (request == null) + { + return Task.CompletedTask; + } + + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Magnifique"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetScopes(request.GetScopes()); + + if (request.HasParameter("attach-authorization")) { - var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); - identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Magnifique"); + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70"); + } - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - OpenIdConnectServerDefaults.AuthenticationScheme); + if (request.HasParameter("attach-public-parameters")) + { + ticket.SetProperty("custom_boolean_parameter" + OpenIddictConstants.PropertyTypes.Boolean, "true"); + ticket.SetProperty("custom_integer_parameter" + OpenIddictConstants.PropertyTypes.Integer, "42"); - ticket.SetScopes(request.GetScopes()); + ticket.SetProperty("custom_json_array_parameter" + OpenIddictConstants.PropertyTypes.Json, + new JArray(1, 2, 3).ToString()); + ticket.SetProperty("custom_json_object_parameter" + OpenIddictConstants.PropertyTypes.Json, + JObject.FromObject(new { Property = "value" }).ToString()); - if (request.HasParameter("attach-authorization")) + ticket.SetProperty("custom_string_parameter" + OpenIddictConstants.PropertyTypes.String, "value"); + } + + if (request.IsAuthorizationRequest() || request.IsTokenRequest()) + { + if (request.HasParameter("deny-authorization")) { - ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70"); + return context.ForbidAsync(OpenIdConnectServerDefaults.AuthenticationScheme, ticket.Properties); } if (request.HasParameter("do-not-flow-original-properties")) @@ -171,7 +1389,7 @@ namespace OpenIddict.Tests else if (request.IsLogoutRequest()) { - return context.SignOutAsync(OpenIdConnectServerDefaults.AuthenticationScheme); + return context.SignOutAsync(OpenIdConnectServerDefaults.AuthenticationScheme, ticket.Properties); } else if (request.IsUserinfoRequest())