From 53b38c93f307f1ebce511687b19df0e4d0acafbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sun, 23 Oct 2016 23:20:10 +0200 Subject: [PATCH] Add the OpenIddict endpoints tests --- .../OpenIddictProvider.Authentication.cs | 3 +- .../OpenIddictProvider.Discovery.cs | 9 +- .../OpenIddictProvider.Introspection.cs | 12 +- .../OpenIddictProvider.Session.cs | 3 +- .../Managers/OpenIddictApplicationManager.cs | 2 +- .../OpenIddictAuthorizationManager.cs | 2 +- .../Managers/OpenIddictScopeManager.cs | 2 +- .../Managers/OpenIddictTokenManager.cs | 2 +- test/OpenIddict.Core.Tests/Certificate.pfx | Bin 0 -> 2482 bytes .../OpenIddictProviderTests.Authentication.cs | 563 ++++++++++++++++++ .../OpenIddictProviderTests.Discovery.cs | 104 ++++ .../OpenIddictProviderTests.Exchange.cs | 552 +++++++++++++++++ .../OpenIddictProviderTests.Introspection.cs | 321 ++++++++++ .../OpenIddictProviderTests.Revocation.cs | 352 +++++++++++ .../OpenIddictProviderTests.Serialization.cs | 101 ++++ .../OpenIddictProviderTests.Session.cs | 112 ++++ .../OpenIddictProviderTests.Userinfo.cs | 45 ++ .../Infrastructure/OpenIddictProviderTests.cs | 150 +++++ test/OpenIddict.Core.Tests/Placeholder.cs | 3 - test/OpenIddict.Core.Tests/project.json | 7 +- 20 files changed, 2328 insertions(+), 17 deletions(-) create mode 100644 test/OpenIddict.Core.Tests/Certificate.pfx create mode 100644 test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Authentication.cs create mode 100644 test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Discovery.cs create mode 100644 test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Exchange.cs create mode 100644 test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Introspection.cs create mode 100644 test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Revocation.cs create mode 100644 test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Serialization.cs create mode 100644 test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Session.cs create mode 100644 test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Userinfo.cs create mode 100644 test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.cs delete mode 100644 test/OpenIddict.Core.Tests/Placeholder.cs diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs index e6585c7c..7fcdf177 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs @@ -338,7 +338,8 @@ namespace OpenIddict.Infrastructure { // Create a new authorization request containing only the request_id parameter. var address = QueryHelpers.AddQueryString( - uri: context.HttpContext.Request.PathBase + context.HttpContext.Request.Path, + uri: context.HttpContext.Request.Scheme + "://" + context.HttpContext.Request.Host + + context.HttpContext.Request.PathBase + context.HttpContext.Request.Path, name: OpenIdConnectConstants.Parameters.RequestId, value: context.Request.RequestId); context.HttpContext.Response.Redirect(address); diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Discovery.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Discovery.cs index fd688f41..ad94d459 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Discovery.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Discovery.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; namespace OpenIddict.Infrastructure { @@ -20,7 +21,8 @@ namespace OpenIddict.Infrastructure { // OpenIddict disallows the use of the unsecure code_challenge_method=plain method, // which must be manually removed from the code_challenge_methods_supported property. // See https://tools.ietf.org/html/rfc7636#section-7.2 for more information. - context.CodeChallengeMethods.Remove(OpenIdConnectConstants.CodeChallengeMethods.Plain); + context.CodeChallengeMethods.Clear(); + context.CodeChallengeMethods.Add(OpenIdConnectConstants.CodeChallengeMethods.Sha256); // Note: the OpenID Connect server middleware automatically populates grant_types_supported // by determining whether the authorization and token endpoints are enabled or not but @@ -39,8 +41,9 @@ namespace OpenIddict.Infrastructure { context.Scopes.Add(OpenIdConnectConstants.Scopes.Phone); context.Scopes.Add(OpenIddictConstants.Scopes.Roles); - // Only add the "offline_access" scope if "refresh_token" is listed as a supported grant type. - if (context.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken)) { + // Only add the "offline_access" scope if the refresh + // token flow is enabled in the OpenIddict options. + if (services.Options.IsRefreshTokenFlowEnabled()) { context.Scopes.Add(OpenIdConnectConstants.Scopes.OfflineAccess); } diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs index db4b0ba8..ee2543cd 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs @@ -16,9 +16,7 @@ using Microsoft.Extensions.Logging; namespace OpenIddict.Infrastructure { public partial class OpenIddictProvider : OpenIdConnectServerProvider where TApplication : class where TAuthorization : class where TScope : class where TToken : class { - public override async Task ValidateIntrospectionRequest([NotNull] ValidateIntrospectionRequestContext context) { - var services = context.HttpContext.RequestServices.GetRequiredService>(); - + public override Task ExtractIntrospectionRequest([NotNull] ExtractIntrospectionRequestContext context) { // Note: the OpenID Connect server middleware supports both GET and POST // introspection requests but OpenIddict only accepts POST requests. if (!string.Equals(context.HttpContext.Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { @@ -26,9 +24,15 @@ namespace OpenIddict.Infrastructure { error: OpenIdConnectConstants.Errors.InvalidRequest, description: "Introspection requests must use HTTP POST."); - return; + return Task.FromResult(0); } + return Task.FromResult(0); + } + + public override async Task ValidateIntrospectionRequest([NotNull] ValidateIntrospectionRequestContext context) { + var services = context.HttpContext.RequestServices.GetRequiredService>(); + // Note: the OpenID Connect server middleware supports unauthenticated introspection requests // but OpenIddict uses a stricter policy preventing unauthenticated/public applications // from using the introspection endpoint, as required by the specifications. diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs index 089f91ad..200ac26c 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs @@ -116,7 +116,8 @@ namespace OpenIddict.Infrastructure { // Create a new logout request containing only the request_id parameter. var address = QueryHelpers.AddQueryString( - uri: context.HttpContext.Request.PathBase + context.HttpContext.Request.Path, + uri: context.HttpContext.Request.Scheme + "://" + context.HttpContext.Request.Host + + context.HttpContext.Request.PathBase + context.HttpContext.Request.Path, name: OpenIdConnectConstants.Parameters.RequestId, value: context.Request.RequestId); context.HttpContext.Response.Redirect(address); diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index 3d61e09b..0df8a3a0 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -24,7 +24,7 @@ namespace OpenIddict { [NotNull] IServiceProvider services, [NotNull] IOpenIddictApplicationStore store, [NotNull] ILogger> logger) { - Context = services?.GetRequiredService()?.HttpContext; + Context = services?.GetService()?.HttpContext; Store = store; Logger = logger; } diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index 5ad6c81f..a6abced6 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -21,7 +21,7 @@ namespace OpenIddict { [NotNull] IServiceProvider services, [NotNull] IOpenIddictAuthorizationStore store, [NotNull] ILogger> logger) { - Context = services?.GetRequiredService()?.HttpContext; + Context = services?.GetService()?.HttpContext; Logger = logger; Store = store; } diff --git a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs index 75288bf8..ce748f66 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs @@ -21,7 +21,7 @@ namespace OpenIddict { [NotNull] IServiceProvider services, [NotNull] IOpenIddictScopeStore store, [NotNull] ILogger> logger) { - Context = services?.GetRequiredService()?.HttpContext; + Context = services?.GetService()?.HttpContext; Logger = logger; Store = store; } diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index a749b41d..3099f983 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -22,7 +22,7 @@ namespace OpenIddict { [NotNull] IServiceProvider services, [NotNull] IOpenIddictTokenStore store, [NotNull] ILogger> logger) { - Context = services?.GetRequiredService()?.HttpContext; + Context = services?.GetService()?.HttpContext; Logger = logger; Store = store; } diff --git a/test/OpenIddict.Core.Tests/Certificate.pfx b/test/OpenIddict.Core.Tests/Certificate.pfx new file mode 100644 index 0000000000000000000000000000000000000000..8c05f9d4d414d5036c38f34f8755b711d4f8811d GIT binary patch literal 2482 zcmY*adpOi-8~)9~jG=@X8WTe~hS6ZwsihX>Y>NC<>g0B7;H%2Ny|7*MT)LXl75H2CWrbxRg^g!5{B?SkmJ19R%Qs-hi}4MT4(21l<7_QTC; zhFRy@iAohZig$N!6`CqnNIX-OrcKMTbXD>*;`^6iMf9U}dzi`ET}#p0kiX zn_E1rxtYUy_&BM~tp_t8;H-q*<=GUVUmD{P%#_n9H?OTv{+@XM2xMHjW5uGKg=k~f z2amqit^(MKN6($FO@8qb&GXH6QDHUc+>>gj!9;Ki*6b?iJ(r zH$ZEI!bbNfD(YJuWL1T|K3soS0x?9m{LHl8*pM|f7COg9o59RbqmdSdK6}rtaRh9R zh{}vxI>PL>d)zjmv8lG`tf;if2r{wa$W8_EVo2ualvRc({_ry4P0H5&`!V`2R?x7n zECIoRGmhIk;mY^m$#nQpF|*J}ILgR|Dl*ACh6 zL!uIQ)(bJ6l*Wh8Uxz<1U+DP6qMhf7rZ3^W&FxmCsO=W3B3h(y1O7maO3mg8x!O4t zKh>*~vIt9#M3Jm3(tPFYugc5>Y8Tab566wYYRQ~>x{j^%@phz=B`d$Y94Vye7!1lm zzopGDR_vuVxEgH~4auUn9&@&8QJ0tb^4H8=1<);5biZd3-6yU|4q6N`N~-JIb>yM< zVktlMx6;*l+0X*qlbDdN-?+~l7>^DFYAZW?`HJ>)Z}YNMo*pE zNt~>i|Kvvc<-WJa%cdlR`>fD}F(Y~`{V>n}QjcNGzS-J#@`rY%vnDsh+@dO5en!(U zBuQnf(^`bUxn-`?1x6G7kq=!e>-s1wDH5P$Xw|O8MMP)D%|lPsblz)f_GG}M4?bft z#XHkP4<^QM~7@0#~Sb)_|tN`J76Lmi5c(cd#O9Qnt3${(~@3g)(|Ev(?M zyFX?&UT*HrAcuu!pB+n38XeYb6D#TdoQ{;|d}$5Ux4hGF_k?q+XwsA%srzbUd-ncY zHs{490^Vvv^0hSM$B5{EzDtyZCISwJ002OA`nQSEw8WbOdVmk`6QBjc6VL}i0GS>L zUyzYNMglnjI0HKL0WXkg0^T5WK_>~UI-sHpYQDrk1WFA7fkSAzVh~MtyenV>1b_i5 z5CVjQ@CD8Ta=CuvR={Zy(+J6mza>Mr6C7Vvxv6 z4lrB08stzB^NX~KaiDBm(~oLwK>5wBpDCpxp=^J2gt*jX%-HPSw7t&j8z~12-6E;F zuGMuD#gAP0;TvL4-+jalN_ZpJpl>Vd4O&>YtR2mghGh+@E(XL?>m@#BjV45z;K{sv z+E?{{8{VMDHV~k*>lYa%pY+7Ex8hEliD!J|>$%&BCm)83Ck^AZ9O6W7aOcl=M@HdFaVt)rR! zyK?k{iUuJENoGWb{Q(eu4`y(Tn+V3eoRcG(DE;G37Wt3uH!^aZI6Up0 zF7kD2SCx3NHcZ7k)zSI+l9YG0wo?wH7_!&t3!4a3&8iE~fbe2fhy zS$9U~H&Pkt&7&o*miw1n9P_^F$o!iz&-6P6=udX)0hsaC;;m`bydX)+;We?F-0{!x zH+}6=f3Gl5Tiz|K9*a|YT+57jPsq9A28Sa}H&!&n6hzuu`@Iy37LO1YrYWlj{5!9C zwtX#^$=Ix{w=0Qaa$Z;4CJs3FZSPTk9G+~FL^#}$O;E5OnIP&CWr;|HfSL#lii2a` uIJb^0)}Y-Z<4p2IzGW4c$JV97F}teVFIaxKqA_Or9lZuCV;KvZEdB|+V@&1% literal 0 HcmV?d00001 diff --git a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Authentication.cs b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Authentication.cs new file mode 100644 index 00000000..6cb262f5 --- /dev/null +++ b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Authentication.cs @@ -0,0 +1,563 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Bson; +using Xunit; + +namespace OpenIddict.Core.Tests.Infrastructure { + public partial class OpenIddictProviderTests { + [Fact] + public async Task ExtractAuthorizationRequest_UnsupportedRequestParameterIsRejected() { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + Request = "eyJhbGciOiJub25lIn0.eyJpc3MiOiJodHRwOi8vd3d3LmZhYnJpa2FtLmNvbSIsImF1ZCI6Imh" + + "0dHA6Ly93d3cuY29udG9zby5jb20iLCJyZXNwb25zZV90eXBlIjoiY29kZSIsImNsaWVudF9pZC" + + "I6IkZhYnJpa2FtIiwicmVkaXJlY3RfdXJpIjoiaHR0cDovL3d3dy5mYWJyaWthbS5jb20vcGF0aCJ9.", + ResponseType = OpenIdConnectConstants.ResponseTypes.Code, + Scope = OpenIdConnectConstants.Scopes.OpenId + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.RequestNotSupported, response.Error); + Assert.Equal("The request parameter is not supported.", response.ErrorDescription); + } + + [Fact] + public async Task ExtractAuthorizationRequest_UnsupportedRequestUriParameterIsRejected() { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + RequestUri = "http://www.fabrikam.com/request/GkurKxf5T0Y-mnPFCHqWOMiZi4VS138cQO_V7PZHAdM", + ResponseType = OpenIdConnectConstants.ResponseTypes.Code, + Scope = OpenIdConnectConstants.Scopes.OpenId + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.RequestUriNotSupported, response.Error); + Assert.Equal("The request_uri parameter is not supported.", response.ErrorDescription); + } + + [Fact] + public async Task ExtractAuthorizationRequest_InvalidRequestIdParameterIsRejected() { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7", + ResponseType = OpenIdConnectConstants.ResponseTypes.Code + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("Invalid request: timeout expired.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateAuthorizationRequest_UnknownResponseTypeParameterIsRejected() { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = "unknown_response_type" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.UnsupportedResponseType, response.Error); + Assert.Equal("The specified response_type parameter is not supported.", response.ErrorDescription); + } + + [Theory] + [InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode, "code")] + [InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode, "code id_token")] + [InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode, "code id_token token")] + [InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode, "code token")] + [InlineData(OpenIdConnectConstants.GrantTypes.Implicit, "code id_token")] + [InlineData(OpenIdConnectConstants.GrantTypes.Implicit, "code id_token token")] + [InlineData(OpenIdConnectConstants.GrantTypes.Implicit, "code token")] + [InlineData(OpenIdConnectConstants.GrantTypes.Implicit, "id_token")] + [InlineData(OpenIdConnectConstants.GrantTypes.Implicit, "id_token token")] + [InlineData(OpenIdConnectConstants.GrantTypes.Implicit, "token")] + public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenCorrespondingFlowIsDisabled(string flow, string type) { + // Arrange + var server = CreateAuthorizationServer(builder => { + builder.Configure(options => options.GrantTypes.Remove(flow)); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + Nonce = "n-0S6_WzA2Mj", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = type, + Scope = OpenIdConnectConstants.Scopes.OpenId + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.UnsupportedResponseType, response.Error); + Assert.Equal("The specified response_type parameter is not allowed.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateAuthorizationRequest_RequestWithOfflineAccessScopeIsRejectedWhenRefreshTokenFlowIsDisabled() { + // Arrange + var server = CreateAuthorizationServer(builder => { + builder.Configure(options => options.GrantTypes.Remove(OpenIdConnectConstants.GrantTypes.RefreshToken)); + }); + + 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, + Scope = OpenIdConnectConstants.Scopes.OfflineAccess + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("The 'offline_access' scope is not allowed.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateAuthorizationRequest_UnknownResponseModeParameterIsRejected() { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + ResponseMode = "unknown_response_mode", + ResponseType = OpenIdConnectConstants.ResponseTypes.Code + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("The specified response_mode parameter is not supported.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenRedirectUriIsMissing() { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + RedirectUri = null, + ResponseType = OpenIdConnectConstants.ResponseTypes.Code + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("The required redirect_uri parameter was missing.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenCodeChallengeMethodIsMissing() { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + CodeChallenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + CodeChallengeMethod = null, + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = OpenIdConnectConstants.ResponseTypes.Code + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("The 'code_challenge_method' parameter must be specified.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenCodeChallengeMethodIsPlain() { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + CodeChallenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + CodeChallengeMethod = OpenIdConnectConstants.CodeChallengeMethods.Plain, + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = OpenIdConnectConstants.ResponseTypes.Code + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("The specified response_type parameter is not allowed when using PKCE.", response.ErrorDescription); + } + + [Theory] + [InlineData("code id_token token")] + [InlineData("code token")] + public async Task ValidateAuthorizationRequest_CodeChallengeRequestWithForbiddenResponseTypeIsRejected(string type) { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + CodeChallenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + CodeChallengeMethod = OpenIdConnectConstants.CodeChallengeMethods.Sha256, + Nonce = "n-0S6_WzA2Mj", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = type, + Scope = OpenIdConnectConstants.Scopes.OpenId + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("The specified response_type parameter is not allowed when using PKCE.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenClientCannotBeFound() { + // Arrange + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(null); + }); + + var server = CreateAuthorizationServer(builder => { + 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.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error); + Assert.Equal("Application not found in the database: ensure that your client_id is correct.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam"), Times.Once()); + } + + [Fact] + public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenRedirectUriIsInvalid() { + // Arrange + var application = Mock.Of(); + + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path")) + .ReturnsAsync(false); + }); + + var server = CreateAuthorizationServer(builder => { + 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.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error); + Assert.Equal("Invalid redirect_uri.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path"), Times.Once()); + } + + [Theory] + [InlineData("code id_token token")] + [InlineData("code token")] + [InlineData("id_token token")] + [InlineData("token")] + public async Task ValidateAuthorizationRequest_ImplicitOrHybridRequestIsRejectedWhenClientIsConfidential(string type) { + // Arrange + var application = Mock.Of(); + + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path")) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + Nonce = "n-0S6_WzA2Mj", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = type, + Scope = OpenIdConnectConstants.Scopes.OpenId + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("Confidential clients are not allowed to retrieve " + + "an access token from the authorization endpoint.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application), Times.Once()); + } + + [Fact] + public async Task HandleAuthorizationRequest_RequestIsPersistedInDistributedCache() { + // Arrange + var cache = new Mock(); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = Mock.Of(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path")) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + builder.Services.AddSingleton(cache.Object); + }); + + 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.Token + }); + + var identifier = (string) response[OpenIdConnectConstants.Parameters.RequestId]; + + // Assert + Assert.Equal(1, response.Count()); + Assert.NotNull(identifier); + + cache.Verify(mock => mock.SetAsync( + OpenIddictConstants.Environment.AuthorizationRequest + identifier, + It.IsAny(), + It.IsAny()), Times.Once()); + } + + [Theory] + [InlineData("code")] + [InlineData("code id_token")] + [InlineData("code id_token token")] + [InlineData("code token")] + [InlineData("id_token")] + [InlineData("id_token token")] + [InlineData("token")] + public async Task HandleAuthorizationRequest_RequestsAreNotHandledLocally(string type) { + // Arrange + var request = new OpenIdConnectRequest { + ClientId = "Fabrikam", + Nonce = "n-0S6_WzA2Mj", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = type, + Scope = OpenIdConnectConstants.Scopes.OpenId + }; + + var stream = new MemoryStream(); + using (var writer = new BsonWriter(stream)) { + writer.CloseOutput = false; + + var serializer = JsonSerializer.CreateDefault(); + serializer.Serialize(writer, request); + } + + var cache = new Mock(); + + cache.Setup(mock => mock.GetAsync(OpenIddictConstants.Environment.AuthorizationRequest + + "b2ee7815-5579-4ff7-86b0-ba671b939d96")) + .ReturnsAsync(stream.ToArray()); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = Mock.Of(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path")) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + builder.Services.AddSingleton(CreateTokenManager(instance => { + instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode)) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + })); + + builder.Services.AddSingleton(cache.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + RequestId = "b2ee7815-5579-4ff7-86b0-ba671b939d96" + }); + + // Assert + Assert.True(!string.IsNullOrEmpty(response.AccessToken) || + !string.IsNullOrEmpty(response.Code) || + !string.IsNullOrEmpty(response.IdToken)); + } + + [Fact] + public async Task ApplyAuthorizationResponse_RequestIsRemovedFromDistributedCache() { + // Arrange + var request = new OpenIdConnectRequest { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = OpenIdConnectConstants.ResponseTypes.Token + }; + + var stream = new MemoryStream(); + using (var writer = new BsonWriter(stream)) { + writer.CloseOutput = false; + + var serializer = JsonSerializer.CreateDefault(); + serializer.Serialize(writer, request); + } + + var cache = new Mock(); + + cache.Setup(mock => mock.GetAsync(OpenIddictConstants.Environment.AuthorizationRequest + + "b2ee7815-5579-4ff7-86b0-ba671b939d96")) + .ReturnsAsync(stream.ToArray()); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = Mock.Of(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path")) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + builder.Services.AddSingleton(cache.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + RequestId = "b2ee7815-5579-4ff7-86b0-ba671b939d96" + }); + + // Assert + Assert.NotNull(response.AccessToken); + + cache.Verify(mock => mock.RemoveAsync( + OpenIddictConstants.Environment.AuthorizationRequest + + "b2ee7815-5579-4ff7-86b0-ba671b939d96"), Times.Once()); + } + + [Fact] + public async Task ApplyAuthorizationResponse_ErroredRequestIsNotHandledLocallyWhenStatusCodeMiddlewareIsEnabled() { + // Arrange + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = Mock.Of(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path")) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + builder.EnableAuthorizationEndpoint("/authorize-status-code-middleware"); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync("/authorize-status-code-middleware", new OpenIdConnectRequest { + ClientId = null, + RedirectUri = null, + ResponseType = null + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, (string) response["error_custom"]); + } + } +} diff --git a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Discovery.cs b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Discovery.cs new file mode 100644 index 00000000..80c21f89 --- /dev/null +++ b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Discovery.cs @@ -0,0 +1,104 @@ +using System.Linq; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using Xunit; + +namespace OpenIddict.Core.Tests.Infrastructure { + public partial class OpenIddictProviderTests { + [Fact] + public async Task HandleConfigurationRequest_PlainCodeChallengeMethodIsNotReturned() { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.GetAsync(ConfigurationEndpoint); + + // Assert + Assert.DoesNotContain( + OpenIdConnectConstants.CodeChallengeMethods.Plain, + response[OpenIdConnectConstants.Metadata.CodeChallengeMethodsSupported].Values()); + } + + [Theory] + [InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode)] + [InlineData(OpenIdConnectConstants.GrantTypes.ClientCredentials)] + [InlineData(OpenIdConnectConstants.GrantTypes.Implicit)] + [InlineData(OpenIdConnectConstants.GrantTypes.Password)] + [InlineData(OpenIdConnectConstants.GrantTypes.RefreshToken)] + public async Task HandleConfigurationRequest_EnabledFlowsAreReturned(string flow) { + // Arrange + var server = CreateAuthorizationServer(builder => { + builder.Configure(options => { + options.GrantTypes.Clear(); + options.GrantTypes.Add(flow); + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.GetAsync(ConfigurationEndpoint); + var types = response[OpenIdConnectConstants.Metadata.GrantTypesSupported].Values(); + + // Assert + Assert.Equal(1, types.Count()); + Assert.Contains(flow, types); + } + + [Theory] + [InlineData(OpenIdConnectConstants.Scopes.Profile)] + [InlineData(OpenIdConnectConstants.Scopes.Email)] + [InlineData(OpenIdConnectConstants.Scopes.Phone)] + [InlineData(OpenIddictConstants.Scopes.Roles)] + public async Task HandleConfigurationRequest_StandardScopesAreExposed(string scope) { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.GetAsync(ConfigurationEndpoint); + + // Assert + Assert.Contains(scope, response[OpenIdConnectConstants.Metadata.ScopesSupported].Values()); + } + + [Fact] + public async Task HandleConfigurationRequest_OfflineAccessScopeIsReturnedWhenRefreshTokenFlowIsEnabled() { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.GetAsync(ConfigurationEndpoint); + + // Assert + Assert.Contains(OpenIdConnectConstants.Scopes.OfflineAccess, + response[OpenIdConnectConstants.Metadata.ScopesSupported].Values()); + } + + [Fact] + public async Task HandleConfigurationRequest_OfflineAccessScopeIsReturnedWhenRefreshTokenFlowIsDisabled() { + // Arrange + var server = CreateAuthorizationServer(builder => { + builder.Configure(options => { + // Note: at least one flow must be enabled. + options.GrantTypes.Clear(); + options.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.AuthorizationCode); + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.GetAsync(ConfigurationEndpoint); + + // Assert + Assert.DoesNotContain(OpenIdConnectConstants.Scopes.OfflineAccess, + response[OpenIdConnectConstants.Metadata.ScopesSupported].Values()); + } + } +} diff --git a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Exchange.cs b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Exchange.cs new file mode 100644 index 00000000..27a13a5f --- /dev/null +++ b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Exchange.cs @@ -0,0 +1,552 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using AspNet.Security.OpenIdConnect.Server; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace OpenIddict.Core.Tests.Infrastructure { + public partial class OpenIddictProviderTests { + [Theory] + [InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode)] + [InlineData(OpenIdConnectConstants.GrantTypes.ClientCredentials)] + [InlineData(OpenIdConnectConstants.GrantTypes.Password)] + [InlineData(OpenIdConnectConstants.GrantTypes.RefreshToken)] + public async Task ValidateTokenRequest_RequestIsRejectedWhenFlowIsNotEnabled(string flow) { + // Arrange + var server = CreateAuthorizationServer(builder => { + builder.Configure(options => options.GrantTypes.Remove(flow)); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = flow, + Username = "johndoe", + Password = "A3ddj3w", + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.UnsupportedGrantType, response.Error); + Assert.Equal("The specified grant_type is not supported by this authorization server.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_RequestWithOfflineAccessScopeIsRejectedWhenRefreshTokenFlowIsDisabled() { + // Arrange + var server = CreateAuthorizationServer(builder => { + builder.Configure(options => options.GrantTypes.Remove(OpenIdConnectConstants.GrantTypes.RefreshToken)); + }); + + 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 + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("The 'offline_access' scope is not allowed.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_ClientCredentialsRequestWithOfflineAccessScopeIsRejected() { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + GrantType = OpenIdConnectConstants.GrantTypes.ClientCredentials, + Scope = OpenIdConnectConstants.Scopes.OfflineAccess + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("The 'offline_access' scope is not allowed when using grant_type=client_credentials.", response.ErrorDescription); + } + + [Theory] + [InlineData("client_id", "")] + [InlineData("", "client_secret")] + public async Task ValidateTokenRequest_ClientCredentialsRequestIsRejectedWhenCredentialsAreMissing(string identifier, string secret) { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + ClientId = identifier, + ClientSecret = secret, + GrantType = OpenIdConnectConstants.GrantTypes.ClientCredentials + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("Client applications must be authenticated to use the client credentials grant.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_RequestWithoutClientIdIsRejectedWhenClientIdentificationIsRequired() { + // Arrange + var server = CreateAuthorizationServer(builder => builder.RequireClientIdentification()); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + ClientId = null, + GrantType = OpenIdConnectConstants.GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("The mandatory 'client_id' parameter was missing.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_RequestIsRejectedWhenClientCannotBeFound() { + // Arrange + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(null); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + GrantType = OpenIdConnectConstants.GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error); + Assert.Equal("Application not found in the database: ensure that your client_id is correct.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam"), Times.Once()); + } + + [Fact] + public async Task ValidateTokenRequest_ClientCredentialsRequestFromPublicClientIsRejected() { + // Arrange + var application = Mock.Of(); + + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + GrantType = OpenIdConnectConstants.GrantTypes.ClientCredentials + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.UnauthorizedClient, response.Error); + Assert.Equal("Public clients are not allowed to use the client credentials grant.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application), Times.Once()); + } + + [Fact] + public async Task ValidateTokenRequest_ClientSecretCannotBeUsedByPublicClients() { + // Arrange + var application = Mock.Of(); + + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + GrantType = OpenIdConnectConstants.GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("Public clients are not allowed to send a client_secret.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application), Times.Once()); + } + + [Fact] + public async Task ValidateTokenRequest_ClientSecretIsRequiredForConfidentialClients() { + // Arrange + var application = Mock.Of(); + + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + ClientSecret = null, + GrantType = OpenIdConnectConstants.GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error); + Assert.Equal("Missing credentials: ensure that you specified a client_secret.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application), Times.Once()); + } + + [Fact] + public async Task ValidateTokenRequest_RequestIsRejectedWhenClientCredentialsAreInvalid() { + // Arrange + var application = Mock.Of(); + + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + + instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw")) + .ReturnsAsync(false); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + GrantType = OpenIdConnectConstants.GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error); + Assert.Equal("Invalid credentials: ensure that you specified a correct client_secret.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application), Times.Once()); + Mock.Get(manager).Verify(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw"), Times.Once()); + } + + [Fact] + public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsExpired() { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetPresenters("Fabrikam"); + ticket.SetTicketId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetUsage(OpenIdConnectConstants.Usages.AuthorizationCode); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) + .Returns(ticket); + + var manager = CreateTokenManager(instance => { + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56")) + .ReturnsAsync(null); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = Mock.Of(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .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 + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The authorization code is no longer valid.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56"), Times.Once()); + } + + [Fact] + public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsExpired() { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTicketId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetUsage(OpenIdConnectConstants.Usages.RefreshToken); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var manager = CreateTokenManager(instance => { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103")) + .ReturnsAsync(null); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = Mock.Of(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The refresh token is no longer valid.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103"), Times.Once()); + } + + [Fact] + public async Task HandleTokenRequest_AuthorizationCodeIsAutomaticallyRevoked() { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(ClaimTypes.NameIdentifier, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetPresenters("Fabrikam"); + ticket.SetTicketId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetUsage(OpenIdConnectConstants.Usages.AuthorizationCode); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) + .Returns(ticket); + + var token = Mock.Of(); + + var manager = CreateTokenManager(instance => { + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56")) + .ReturnsAsync(token); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = Mock.Of(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .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 + }); + + // Assert + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.RevokeAsync(token), Times.Once()); + } + + [Fact] + public async Task HandleTokenRequest_RefreshTokenIsAutomaticallyRevokedWhenSlidingExpirationIsEnabled() { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(ClaimTypes.NameIdentifier, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTicketId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetUsage(OpenIdConnectConstants.Usages.RefreshToken); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = Mock.Of(); + + var manager = CreateTokenManager(instance => { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103")) + .ReturnsAsync(token); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = Mock.Of(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.RevokeAsync(token), Times.Once()); + } + + [Theory] + [InlineData(OpenIdConnectConstants.GrantTypes.ClientCredentials)] + [InlineData(OpenIdConnectConstants.GrantTypes.Password)] + [InlineData("urn:ietf:params:oauth:grant-type:custom_grant")] + public async Task HandleTokenRequest_RequestsAreNotHandledLocally(string flow) { + // Arrange + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = Mock.Of(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + + instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw")) + .ReturnsAsync(true); + })); + + builder.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant"); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + GrantType = flow, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.NotNull(response.AccessToken); + } + } +} diff --git a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Introspection.cs b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Introspection.cs new file mode 100644 index 00000000..b626c398 --- /dev/null +++ b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Introspection.cs @@ -0,0 +1,321 @@ +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using AspNet.Security.OpenIdConnect.Server; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace OpenIddict.Core.Tests.Infrastructure { + public partial class OpenIddictProviderTests { + [Fact] + public async Task ExtractIntrospectionRequest_GetRequestsAreRejected() { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.GetAsync(IntrospectionEndpoint, new OpenIdConnectRequest { + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("Introspection requests must use HTTP POST.", response.ErrorDescription); + } + + [Theory] + [InlineData("client_id", "")] + [InlineData("", "client_secret")] + public async Task ValidateIntrospectionRequest_ClientCredentialsRequestIsRejectedWhenCredentialsAreMissing(string identifier, string secret) { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest { + ClientId = identifier, + ClientSecret = secret, + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("Clients must be authenticated to use the introspection endpoint.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientCannotBeFound() { + // Arrange + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(null); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error); + Assert.Equal("Application not found in the database: ensure that your client_id is correct.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam"), Times.Once()); + } + + [Fact] + public async Task ValidateIntrospectionRequest_RequestsSentByPublicClientsAreRejected() { + // Arrange + var application = Mock.Of(); + + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error); + Assert.Equal("Public applications are not allowed to use the introspection endpoint.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application), Times.Once()); + } + + [Fact] + public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientCredentialsAreInvalid() { + // Arrange + var application = Mock.Of(); + + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + + instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw")) + .ReturnsAsync(false); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error); + Assert.Equal("Invalid credentials: ensure that you specified a correct client_secret.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application), Times.Once()); + Mock.Get(manager).Verify(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw"), Times.Once()); + } + + [Fact] + public async Task HandleIntrospectionRequest_RequestIsRejectedWhenClientIsNotAValidAudience() { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(ClaimTypes.NameIdentifier, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetAudiences("Contoso"); + ticket.SetTicketId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetUsage(OpenIdConnectConstants.Usages.AccessToken); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(ticket); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = Mock.Of(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + + instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw")) + .ReturnsAsync(true); + })); + + builder.Configure(options => options.AccessTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(1, response.Count()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + } + + [Fact] + public async Task HandleIntrospectionRequest_RequestIsRejectedWhenAuthorizationCodeIsRevoked() { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(ClaimTypes.NameIdentifier, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTicketId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetUsage(OpenIdConnectConstants.Usages.AuthorizationCode); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(ticket); + + var manager = CreateTokenManager(instance => { + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56")) + .ReturnsAsync(null); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = Mock.Of(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + + instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw")) + .ReturnsAsync(true); + })); + + builder.Services.AddSingleton(manager); + + 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" + }); + + // Assert + Assert.Equal(1, response.Count()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56"), Times.Once()); + } + + [Fact] + public async Task HandleIntrospectionRequest_RequestIsRejectedWhenRefreshTokenIsRevoked() { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(ClaimTypes.NameIdentifier, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTicketId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetUsage(OpenIdConnectConstants.Usages.RefreshToken); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(ticket); + + var manager = CreateTokenManager(instance => { + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56")) + .ReturnsAsync(null); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = Mock.Of(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + + instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw")) + .ReturnsAsync(true); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(1, response.Count()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56"), Times.Once()); + } + } +} diff --git a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Revocation.cs b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Revocation.cs new file mode 100644 index 00000000..965c69b9 --- /dev/null +++ b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Revocation.cs @@ -0,0 +1,352 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using AspNet.Security.OpenIdConnect.Server; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using Moq; +using Xunit; + +namespace OpenIddict.Core.Tests.Infrastructure { + public partial class OpenIddictProviderTests { + [Theory] + [InlineData(OpenIdConnectConstants.TokenTypeHints.AccessToken)] + [InlineData(OpenIdConnectConstants.TokenTypeHints.IdToken)] + public async Task ValidateRevocationRequest_UnknownTokenTokenHintIsRejected(string hint) { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest { + Token = "SlAV32hkKG", + TokenTypeHint = hint + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.UnsupportedTokenType, response.Error); + Assert.Equal("Only authorization codes and refresh tokens can be revoked. When specifying a token_type_hint " + + "parameter, its value must be equal to 'authorization_code' or 'refresh_token'.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateRevocationRequest_RequestWithoutClientIdIsRejectedWhenClientIdentificationIsRequired() { + // Arrange + var server = CreateAuthorizationServer(builder => builder.RequireClientIdentification()); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest { + Token = "SlAV32hkKG", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("The mandatory 'client_id' parameter was missing.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateRevocationRequest_RequestIsRejectedWhenClientCannotBeFound() { + // Arrange + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(null); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + Token = "SlAV32hkKG", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error); + Assert.Equal("Application not found in the database: ensure that your client_id is correct.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam"), Times.Once()); + } + + [Fact] + public async Task ValidateRevocationRequest_ClientSecretCannotBeUsedByPublicClients() { + // Arrange + var application = Mock.Of(); + + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "SlAV32hkKG", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("Public clients are not allowed to send a client_secret.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application), Times.Once()); + } + + [Fact] + public async Task ValidateRevocationRequest_ClientSecretIsRequiredForConfidentialClients() { + // Arrange + var application = Mock.Of(); + + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + ClientSecret = null, + Token = "SlAV32hkKG", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error); + Assert.Equal("Missing credentials: ensure that you specified a client_secret.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application), Times.Once()); + } + + [Fact] + public async Task ValidateRevocationRequest_RequestIsRejectedWhenClientCredentialsAreInvalid() { + // Arrange + var application = Mock.Of(); + + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + + instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw")) + .ReturnsAsync(false); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "SlAV32hkKG", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.RefreshToken + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error); + Assert.Equal("Invalid credentials: ensure that you specified a correct client_secret.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application), Times.Once()); + Mock.Get(manager).Verify(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw"), Times.Once()); + } + + [Fact] + public async Task HandleRevocationRequest_RequestIsRejectedWhenTokenIsAnAccessToken() { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTicketId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetUsage(OpenIdConnectConstants.Usages.AccessToken); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("SlAV32hkKG")) + .Returns(ticket); + + var server = CreateAuthorizationServer(builder => { + builder.Configure(options => options.AccessTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest { + Token = "SlAV32hkKG" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.UnsupportedTokenType, response.Error); + Assert.Equal("Only authorization codes and refresh tokens can be revoked.", response.ErrorDescription); + + format.Verify(mock => mock.Unprotect("SlAV32hkKG"), Times.Once()); + } + + [Fact] + public async Task HandleRevocationRequest_RequestIsNotRejectedWhenTokenIsAnIdentityToken() { + // Arrange + var token = Mock.Of(mock => + mock.ValidFrom == DateTime.UtcNow.AddDays(-1) && + mock.ValidTo == DateTime.UtcNow.AddDays(1)); + + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Usage, OpenIdConnectConstants.Usages.IdentityToken); + + var handler = new Mock(); + + handler.Setup(mock => mock.CanReadToken("SlAV32hkKG")) + .Returns(true); + + handler.As() + .Setup(mock => mock.ValidateToken("SlAV32hkKG", It.IsAny(), out token)) + .Returns(new ClaimsPrincipal(identity)); + + var server = CreateAuthorizationServer(builder => { + builder.Configure(options => options.IdentityTokenHandler = handler.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest { + Token = "SlAV32hkKG" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.UnsupportedTokenType, response.Error); + Assert.Equal("Only authorization codes and refresh tokens can be revoked.", response.ErrorDescription); + + handler.As() + .Verify(mock => mock.CanReadToken("SlAV32hkKG"), Times.Once()); + + handler.As() + .Verify(mock => mock.ValidateToken("SlAV32hkKG", It.IsAny(), out token), Times.Once()); + } + + [Fact] + public async Task HandleRevocationRequest_TokenIsNotRevokedWhenItIsAlreadyInvalid() { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTicketId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetUsage(OpenIdConnectConstants.Usages.RefreshToken); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("SlAV32hkKG")) + .Returns(ticket); + + var manager = CreateTokenManager(instance => { + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56")) + .ReturnsAsync(null); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.AccessTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest { + Token = "SlAV32hkKG" + }); + + // Assert + Assert.Equal(0, response.Count()); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.RevokeAsync(It.IsAny()), Times.Never()); + } + + [Fact] + public async Task HandleRevocationRequest_TokenIsSuccessfullyRevoked() { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTicketId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetUsage(OpenIdConnectConstants.Usages.RefreshToken); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("SlAV32hkKG")) + .Returns(ticket); + + var token = Mock.Of(); + + var manager = CreateTokenManager(instance => { + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56")) + .ReturnsAsync(token); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.AccessTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(RevocationEndpoint, new OpenIdConnectRequest { + Token = "SlAV32hkKG" + }); + + // Assert + Assert.Equal(0, response.Count()); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56"), Times.Once()); + Mock.Get(manager).Verify(mock => mock.RevokeAsync(token), Times.Once()); + } + } +} diff --git a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Serialization.cs b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Serialization.cs new file mode 100644 index 00000000..bbe0c702 --- /dev/null +++ b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Serialization.cs @@ -0,0 +1,101 @@ +using System.IO; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Bson; +using Xunit; + +namespace OpenIddict.Core.Tests.Infrastructure { + public partial class OpenIddictProviderTests { + [Fact] + public async Task SerializeAuthorizationCode_AuthorizationCodeIsAutomaticallyPersisted() { + // Arrange + var request = new OpenIdConnectRequest { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = OpenIdConnectConstants.ResponseTypes.Code + }; + + var stream = new MemoryStream(); + using (var writer = new BsonWriter(stream)) { + writer.CloseOutput = false; + + var serializer = JsonSerializer.CreateDefault(); + serializer.Serialize(writer, request); + } + + var cache = new Mock(); + + cache.Setup(mock => mock.GetAsync(OpenIddictConstants.Environment.AuthorizationRequest + + "b2ee7815-5579-4ff7-86b0-ba671b939d96")) + .ReturnsAsync(stream.ToArray()); + + var manager = CreateTokenManager(instance => { + instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode)) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = Mock.Of(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam")) + .ReturnsAsync(application); + + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path")) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application)) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + builder.Services.AddSingleton(manager); + + builder.Services.AddSingleton(cache.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + RequestId = "b2ee7815-5579-4ff7-86b0-ba671b939d96" + }); + + // Assert + Assert.NotNull(response.Code); + + Mock.Get(manager).Verify(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode), Times.Once()); + } + + [Fact] + public async Task SerializeRefreshToken_RefreshTokenIsAutomaticallyPersisted() { + // Arrange + var manager = CreateTokenManager(instance => { + instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken)) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + 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 + }); + + // Assert + Assert.NotNull(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken), Times.Once()); + } + } +} diff --git a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Session.cs b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Session.cs new file mode 100644 index 00000000..62085483 --- /dev/null +++ b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Session.cs @@ -0,0 +1,112 @@ +using System.Linq; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace OpenIddict.Core.Tests.Infrastructure { + public partial class OpenIddictProviderTests { + [Fact] + public async Task ExtractLogoutRequest_InvalidRequestIdParameterIsRejected() { + // Arrange + var server = CreateAuthorizationServer(); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest { + RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("Invalid request: timeout expired.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateLogoutRequest_RequestIsRejectedWhenRedirectUriIsInvalid() { + // Arrange + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByLogoutRedirectUri("http://www.fabrikam.com/path")) + .ReturnsAsync(null); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest { + PostLogoutRedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error); + Assert.Equal("Invalid post_logout_redirect_uri.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByLogoutRedirectUri("http://www.fabrikam.com/path"), Times.Once()); + } + + [Fact] + public async Task HandleLogoutRequest_RequestIsPersistedInDistributedCache() { + // Arrange + var cache = new Mock(); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = Mock.Of(); + + instance.Setup(mock => mock.FindByLogoutRedirectUri("http://www.fabrikam.com/path")) + .ReturnsAsync(application); + })); + + builder.Services.AddSingleton(cache.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest { + PostLogoutRedirectUri = "http://www.fabrikam.com/path" + }); + + var identifier = (string) response[OpenIdConnectConstants.Parameters.RequestId]; + + // Assert + Assert.Equal(1, response.Count()); + Assert.NotNull(identifier); + + cache.Verify(mock => mock.SetAsync( + OpenIddictConstants.Environment.LogoutRequest + identifier, + It.IsAny(), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ApplyLogoutResponse_ErroredRequestIsNotHandledLocallyWhenStatusCodeMiddlewareIsEnabled() { + // Arrange + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByLogoutRedirectUri("http://www.fabrikam.com/path")) + .ReturnsAsync(null); + })); + + builder.EnableAuthorizationEndpoint("/logout-status-code-middleware"); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync("/logout-status-code-middleware", new OpenIdConnectRequest { + PostLogoutRedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, (string) response["error_custom"]); + } + } +} diff --git a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Userinfo.cs b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Userinfo.cs new file mode 100644 index 00000000..a384a07d --- /dev/null +++ b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Userinfo.cs @@ -0,0 +1,45 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using AspNet.Security.OpenIdConnect.Server; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http.Authentication; +using Moq; +using Xunit; + +namespace OpenIddict.Core.Tests.Infrastructure { + public partial class OpenIddictProviderTests { + [Fact] + public async Task HandleUserinfoRequest_RequestIsHandledByUserCode() { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(ClaimTypes.NameIdentifier, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("SlAV32hkKG")) + .Returns(ticket); + + var server = CreateAuthorizationServer(builder => { + builder.Configure(options => options.AccessTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(UserinfoEndpoint, new OpenIdConnectRequest { + AccessToken = "SlAV32hkKG" + }); + + // Assert + Assert.Equal("Bob le Bricoleur", (string) response[OpenIdConnectConstants.Claims.Subject]); + + format.Verify(mock => mock.Unprotect("SlAV32hkKG"), Times.Once()); + } + } +} diff --git a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.cs b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.cs new file mode 100644 index 00000000..7567df89 --- /dev/null +++ b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.cs @@ -0,0 +1,150 @@ +using System; +using System.Reflection; +using System.Security.Claims; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using AspNet.Security.OpenIdConnect.Server; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using Moq; +using Newtonsoft.Json; + +namespace OpenIddict.Core.Tests.Infrastructure { + public partial class OpenIddictProviderTests { + public const string AuthorizationEndpoint = "/connect/authorize"; + public const string ConfigurationEndpoint = "/.well-known/openid-configuration"; + public const string IntrospectionEndpoint = "/connect/introspect"; + public const string LogoutEndpoint = "/connect/logout"; + public const string RevocationEndpoint = "/connect/revoke"; + public const string TokenEndpoint = "/connect/token"; + public const string UserinfoEndpoint = "/connect/userinfo"; + + private static TestServer CreateAuthorizationServer(Action configuration = null) { + var builder = new WebHostBuilder(); + + builder.UseEnvironment("Testing"); + + builder.ConfigureLogging(options => options.AddDebug()); + + builder.ConfigureServices(services => { + var instance = services.AddOpenIddict() + // Disable the transport security requirement during testing. + .DisableHttpsRequirement() + + // Enable the tested endpoints. + .EnableAuthorizationEndpoint(AuthorizationEndpoint) + .EnableIntrospectionEndpoint(IntrospectionEndpoint) + .EnableLogoutEndpoint(LogoutEndpoint) + .EnableRevocationEndpoint(RevocationEndpoint) + .EnableTokenEndpoint(TokenEndpoint) + .EnableUserinfoEndpoint(UserinfoEndpoint) + + // Enable the tested flows. + .AllowAuthorizationCodeFlow() + .AllowClientCredentialsFlow() + .AllowImplicitFlow() + .AllowPasswordFlow() + .AllowRefreshTokenFlow() + + // Register the X.509 certificate used to sign the identity tokens. + .AddSigningCertificate( + assembly: typeof(OpenIddictProviderTests).GetTypeInfo().Assembly, + resource: "OpenIddict.Core.Tests.Certificate.pfx", + password: "OpenIddict") + + // Note: overriding the default data protection provider is not necessary for the tests to pass, + // but is useful to ensure unnecessary keys are not persisted in testing environments, which also + // helps make the unit tests run faster, as no registry or disk access is required in this case. + .UseDataProtectionProvider(new EphemeralDataProtectionProvider()); + + // Run the configuration delegate + // registered by the unit tests. + configuration?.Invoke(instance); + }); + + builder.Configure(app => { + app.UseStatusCodePages(context => { + context.HttpContext.Response.Headers[HeaderNames.ContentType] = "application/json"; + + return context.HttpContext.Response.WriteAsync(JsonConvert.SerializeObject(new { + error_custom = OpenIdConnectConstants.Errors.InvalidRequest + })); + }); + + app.Use(next => context => { + if (context.Request.Path != "/authorize-status-code-middleware" && + context.Request.Path != "/logout-status-code-middleware") { + var feature = context.Features.Get(); + feature.Enabled = false; + } + + return next(context); + }); + + app.UseOpenIddict(); + + app.Run(context => { + if (context.Request.Path == AuthorizationEndpoint || + context.Request.Path == TokenEndpoint) { + var request = context.GetOpenIdConnectRequest(); + + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(ClaimTypes.NameIdentifier, "Bob le Magnifique"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetScopes(request.GetScopes()); + + return context.Authentication.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties); + } + + else if (context.Request.Path == UserinfoEndpoint) { + context.Response.Headers[HeaderNames.ContentType] = "application/json"; + + return context.Response.WriteAsync(JsonConvert.SerializeObject(new { + sub = "Bob le Bricoleur" + })); + } + + return Task.FromResult(0); + }); + }); + + return new TestServer(builder); + } + + private static OpenIddictApplicationManager CreateApplicationManager(Action>> setup = null) { + var manager = new Mock>( + Mock.Of(), + Mock.Of>(), + Mock.Of>>()); + + setup?.Invoke(manager); + + return manager.Object; + } + + private static OpenIddictTokenManager CreateTokenManager(Action>> setup = null) { + var manager = new Mock>( + Mock.Of(), + Mock.Of>(), + Mock.Of>>()); + + setup?.Invoke(manager); + + return manager.Object; + } + } +} diff --git a/test/OpenIddict.Core.Tests/Placeholder.cs b/test/OpenIddict.Core.Tests/Placeholder.cs deleted file mode 100644 index 7aacac8a..00000000 --- a/test/OpenIddict.Core.Tests/Placeholder.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace OpenIddict.Core.Tests { - public class Placeholder { } -} diff --git a/test/OpenIddict.Core.Tests/project.json b/test/OpenIddict.Core.Tests/project.json index ea3e3ebb..7fcc2490 100644 --- a/test/OpenIddict.Core.Tests/project.json +++ b/test/OpenIddict.Core.Tests/project.json @@ -1,10 +1,15 @@ { "buildOptions": { - "warningsAsErrors": true + "warningsAsErrors": true, + + "embed": { + "include": [ "Certificate.pfx" ] + } }, "dependencies": { "dotnet-test-xunit": "2.2.0-preview2-build1029", + "Microsoft.AspNetCore.Diagnostics": "1.0.0", "Microsoft.AspNetCore.TestHost": "1.0.0", "Microsoft.Extensions.Caching.Memory": "1.0.0", "Microsoft.Extensions.Logging.Debug": "1.0.0",