From 0a409a3013453f5e1e29a9db3069dd70726e9684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 23 Oct 2017 15:07:11 +0200 Subject: [PATCH] Correctly restore the authentication properties of non-reference authorization codes/refresh tokens --- src/OpenIddict/OpenIddictProvider.Helpers.cs | 158 ++++--- .../OpenIddictProvider.Serialization.cs | 52 ++- src/OpenIddict/OpenIddictProvider.cs | 4 +- .../OpenIddictProviderTests.Exchange.cs | 55 ++- .../OpenIddictProviderTests.Introspection.cs | 10 +- .../OpenIddictProviderTests.Revocation.cs | 5 +- .../OpenIddictProviderTests.Serialization.cs | 423 ++++++++++++++++-- .../OpenIddictProviderTests.cs | 52 ++- 8 files changed, 637 insertions(+), 122 deletions(-) diff --git a/src/OpenIddict/OpenIddictProvider.Helpers.cs b/src/OpenIddict/OpenIddictProvider.Helpers.cs index 1be8b1ec..22cce5f8 100644 --- a/src/OpenIddict/OpenIddictProvider.Helpers.cs +++ b/src/OpenIddict/OpenIddictProvider.Helpers.cs @@ -31,11 +31,6 @@ namespace OpenIddict [NotNull] AuthenticationTicket ticket, [NotNull] OpenIddictOptions options, [NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request) { - if (options.DisableTokenRevocation) - { - return; - } - var descriptor = new OpenIddictAuthorizationDescriptor { Status = OpenIddictConstants.Statuses.Valid, @@ -218,71 +213,118 @@ namespace OpenIddict } private async Task ReceiveTokenAsync( - [NotNull] string value, [NotNull] OpenIddictOptions options, - [NotNull] HttpContext context, [NotNull] OpenIdConnectRequest request, + [NotNull] string type, [NotNull] string value, + [NotNull] OpenIddictOptions options, [NotNull] HttpContext context, + [NotNull] OpenIdConnectRequest request, [NotNull] ISecureDataFormat format) { - if (!options.UseReferenceTokens) - { - return null; - } + Debug.Assert(!(options.DisableTokenRevocation && options.UseReferenceTokens), + "Token revocation cannot be disabled when using reference tokens."); - string hash; - try + Debug.Assert(type == OpenIdConnectConstants.TokenUsages.AccessToken || + type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || + type == OpenIdConnectConstants.TokenUsages.RefreshToken, + "Only authorization codes, access and refresh tokens should be validated using this method."); + + string identifier; + AuthenticationTicket ticket; + TToken token; + + if (options.UseReferenceTokens) { - // Compute the digest of the received token and use it - // to retrieve the reference token from the database. - using (var algorithm = SHA256.Create()) + string hash; + try { - hash = Convert.ToBase64String(algorithm.ComputeHash(Base64UrlEncoder.DecodeBytes(value))); + // Compute the digest of the received token and use it + // to retrieve the reference token from the database. + using (var algorithm = SHA256.Create()) + { + hash = Convert.ToBase64String(algorithm.ComputeHash(Base64UrlEncoder.DecodeBytes(value))); + } } - } - // Swallow format-related exceptions to ensure badly formed - // or tampered tokens don't cause an exception at this stage. - catch - { - return null; - } + // Swallow format-related exceptions to ensure badly formed + // or tampered tokens don't cause an exception at this stage. + catch + { + return null; + } - // Retrieve the token entry from the database. If it - // cannot be found, assume the token is not valid. - var token = await Tokens.FindByHashAsync(hash, context.RequestAborted); - if (token == null) - { - Logger.LogInformation("The reference token corresponding to the '{Hash}' hashed " + - "identifier cannot be found in the database.", hash); + // Retrieve the token entry from the database. If it + // cannot be found, assume the token is not valid. + token = await Tokens.FindByHashAsync(hash, context.RequestAborted); + if (token == null) + { + Logger.LogInformation("The reference token corresponding to the '{Hash}' hashed " + + "identifier cannot be found in the database.", hash); - return null; - } + return null; + } - var identifier = await Tokens.GetIdAsync(token, context.RequestAborted); - if (string.IsNullOrEmpty(identifier)) - { - Logger.LogWarning("The identifier associated with the received token cannot be retrieved. " + - "This may indicate that the token entry is corrupted."); + identifier = await Tokens.GetIdAsync(token, context.RequestAborted); + if (string.IsNullOrEmpty(identifier)) + { + Logger.LogWarning("The identifier associated with the received token cannot be retrieved. " + + "This may indicate that the token entry is corrupted."); - return null; + return null; + } + + // Extract the encrypted payload from the token. If it's null or empty, + // assume the token is not a reference token and consider it as invalid. + var ciphertext = await Tokens.GetCiphertextAsync(token, context.RequestAborted); + if (string.IsNullOrEmpty(ciphertext)) + { + Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be retrieved. " + + "This may indicate that the token is not a reference token.", identifier); + + return null; + } + + ticket = format.Unprotect(ciphertext); + if (ticket == null) + { + Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be decrypted. " + + "This may indicate that the token entry is corrupted or tampered.", + await Tokens.GetIdAsync(token, context.RequestAborted)); + + return null; + } } - // Extract the encrypted payload from the token. If it's null or empty, - // assume the token is not a reference token and consider it as invalid. - var ciphertext = await Tokens.GetCiphertextAsync(token, context.RequestAborted); - if (string.IsNullOrEmpty(ciphertext)) + else if (type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || + type == OpenIdConnectConstants.TokenUsages.RefreshToken) { - Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be retrieved. " + - "This may indicate that the token is not a reference token.", identifier); + ticket = format.Unprotect(value); + if (ticket == null) + { + Logger.LogTrace("The received token was invalid or malformed: {Token}.", value); - return null; + return null; + } + + // Retrieve the authorization code/refresh token entry from the database. + // If it cannot be found, assume the authorization code/refresh token is not valid. + token = await Tokens.FindByIdAsync(ticket.GetTokenId(), context.RequestAborted); + if (token == null) + { + Logger.LogInformation("The token '{Identifier}' cannot be found in the database.", ticket.GetTokenId()); + + return null; + } + + identifier = await Tokens.GetIdAsync(token, context.RequestAborted); + if (string.IsNullOrEmpty(identifier)) + { + Logger.LogWarning("The identifier associated with the received token cannot be retrieved. " + + "This may indicate that the token entry is corrupted."); + + return null; + } } - var ticket = format.Unprotect(ciphertext); - if (ticket == null) + else { - Logger.LogWarning("The ciphertext associated with the token '{Identifier}' cannot be decrypted. " + - "This may indicate that the token entry is corrupted or tampered.", - await Tokens.GetIdAsync(token, context.RequestAborted)); - return null; } @@ -298,9 +340,9 @@ namespace OpenIddict ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, await Tokens.GetAuthorizationIdAsync(token, context.RequestAborted)); - Logger.LogTrace("The reference token '{Identifier}' was successfully retrieved " + - "from the database and decrypted: {Claims} ; {Properties}.", - identifier, ticket.Principal.Claims, ticket.Properties.Items); + Logger.LogTrace("The token '{Identifier}' was successfully decrypted and " + + "retrieved from the database: {Claims} ; {Properties}.", + ticket.GetTokenId(), ticket.Principal.Claims, ticket.Properties.Items); return ticket; } @@ -351,12 +393,6 @@ namespace OpenIddict foreach (var token in await Tokens.FindByAuthorizationIdAsync(identifier, context.RequestAborted)) { - // Don't overwrite the status of the token used in the token request. - if (string.Equals(ticket.GetTokenId(), await Tokens.GetIdAsync(token, context.RequestAborted))) - { - continue; - } - try { await Tokens.RevokeAsync(token, context.RequestAborted); diff --git a/src/OpenIddict/OpenIddictProvider.Serialization.cs b/src/OpenIddict/OpenIddictProvider.Serialization.cs index 29ca9166..9b913895 100644 --- a/src/OpenIddict/OpenIddictProvider.Serialization.cs +++ b/src/OpenIddict/OpenIddictProvider.Serialization.cs @@ -18,60 +18,70 @@ namespace OpenIddict public override async Task DeserializeAccessToken([NotNull] DeserializeAccessTokenContext context) { var options = (OpenIddictOptions) context.Options; - if (!options.UseReferenceTokens) + if (options.DisableTokenRevocation) { return; } context.Ticket = await ReceiveTokenAsync( + OpenIdConnectConstants.TokenUsages.AccessToken, context.AccessToken, options, context.HttpContext, context.Request, context.DataFormat); // Prevent the OpenID Connect server middleware from using - // its default logic to deserialize the reference token. - context.HandleDeserialization(); + // its default logic to deserialize reference access tokens. + if (options.UseReferenceTokens) + { + context.HandleDeserialization(); + } } public override async Task DeserializeAuthorizationCode([NotNull] DeserializeAuthorizationCodeContext context) { var options = (OpenIddictOptions) context.Options; - if (!options.UseReferenceTokens) + if (options.DisableTokenRevocation) { return; } context.Ticket = await ReceiveTokenAsync( + OpenIdConnectConstants.TokenUsages.AuthorizationCode, context.AuthorizationCode, options, context.HttpContext, context.Request, context.DataFormat); - // Prevent the OpenID Connect server middleware from using - // its default logic to deserialize the reference token. + // Prevent the OpenID Connect server middleware from using its default logic. context.HandleDeserialization(); } public override async Task DeserializeRefreshToken([NotNull] DeserializeRefreshTokenContext context) { var options = (OpenIddictOptions) context.Options; - if (!options.UseReferenceTokens) + if (options.DisableTokenRevocation) { return; } context.Ticket = await ReceiveTokenAsync( + OpenIdConnectConstants.TokenUsages.RefreshToken, context.RefreshToken, options, context.HttpContext, context.Request, context.DataFormat); - // Prevent the OpenID Connect server middleware from using - // its default logic to deserialize the reference token. + // Prevent the OpenID Connect server middleware from using its default logic. context.HandleDeserialization(); } public override async Task SerializeAccessToken([NotNull] SerializeAccessTokenContext context) { + var options = (OpenIddictOptions) context.Options; + if (options.DisableTokenRevocation) + { + return; + } + var token = await CreateTokenAsync( OpenIdConnectConstants.TokenUsages.AccessToken, - context.Ticket, (OpenIddictOptions) context.Options, - context.HttpContext, context.Request, context.DataFormat); + context.Ticket, options, context.HttpContext, + context.Request, context.DataFormat); // If a reference token was returned by CreateTokenAsync(), // force the OpenID Connect server middleware to use it. @@ -87,12 +97,18 @@ namespace OpenIddict public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context) { + var options = (OpenIddictOptions) context.Options; + if (options.DisableTokenRevocation) + { + return; + } + Debug.Assert(context.Request.IsAuthorizationRequest(), "The request should be an authorization request."); var token = await CreateTokenAsync( OpenIdConnectConstants.TokenUsages.AuthorizationCode, - context.Ticket, (OpenIddictOptions) context.Options, - context.HttpContext, context.Request, context.DataFormat); + context.Ticket, options, context.HttpContext, + context.Request, context.DataFormat); // If a reference token was returned by CreateTokenAsync(), // force the OpenID Connect server middleware to use it. @@ -108,12 +124,18 @@ namespace OpenIddict public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) { + var options = (OpenIddictOptions) context.Options; + if (options.DisableTokenRevocation) + { + return; + } + Debug.Assert(context.Request.IsTokenRequest(), "The request should be a token request."); var token = await CreateTokenAsync( OpenIdConnectConstants.TokenUsages.RefreshToken, - context.Ticket, (OpenIddictOptions) context.Options, - context.HttpContext, context.Request, context.DataFormat); + context.Ticket, options, context.HttpContext, + context.Request, context.DataFormat); // If a reference token was returned by CreateTokenAsync(), // force the OpenID Connect server middleware to use it. diff --git a/src/OpenIddict/OpenIddictProvider.cs b/src/OpenIddict/OpenIddictProvider.cs index 8aee01ec..9356c8bc 100644 --- a/src/OpenIddict/OpenIddictProvider.cs +++ b/src/OpenIddict/OpenIddictProvider.cs @@ -156,7 +156,9 @@ namespace OpenIddict { context.Reject( error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The specified authorization code is no longer valid."); + description: context.Request.IsAuthorizationCodeGrantType() ? + "The specified authorization code is no longer valid." : + "The specified refresh token is no longer valid."); return; } diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs index 945a21b1..0a2d391e 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs @@ -560,7 +560,7 @@ namespace OpenIddict.Tests // Assert Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); + Assert.Equal("The specified authorization code is invalid.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); } @@ -617,7 +617,7 @@ namespace OpenIddict.Tests // Assert Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); + Assert.Equal("The specified refresh token is invalid.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); } @@ -647,6 +647,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) .ReturnsAsync(true); }); @@ -684,7 +687,7 @@ namespace OpenIddict.Tests Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny()), Times.Once()); } @@ -712,6 +715,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) .ReturnsAsync(true); }); @@ -747,7 +753,7 @@ namespace OpenIddict.Tests Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny()), Times.Once()); } @@ -798,6 +804,12 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) + .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) .ReturnsAsync(true); @@ -875,6 +887,12 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) + .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) .ReturnsAsync(true); @@ -933,6 +951,12 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) .ReturnsAsync(tokens[0]); + instance.Setup(mock => mock.GetIdAsync(tokens[0], It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.GetAuthorizationIdAsync(tokens[0], It.IsAny())) + .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) .ReturnsAsync(true); @@ -981,7 +1005,7 @@ namespace OpenIddict.Tests Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Once()); @@ -1016,6 +1040,12 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) .ReturnsAsync(tokens[0]); + instance.Setup(mock => mock.GetIdAsync(tokens[0], It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.GetAuthorizationIdAsync(tokens[0], It.IsAny())) + .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) .ReturnsAsync(true); @@ -1062,7 +1092,7 @@ namespace OpenIddict.Tests Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Once()); @@ -1094,6 +1124,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) .ReturnsAsync(false); @@ -1134,7 +1167,7 @@ namespace OpenIddict.Tests Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); } @@ -1163,6 +1196,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) .ReturnsAsync(false); @@ -1201,7 +1237,7 @@ namespace OpenIddict.Tests Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); } @@ -1248,6 +1284,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) .ReturnsAsync(true); }); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs index bbcf8c67..077f36bc 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs @@ -571,6 +571,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) .ReturnsAsync(false); }); @@ -610,7 +613,7 @@ namespace OpenIddict.Tests Assert.Single(response.GetParameters()); Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); } @@ -708,6 +711,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) .ReturnsAsync(false); }); @@ -747,7 +753,7 @@ namespace OpenIddict.Tests Assert.Single(response.GetParameters()); Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); } } diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs index 2adb784a..1133b382 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs @@ -455,6 +455,9 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); }); var server = CreateAuthorizationServer(builder => @@ -475,7 +478,7 @@ namespace OpenIddict.Tests // Assert Assert.Empty(response.GetParameters()); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(token, It.IsAny()), Times.Once()); } } diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs index d44e48cf..8fef4e9d 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs @@ -86,7 +86,7 @@ namespace OpenIddict.Tests OpenIdConnectServerDefaults.AuthenticationScheme); ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken); var format = new Mock>(); @@ -122,7 +122,7 @@ namespace OpenIddict.Tests builder.Services.AddSingleton(manager); - builder.Configure(options => options.RefreshTokenFormat = format.Object); + builder.Configure(options => options.AccessTokenFormat = format.Object); }); var client = new OpenIdConnectClient(server.CreateClient()); @@ -144,7 +144,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task DeserializeAccessToken_ReturnsNullForMissingTokenIdentifier() + public async Task DeserializeAccessToken_ReturnsNullForMissingReferenceTokenIdentifier() { // Arrange var token = new OpenIddictToken(); @@ -199,7 +199,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task DeserializeAccessToken_ReturnsNullForMissingTokenCiphertext() + public async Task DeserializeAccessToken_ReturnsNullForMissingReferenceTokenCiphertext() { // Arrange var token = new OpenIddictToken(); @@ -257,7 +257,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task DeserializeAccessToken_ReturnsNullForInvalidTokenCiphertext() + public async Task DeserializeAccessToken_ReturnsNullForInvalidReferenceTokenCiphertext() { // Arrange var format = new Mock>(); @@ -323,7 +323,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task DeserializeAccessToken_ReturnsExpectedToken() + public async Task DeserializeAccessToken_ReturnsExpectedReferenceToken() { // Arrange var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); @@ -335,7 +335,6 @@ namespace OpenIddict.Tests OpenIdConnectServerDefaults.AuthenticationScheme); ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); var format = new Mock>(); @@ -469,7 +468,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task DeserializeAuthorizationCode_AuthorizationCodeIsNotRetrievedFromDatabaseWhenReferenceTokensAreDisabled() + public async Task DeserializeAuthorizationCode_AuthorizationCodeIsNotRetrievedUsingHashWhenReferenceTokensAreDisabled() { // Arrange var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); @@ -481,7 +480,6 @@ namespace OpenIddict.Tests OpenIdConnectServerDefaults.AuthenticationScheme); ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); var format = new Mock>(); @@ -495,6 +493,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) .ReturnsAsync(true); }); @@ -539,7 +540,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task DeserializeAuthorizationCode_ReturnsNullForMissingTokenIdentifier() + public async Task DeserializeAuthorizationCode_ReturnsNullForMissingReferenceTokenIdentifier() { // Arrange var token = new OpenIddictToken(); @@ -594,7 +595,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task DeserializeAuthorizationCode_ReturnsNullForMissingTokenCiphertext() + public async Task DeserializeAuthorizationCode_ReturnsNullForMissingReferenceTokenCiphertext() { // Arrange var token = new OpenIddictToken(); @@ -652,7 +653,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task DeserializeAuthorizationCode_ReturnsNullForInvalidTokenCiphertext() + public async Task DeserializeAuthorizationCode_ReturnsNullForInvalidReferenceTokenCiphertext() { // Arrange var format = new Mock>(); @@ -718,7 +719,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task DeserializeAuthorizationCode_ReturnsExpectedToken() + public async Task DeserializeAuthorizationCode_ReturnsExpectedReferenceToken() { // Arrange var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); @@ -729,9 +730,6 @@ namespace OpenIddict.Tests new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme); - ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - var format = new Mock>(); format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) @@ -814,6 +812,192 @@ namespace OpenIddict.Tests format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); } + [Fact] + public async Task DeserializeAuthorizationCode_ReturnsNullForMissingTokenIdentifier() + { + // 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); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(ticket); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + builder.Configure(options => options.AuthorizationCodeFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AuthorizationCode + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + } + + [Fact] + public async Task DeserializeAuthorizationCode_ReturnsNullForInvalidTokenCiphertext() + { + // Arrange + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(value: null); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + builder.Configure(options => options.AuthorizationCodeFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AuthorizationCode + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); + } + + [Fact] + public async Task DeserializeAuthorizationCode_ReturnsExpectedToken() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny())) + .ReturnsAsync(new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero)); + + instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) + .ReturnsAsync(new DateTimeOffset(2017, 01, 10, 00, 00, 00, TimeSpan.Zero)); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + + options.AuthorizationCodeFormat = format.Object; + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AuthorizationCode + }); + + // Assert + Assert.True((bool) response[OpenIdConnectConstants.Claims.Active]); + Assert.Equal("3E228451-1555-46F7-A471-951EFBA23A56", response[OpenIdConnectConstants.Claims.JwtId]); + Assert.Equal(1483228800, (long) response[OpenIdConnectConstants.Claims.IssuedAt]); + Assert.Equal(1484006400, (long) response[OpenIdConnectConstants.Claims.ExpiresAt]); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); + } + [Fact] public async Task DeserializeRefreshToken_ReturnsNullForMalformedReferenceToken() { @@ -864,7 +1048,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task DeserializeRefreshToken_RefreshTokenIsNotRetrievedFromDatabaseWhenReferenceTokensAreDisabled() + public async Task DeserializeRefreshToken_RefreshTokenIsNotRetrievedUsingHashWhenReferenceTokensAreDisabled() { // Arrange var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); @@ -876,7 +1060,6 @@ namespace OpenIddict.Tests OpenIdConnectServerDefaults.AuthenticationScheme); ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); var format = new Mock>(); @@ -890,6 +1073,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) .ReturnsAsync(true); }); @@ -934,7 +1120,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task DeserializeRefreshToken_ReturnsNullForMissingTokenIdentifier() + public async Task DeserializeRefreshToken_ReturnsNullForMissingReferenceTokenIdentifier() { // Arrange var token = new OpenIddictToken(); @@ -989,7 +1175,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task DeserializeRefreshToken_ReturnsNullForMissingTokenCiphertext() + public async Task DeserializeRefreshToken_ReturnsNullForMissingReferenceTokenCiphertext() { // Arrange var token = new OpenIddictToken(); @@ -1047,7 +1233,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task DeserializeRefreshToken_ReturnsNullForInvalidTokenCiphertext() + public async Task DeserializeRefreshToken_ReturnsNullForInvalidReferenceTokenCiphertext() { // Arrange var format = new Mock>(); @@ -1113,7 +1299,7 @@ namespace OpenIddict.Tests } [Fact] - public async Task DeserializeRefreshToken_ReturnsExpectedToken() + public async Task DeserializeRefreshToken_ReturnsExpectedReferenceToken() { // Arrange var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); @@ -1124,9 +1310,6 @@ namespace OpenIddict.Tests new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme); - ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); - ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); - var format = new Mock>(); format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) @@ -1209,6 +1392,192 @@ namespace OpenIddict.Tests format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); } + [Fact] + public async Task DeserializeRefreshToken_ReturnsNullForMissingTokenIdentifier() + { + // 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); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(ticket); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + } + + [Fact] + public async Task DeserializeRefreshToken_ReturnsNullForInvalidTokenCiphertext() + { + // Arrange + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(value: null); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); + } + + [Fact] + public async Task DeserializeRefreshToken_ReturnsExpectedToken() + { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny())) + .ReturnsAsync(new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero)); + + instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) + .ReturnsAsync(new DateTimeOffset(2017, 01, 10, 00, 00, 00, TimeSpan.Zero)); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + + options.RefreshTokenFormat = format.Object; + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken + }); + + // Assert + Assert.True((bool) response[OpenIdConnectConstants.Claims.Active]); + Assert.Equal("3E228451-1555-46F7-A471-951EFBA23A56", response[OpenIdConnectConstants.Claims.JwtId]); + Assert.Equal(1483228800, (long) response[OpenIdConnectConstants.Claims.IssuedAt]); + Assert.Equal(1484006400, (long) response[OpenIdConnectConstants.Claims.ExpiresAt]); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); + format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); + } + [Fact] public async Task SerializeAccessToken_AccessTokenIsNotPersistedWhenReferenceTokensAreDisabled() { @@ -1734,7 +2103,6 @@ namespace OpenIddict.Tests 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>(); @@ -1752,6 +2120,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) .ReturnsAsync(false); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.cs index 3c2935e6..27256a76 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.cs @@ -147,6 +147,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) .ReturnsAsync(false); @@ -212,6 +215,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) .ReturnsAsync(false); @@ -285,6 +291,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) .ReturnsAsync(false); @@ -400,6 +409,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) .ReturnsAsync(true); }); @@ -434,7 +446,7 @@ namespace OpenIddict.Tests }); // Assert - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Exactly(2)); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); } @@ -466,6 +478,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) .ReturnsAsync(true); @@ -506,7 +521,7 @@ namespace OpenIddict.Tests 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.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); } @@ -541,6 +556,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) .ReturnsAsync(false); @@ -569,7 +587,7 @@ namespace OpenIddict.Tests // 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.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); } @@ -604,6 +622,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) .ReturnsAsync(false); @@ -634,9 +655,9 @@ namespace OpenIddict.Tests // Assert Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); - Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); + Assert.Equal("The specified refresh token 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.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); } @@ -694,7 +715,7 @@ namespace OpenIddict.Tests // 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.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Never()); } @@ -733,6 +754,12 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) .ReturnsAsync(tokens[0]); + instance.Setup(mock => mock.GetIdAsync(tokens[0], It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + + instance.Setup(mock => mock.GetAuthorizationIdAsync(tokens[0], It.IsAny())) + .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) .ReturnsAsync(false); @@ -764,7 +791,7 @@ namespace OpenIddict.Tests // 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.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); 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()); } @@ -801,6 +828,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) .ReturnsAsync(tokens[0]); + instance.Setup(mock => mock.GetIdAsync(tokens[0], It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny())) .ReturnsAsync(false); @@ -830,7 +860,7 @@ namespace OpenIddict.Tests // 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.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); 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()); } @@ -863,6 +893,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) .ReturnsAsync(false); @@ -995,6 +1028,9 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) .ReturnsAsync(false);