From 6d1ce096904e188da8538bea0d7d7280930ff600 Mon Sep 17 00:00:00 2001 From: tuath Date: Thu, 25 Jan 2018 14:29:14 -0500 Subject: [PATCH 1/4] Remove mentions of the nightly builds feed from README.md --- README.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/README.md b/README.md index 04417933..dad08aa1 100644 --- a/README.md +++ b/README.md @@ -47,18 +47,6 @@ To use OpenIddict, you need to: - **Have an existing project or create a new one**: when creating a new project using Visual Studio's default ASP.NET Core template, using **individual user accounts authentication** is strongly recommended. When updating an existing project, you must provide your own `AccountController` to handle the registration process and the authentication flow. - - **Add the appropriate MyGet repositories to your NuGet sources**. This can be done by adding a new `NuGet.Config` file at the root of your solution: - -```xml - - - - - - - -``` - - **Update your `.csproj` file** to reference `AspNet.Security.OAuth.Validation` and the `OpenIddict` packages: ```xml From fb5d8c9351210d632c14fc2d95b5a3e0fc7eaa37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 29 Jan 2018 00:05:10 +0100 Subject: [PATCH 2/4] Introduce OpenIddictApplication/Authorization/Scope/Token.Properties as a way to store untyped properties in the database --- build/dependencies.props | 1 + .../Managers/OpenIddictTokenManager.cs | 28 +++++----- src/OpenIddict.Core/OpenIddict.Core.csproj | 1 + .../Stores/IOpenIddictApplicationStore.cs | 23 ++++++++ .../Stores/IOpenIddictAuthorizationStore.cs | 23 ++++++++ .../Stores/IOpenIddictScopeStore.cs | 23 ++++++++ .../Stores/IOpenIddictTokenStore.cs | 23 ++++++++ .../Stores/OpenIddictApplicationStore.cs | 54 +++++++++++++++++++ .../Stores/OpenIddictAuthorizationStore.cs | 54 +++++++++++++++++++ .../Stores/OpenIddictScopeStore.cs | 54 +++++++++++++++++++ .../Stores/OpenIddictTokenStore.cs | 54 +++++++++++++++++++ .../OpenIddictApplication.cs | 6 +++ .../OpenIddictAuthorization.cs | 6 +++ src/OpenIddict.Models/OpenIddictScope.cs | 6 +++ src/OpenIddict.Models/OpenIddictToken.cs | 6 +++ 15 files changed, 348 insertions(+), 14 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 93e8fdcf..b87a62b3 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -8,6 +8,7 @@ 3.0.0 6.1.3 10.3.0 + 10.0.2 1.0.1 1.4.0 4.7.63 diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index 91282f10..6e04c765 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -389,62 +389,62 @@ namespace OpenIddict.Core } /// - /// Retrieves the reference identifier associated with a token. - /// Note: depending on the manager used to create the token, - /// the reference identifier may be hashed for security reasons. + /// Retrieves the unique identifier associated with a token. /// /// The token. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, - /// whose result returns the reference identifier associated with the specified token. + /// whose result returns the unique identifier associated with the token. /// - public virtual Task GetReferenceIdAsync([NotNull] TToken token, CancellationToken cancellationToken) + public virtual Task GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { throw new ArgumentNullException(nameof(token)); } - return Store.GetReferenceIdAsync(token, cancellationToken); + return Store.GetIdAsync(token, cancellationToken); } /// - /// Retrieves the unique identifier associated with a token. + /// Retrieves the payload associated with a token. /// /// The token. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, - /// whose result returns the unique identifier associated with the token. + /// whose result returns the payload associated with the specified token. /// - public virtual Task GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken) + public virtual Task GetPayloadAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { throw new ArgumentNullException(nameof(token)); } - return Store.GetIdAsync(token, cancellationToken); + return Store.GetPayloadAsync(token, cancellationToken); } /// - /// Retrieves the payload associated with a token. + /// Retrieves the reference identifier associated with a token. + /// Note: depending on the manager used to create the token, + /// the reference identifier may be hashed for security reasons. /// /// The token. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, - /// whose result returns the payload associated with the specified token. + /// whose result returns the reference identifier associated with the specified token. /// - public virtual Task GetPayloadAsync([NotNull] TToken token, CancellationToken cancellationToken) + public virtual Task GetReferenceIdAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { throw new ArgumentNullException(nameof(token)); } - return Store.GetPayloadAsync(token, cancellationToken); + return Store.GetReferenceIdAsync(token, cancellationToken); } /// diff --git a/src/OpenIddict.Core/OpenIddict.Core.csproj b/src/OpenIddict.Core/OpenIddict.Core.csproj index c5668db7..0731da1c 100644 --- a/src/OpenIddict.Core/OpenIddict.Core.csproj +++ b/src/OpenIddict.Core/OpenIddict.Core.csproj @@ -22,6 +22,7 @@ + diff --git a/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs index 78a99c69..f9dd5470 100644 --- a/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs +++ b/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; +using Newtonsoft.Json.Linq; namespace OpenIddict.Core { @@ -189,6 +190,17 @@ namespace OpenIddict.Core /// Task> GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken); + /// + /// Retrieves the additional properties associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose + /// result returns all the additional properties associated with the application. + /// + Task GetPropertiesAsync([NotNull] TApplication application, CancellationToken cancellationToken); + /// /// Retrieves the callback addresses associated with an application. /// @@ -296,6 +308,17 @@ namespace OpenIddict.Core Task SetPostLogoutRedirectUrisAsync([NotNull] TApplication application, ImmutableArray addresses, CancellationToken cancellationToken); + /// + /// Sets the additional properties associated with an application. + /// + /// The application. + /// The additional properties associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task SetPropertiesAsync([NotNull] TApplication application, [CanBeNull] JObject properties, CancellationToken cancellationToken); + /// /// Sets the callback addresses associated with an application. /// diff --git a/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs index 44058868..639b62c1 100644 --- a/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; +using Newtonsoft.Json.Linq; namespace OpenIddict.Core { @@ -123,6 +124,17 @@ namespace OpenIddict.Core /// Task GetIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken); + /// + /// Retrieves the additional properties associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose + /// result returns all the additional properties associated with the authorization. + /// + Task GetPropertiesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken); + /// /// Retrieves the scopes associated with an authorization. /// @@ -230,6 +242,17 @@ namespace OpenIddict.Core Task SetApplicationIdAsync([NotNull] TAuthorization authorization, [CanBeNull] string identifier, CancellationToken cancellationToken); + /// + /// Sets the additional properties associated with an authorization. + /// + /// The authorization. + /// The additional properties associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task SetPropertiesAsync([NotNull] TAuthorization authorization, [CanBeNull] JObject properties, CancellationToken cancellationToken); + /// /// Sets the scopes associated with an authorization. /// diff --git a/src/OpenIddict.Core/Stores/IOpenIddictScopeStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictScopeStore.cs index 9b603639..e061433d 100644 --- a/src/OpenIddict.Core/Stores/IOpenIddictScopeStore.cs +++ b/src/OpenIddict.Core/Stores/IOpenIddictScopeStore.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; +using Newtonsoft.Json.Linq; namespace OpenIddict.Core { @@ -121,6 +122,17 @@ namespace OpenIddict.Core /// Task GetNameAsync([NotNull] TScope scope, CancellationToken cancellationToken); + /// + /// Retrieves the additional properties associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose + /// result returns all the additional properties associated with the scope. + /// + Task GetPropertiesAsync([NotNull] TScope scope, CancellationToken cancellationToken); + /// /// Instantiates a new scope. /// @@ -181,6 +193,17 @@ namespace OpenIddict.Core /// Task SetNameAsync([NotNull] TScope scope, [CanBeNull] string name, CancellationToken cancellationToken); + /// + /// Sets the additional properties associated with a scope. + /// + /// The scope. + /// The additional properties associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task SetPropertiesAsync([NotNull] TScope scope, [CanBeNull] JObject properties, CancellationToken cancellationToken); + /// /// Updates an existing scope. /// diff --git a/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs index 566b78ad..4c1581bd 100644 --- a/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs +++ b/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; +using Newtonsoft.Json.Linq; namespace OpenIddict.Core { @@ -197,6 +198,17 @@ namespace OpenIddict.Core /// Task GetPayloadAsync([NotNull] TToken token, CancellationToken cancellationToken); + /// + /// Retrieves the additional properties associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose + /// result returns all the additional properties associated with the token. + /// + Task GetPropertiesAsync([NotNull] TToken token, CancellationToken cancellationToken); + /// /// Retrieves the reference identifier associated with a token. /// Note: depending on the manager used to create the token, @@ -349,6 +361,17 @@ namespace OpenIddict.Core /// Task SetPayloadAsync([NotNull] TToken token, [CanBeNull] string payload, CancellationToken cancellationToken); + /// + /// Sets the additional properties associated with a token. + /// + /// The token. + /// The additional properties associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task SetPropertiesAsync([NotNull] TToken token, [CanBeNull] JObject properties, CancellationToken cancellationToken); + /// /// Sets the reference identifier associated with a token. /// Note: depending on the manager used to create the token, diff --git a/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs index ce4f50e9..620e0d1a 100644 --- a/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs @@ -11,6 +11,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OpenIddict.Models; namespace OpenIddict.Core @@ -381,6 +383,30 @@ namespace OpenIddict.Core return Task.FromResult(ImmutableArray.Create(uris)); } + /// + /// Retrieves the additional properties associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose + /// result returns all the additional properties associated with the application. + /// + public virtual Task GetPropertiesAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(application.Properties)) + { + return Task.FromResult(new JObject()); + } + + return Task.FromResult(JObject.Parse(application.Properties)); + } + /// /// Retrieves the callback addresses associated with an application. /// @@ -596,6 +622,34 @@ namespace OpenIddict.Core return Task.CompletedTask; } + /// + /// Sets the additional properties associated with an application. + /// + /// The application. + /// The additional properties associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPropertiesAsync([NotNull] TApplication application, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (properties == null) + { + application.Properties = null; + + return Task.CompletedTask; + } + + application.Properties = properties.ToString(Formatting.None); + + return Task.CompletedTask; + } + /// /// Sets the callback addresses associated with an application. /// diff --git a/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs index a3b7d98a..da5493fd 100644 --- a/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs @@ -11,6 +11,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OpenIddict.Models; namespace OpenIddict.Core @@ -199,6 +201,30 @@ namespace OpenIddict.Core return Task.FromResult(ConvertIdentifierToString(authorization.Id)); } + /// + /// Retrieves the additional properties associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose + /// result returns all the additional properties associated with the authorization. + /// + public virtual Task GetPropertiesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (string.IsNullOrEmpty(authorization.Properties)) + { + return Task.FromResult(new JObject()); + } + + return Task.FromResult(JObject.Parse(authorization.Properties)); + } + /// /// Retrieves the scopes associated with an authorization. /// @@ -387,6 +413,34 @@ namespace OpenIddict.Core public abstract Task SetApplicationIdAsync([NotNull] TAuthorization authorization, [CanBeNull] string identifier, CancellationToken cancellationToken); + /// + /// Sets the additional properties associated with an authorization. + /// + /// The authorization. + /// The additional properties associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPropertiesAsync([NotNull] TAuthorization authorization, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (properties == null) + { + authorization.Properties = null; + + return Task.CompletedTask; + } + + authorization.Properties = properties.ToString(Formatting.None); + + return Task.CompletedTask; + } + /// /// Sets the scopes associated with an authorization. /// diff --git a/src/OpenIddict.Core/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.Core/Stores/OpenIddictScopeStore.cs index 7cb7dd6a..a9aa4652 100644 --- a/src/OpenIddict.Core/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.Core/Stores/OpenIddictScopeStore.cs @@ -11,6 +11,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OpenIddict.Models; namespace OpenIddict.Core @@ -167,6 +169,30 @@ namespace OpenIddict.Core return Task.FromResult(scope.Name); } + /// + /// Retrieves the additional properties associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose + /// result returns all the additional properties associated with the scope. + /// + public virtual Task GetPropertiesAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (string.IsNullOrEmpty(scope.Properties)) + { + return Task.FromResult(new JObject()); + } + + return Task.FromResult(JObject.Parse(scope.Properties)); + } + /// /// Instantiates a new scope. /// @@ -267,6 +293,34 @@ namespace OpenIddict.Core return Task.CompletedTask; } + /// + /// Sets the additional properties associated with a scope. + /// + /// The scope. + /// The additional properties associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPropertiesAsync([NotNull] TScope scope, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (properties == null) + { + scope.Properties = null; + + return Task.CompletedTask; + } + + scope.Properties = properties.ToString(Formatting.None); + + return Task.CompletedTask; + } + /// /// Updates an existing scope. /// diff --git a/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs index dffd034e..39419e13 100644 --- a/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs @@ -11,6 +11,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OpenIddict.Models; namespace OpenIddict.Core @@ -347,6 +349,30 @@ namespace OpenIddict.Core return Task.FromResult(token.Payload); } + /// + /// Retrieves the additional properties associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose + /// result returns all the additional properties associated with the token. + /// + public virtual Task GetPropertiesAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (string.IsNullOrEmpty(token.Properties)) + { + return Task.FromResult(new JObject()); + } + + return Task.FromResult(JObject.Parse(token.Properties)); + } + /// /// Retrieves the reference identifier associated with a token. /// Note: depending on the manager used to create the token, @@ -607,6 +633,34 @@ namespace OpenIddict.Core return Task.CompletedTask; } + /// + /// Sets the additional properties associated with a token. + /// + /// The token. + /// The additional properties associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPropertiesAsync([NotNull] TToken token, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (properties == null) + { + token.Properties = null; + + return Task.CompletedTask; + } + + token.Properties = properties.ToString(Formatting.None); + + return Task.CompletedTask; + } + /// /// Sets the reference identifier associated with a token. /// Note: depending on the manager used to create the token, diff --git a/src/OpenIddict.Models/OpenIddictApplication.cs b/src/OpenIddict.Models/OpenIddictApplication.cs index e82ef837..f198b7e8 100644 --- a/src/OpenIddict.Models/OpenIddictApplication.cs +++ b/src/OpenIddict.Models/OpenIddictApplication.cs @@ -75,6 +75,12 @@ namespace OpenIddict.Models /// public virtual string PostLogoutRedirectUris { get; set; } + /// + /// Gets or sets the additional properties serialized as a JSON object, + /// or null if no bag was associated with the current application. + /// + public virtual string Properties { get; set; } + /// /// Gets or sets the callback URLs /// associated with the current application, diff --git a/src/OpenIddict.Models/OpenIddictAuthorization.cs b/src/OpenIddict.Models/OpenIddictAuthorization.cs index cd4eaca8..273b43a1 100644 --- a/src/OpenIddict.Models/OpenIddictAuthorization.cs +++ b/src/OpenIddict.Models/OpenIddictAuthorization.cs @@ -49,6 +49,12 @@ namespace OpenIddict.Models /// public virtual TKey Id { get; set; } + /// + /// Gets or sets the additional properties serialized as a JSON object, + /// or null if no bag was associated with the current authorization. + /// + public virtual string Properties { get; set; } + /// /// Gets or sets the space-delimited scopes /// associated with the current authorization. diff --git a/src/OpenIddict.Models/OpenIddictScope.cs b/src/OpenIddict.Models/OpenIddictScope.cs index 28e796e2..2b726c31 100644 --- a/src/OpenIddict.Models/OpenIddictScope.cs +++ b/src/OpenIddict.Models/OpenIddictScope.cs @@ -47,5 +47,11 @@ namespace OpenIddict.Models /// associated with the current scope. /// public virtual string Name { get; set; } + + /// + /// Gets or sets the additional properties serialized as a JSON object, + /// or null if no bag was associated with the current scope. + /// + public virtual string Properties { get; set; } } } diff --git a/src/OpenIddict.Models/OpenIddictToken.cs b/src/OpenIddict.Models/OpenIddictToken.cs index ce10c1fc..0242874b 100644 --- a/src/OpenIddict.Models/OpenIddictToken.cs +++ b/src/OpenIddict.Models/OpenIddictToken.cs @@ -73,6 +73,12 @@ namespace OpenIddict.Models /// public virtual string Payload { get; set; } + /// + /// Gets or sets the additional properties serialized as a JSON object, + /// or null if no bag was associated with the current token. + /// + public virtual string Properties { get; set; } + /// /// Gets or sets the reference identifier associated /// with the current token, if applicable. From c079027ca742cdfed45ccc9699ba76d2a274d05c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 29 Jan 2018 19:36:23 +0100 Subject: [PATCH 3/4] Introduce full support for application permissions --- samples/Mvc.Server/Startup.cs | 18 +- .../OpenIddictApplicationDescriptor.cs | 5 + .../Managers/OpenIddictApplicationManager.cs | 45 +++ src/OpenIddict.Core/OpenIddictConstants.cs | 27 ++ .../Stores/IOpenIddictApplicationStore.cs | 22 ++ .../Stores/OpenIddictApplicationStore.cs | 52 ++++ .../OpenIddictApplication.cs | 6 + .../OpenIddictProvider.Authentication.cs | 101 ++++++- src/OpenIddict/OpenIddictProvider.Exchange.cs | 32 ++- .../OpenIddictProvider.Introspection.cs | 14 + .../OpenIddictProvider.Revocation.cs | 14 + .../OpenIddictProviderTests.Authentication.cs | 200 +++++++++++-- .../OpenIddictProviderTests.Exchange.cs | 268 ++++++++++++++++++ .../OpenIddictProviderTests.Introspection.cs | 88 ++++++ .../OpenIddictProviderTests.Revocation.cs | 63 ++++ .../OpenIddictProviderTests.Serialization.cs | 152 ++++++++++ .../OpenIddictProviderTests.cs | 48 ++++ 17 files changed, 1108 insertions(+), 47 deletions(-) diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index d6bad2d6..97063028 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -151,7 +151,15 @@ namespace Mvc.Server ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", DisplayName = "MVC client application", PostLogoutRedirectUris = { new Uri("http://localhost:53507/signout-callback-oidc") }, - RedirectUris = { new Uri("http://localhost:53507/signin-oidc") } + RedirectUris = { new Uri("http://localhost:53507/signin-oidc") }, + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Logout, + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken + } }; await manager.CreateAsync(descriptor, cancellationToken); @@ -172,7 +180,13 @@ namespace Mvc.Server { ClientId = "postman", DisplayName = "Postman", - RedirectUris = { new Uri("https://www.getpostman.com/oauth2/callback") } + RedirectUris = { new Uri("https://www.getpostman.com/oauth2/callback") }, + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode + } }; await manager.CreateAsync(descriptor, cancellationToken); diff --git a/src/OpenIddict.Core/Descriptors/OpenIddictApplicationDescriptor.cs b/src/OpenIddict.Core/Descriptors/OpenIddictApplicationDescriptor.cs index 11cbcdae..8f1524cc 100644 --- a/src/OpenIddict.Core/Descriptors/OpenIddictApplicationDescriptor.cs +++ b/src/OpenIddict.Core/Descriptors/OpenIddictApplicationDescriptor.cs @@ -27,6 +27,11 @@ namespace OpenIddict.Core /// public string DisplayName { get; set; } + /// + /// Gets the permissions associated with the application. + /// + public ISet Permissions { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); + /// /// Gets the logout callback URLs /// associated with the application. diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index 0585d32a..1cbcd6a8 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -417,6 +417,25 @@ namespace OpenIddict.Core return Store.GetIdAsync(application, cancellationToken); } + /// + /// Retrieves the permissions associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the permissions associated with the application. + /// + public virtual Task> GetPermissionsAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return Store.GetPermissionsAsync(application, cancellationToken); + } + /// /// Retrieves the logout callback addresses associated with an application. /// @@ -455,6 +474,23 @@ namespace OpenIddict.Core return Store.GetRedirectUrisAsync(application, cancellationToken); } + /// + /// Determines whether the specified permission has been granted to the application. + /// + /// The application. + /// The permission. + /// The that can be used to abort the operation. + /// true if the application has been granted the specified permission, false otherwise. + public virtual async Task HasPermissionAsync([NotNull] TApplication application, [NotNull] string permission, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return (await Store.GetPermissionsAsync(application, cancellationToken)).Contains(permission, StringComparer.OrdinalIgnoreCase); + } + /// /// Determines whether an application is a confidential client. /// @@ -668,6 +704,8 @@ namespace OpenIddict.Core Type = await Store.GetClientTypeAsync(application, cancellationToken) }; + descriptor.Permissions.UnionWith(await Store.GetPermissionsAsync(application, cancellationToken)); + foreach (var address in await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken)) { // Ensure the address is not null or empty. @@ -783,6 +821,12 @@ namespace OpenIddict.Core // To ensure a case-sensitive comparison is used, string.Equals(Ordinal) is manually called here. foreach (var application in await Store.FindByPostLogoutRedirectUriAsync(address, cancellationToken)) { + // If the application is not allowed to use the logout endpoint, ignore it and keep iterating. + if (!await HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Logout, cancellationToken)) + { + continue; + } + foreach (var uri in await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken)) { // Note: the post_logout_redirect_uri must be compared using case-sensitive "Simple String Comparison". @@ -864,6 +908,7 @@ namespace OpenIddict.Core await Store.SetClientSecretAsync(application, descriptor.ClientSecret, cancellationToken); await Store.SetClientTypeAsync(application, descriptor.Type, cancellationToken); await Store.SetDisplayNameAsync(application, descriptor.DisplayName, cancellationToken); + await Store.SetPermissionsAsync(application, ImmutableArray.CreateRange(descriptor.Permissions), cancellationToken); await Store.SetPostLogoutRedirectUrisAsync(application, ImmutableArray.CreateRange( descriptor.PostLogoutRedirectUris.Select(address => address.OriginalString)), cancellationToken); await Store.SetRedirectUrisAsync(application, ImmutableArray.CreateRange( diff --git a/src/OpenIddict.Core/OpenIddictConstants.cs b/src/OpenIddict.Core/OpenIddictConstants.cs index 56d38393..201f1e37 100644 --- a/src/OpenIddict.Core/OpenIddictConstants.cs +++ b/src/OpenIddict.Core/OpenIddictConstants.cs @@ -37,6 +37,33 @@ namespace OpenIddict.Core public const string ExternalProvidersSupported = "external_providers_supported"; } + public static class Permissions + { + public static class Endpoints + { + public const string Authorization = "ept:authorization"; + public const string Introspection = "ept:introspection"; + public const string Logout = "ept:logout"; + public const string Revocation = "ept:revocation"; + public const string Token = "ept:token"; + } + + public static class GrantTypes + { + public const string AuthorizationCode = "gt:authorization_code"; + public const string ClientCredentials = "gt:client_credentials"; + public const string Implicit = "gt:implicit"; + public const string Password = "gt:password"; + public const string RefreshToken = "gt:refresh_token"; + } + + public static class Prefixes + { + public const string Endpoint = "ept:"; + public const string GrantType = "gt:"; + } + } + public static class Properties { public const string Application = ".application"; diff --git a/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs index f9dd5470..8c87415c 100644 --- a/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs +++ b/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs @@ -179,6 +179,17 @@ namespace OpenIddict.Core /// Task GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken); + /// + /// Retrieves the permissions associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the permissions associated with the application. + /// + Task> GetPermissionsAsync([NotNull] TApplication application, CancellationToken cancellationToken); + /// /// Retrieves the logout callback addresses associated with an application. /// @@ -296,6 +307,17 @@ namespace OpenIddict.Core /// Task SetDisplayNameAsync([NotNull] TApplication application, [CanBeNull] string name, CancellationToken cancellationToken); + /// + /// Sets the permissions associated with an application. + /// + /// The application. + /// The permissions associated with the application + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task SetPermissionsAsync([NotNull] TApplication application, ImmutableArray permissions, CancellationToken cancellationToken); + /// /// Sets the logout callback addresses associated with an application. /// diff --git a/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs index 620e0d1a..1e9002a9 100644 --- a/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs @@ -355,6 +355,30 @@ namespace OpenIddict.Core return Task.FromResult(ConvertIdentifierToString(application.Id)); } + /// + /// Retrieves the permissions associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the permissions associated with the application. + /// + public virtual Task> GetPermissionsAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(application.Permissions)) + { + return Task.FromResult(ImmutableArray.Create()); + } + + return Task.FromResult(JArray.Parse(application.Permissions).Select(element => (string) element).ToImmutableArray()); + } + /// /// Retrieves the logout callback addresses associated with an application. /// @@ -583,6 +607,34 @@ namespace OpenIddict.Core return Task.CompletedTask; } + /// + /// Sets the permissions associated with an application. + /// + /// The application. + /// The permissions associated with the application + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPermissionsAsync([NotNull] TApplication application, ImmutableArray permissions, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (permissions.IsDefaultOrEmpty) + { + application.Permissions = null; + + return Task.CompletedTask; + } + + application.Permissions = new JArray(permissions.ToArray()).ToString(Formatting.None); + + return Task.CompletedTask; + } + /// /// Sets the logout callback addresses associated with an application. /// diff --git a/src/OpenIddict.Models/OpenIddictApplication.cs b/src/OpenIddict.Models/OpenIddictApplication.cs index f198b7e8..f5d6b0d6 100644 --- a/src/OpenIddict.Models/OpenIddictApplication.cs +++ b/src/OpenIddict.Models/OpenIddictApplication.cs @@ -68,6 +68,12 @@ namespace OpenIddict.Models /// public virtual TKey Id { get; set; } + /// + /// Gets or sets the permissions associated with the + /// current application, serialized as a JSON array. + /// + public virtual string Permissions { get; set; } + /// /// Gets or sets the logout callback URLs /// associated with the current application, diff --git a/src/OpenIddict/OpenIddictProvider.Authentication.cs b/src/OpenIddict/OpenIddictProvider.Authentication.cs index 10910073..47c52c3d 100644 --- a/src/OpenIddict/OpenIddictProvider.Authentication.cs +++ b/src/OpenIddict/OpenIddictProvider.Authentication.cs @@ -271,30 +271,105 @@ namespace OpenIddict // from the other provider methods without having to call the store twice. context.Request.SetProperty($"{OpenIddictConstants.Properties.Application}:{context.ClientId}", application); - // Ensure that the specified redirect_uri is valid and is associated with the client application. - if (!await Applications.ValidateRedirectUriAsync(application, context.RedirectUri, context.HttpContext.RequestAborted)) + // To prevent downgrade attacks, ensure that authorization requests returning a token directly from + // the authorization endpoint are rejected if the client_id corresponds to a confidential application. + // Note: when using the authorization code grant, ValidateTokenRequest is responsible of rejecting + // the token request if the client_id corresponds to an unauthenticated confidential client. + if (await Applications.IsConfidentialAsync(application, context.HttpContext.RequestAborted) && + (context.Request.HasResponseType(OpenIdConnectConstants.ResponseTypes.IdToken) || + context.Request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token))) { - Logger.LogError("The authorization request was rejected because the redirect_uri " + - "was invalid: '{RedirectUri}'.", context.RedirectUri); + context.Reject( + error: OpenIdConnectConstants.Errors.UnsupportedResponseType, + description: "The specified 'response_type' parameter is not valid for this client application."); + + return; + } + + // Reject the request if the application is not allowed to use the authorization endpoint. + if (!await Applications.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, context.HttpContext.RequestAborted)) + { + Logger.LogError("The authorization request was rejected because the application '{ClientId}' " + + "was not allowed to use the authorization endpoint.", context.ClientId); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnauthorizedClient, + description: "This client application is not allowed to use the authorization endpoint."); + + return; + } + + // Reject the request if the application is not allowed to use the authorization code flow. + if (context.Request.IsAuthorizationCodeFlow() && !await Applications.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, context.HttpContext.RequestAborted)) + { + Logger.LogError("The authorization request was rejected because the application '{ClientId}' " + + "was not allowed to use the authorization code flow.", context.ClientId); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnauthorizedClient, + description: "The client application is not allowed to use the authorization code flow."); + + return; + } + + // Reject the request if the application is not allowed to use the implicit flow. + if (context.Request.IsImplicitFlow() && !await Applications.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Implicit, context.HttpContext.RequestAborted)) + { + Logger.LogError("The authorization request was rejected because the application '{ClientId}' " + + "was not allowed to use the implicit flow.", context.ClientId); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnauthorizedClient, + description: "The client application is not allowed to use the implicit flow."); + + return; + } + + // Reject the request if the application is not allowed to use the authorization code/implicit flows. + if (context.Request.IsHybridFlow() && + (!await Applications.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, context.HttpContext.RequestAborted) || + !await Applications.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Implicit, context.HttpContext.RequestAborted))) + { + Logger.LogError("The authorization request was rejected because the application '{ClientId}' " + + "was not allowed to use the hybrid flow.", context.ClientId); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnauthorizedClient, + description: "The client application is not allowed to use the hybrid flow."); + + return; + } + + // Reject the request if the offline_access scope was request and if the + // application is not allowed to use the authorization code/implicit flows. + if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) && + !await Applications.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, context.HttpContext.RequestAborted)) + { + Logger.LogError("The authorization request was rejected because the application '{ClientId}' " + + "was not allowed to request the 'offline_access' scope.", context.ClientId); context.Reject( error: OpenIdConnectConstants.Errors.InvalidRequest, - description: "The specified 'redirect_uri' parameter is not valid for this client application."); + description: "The client application is not allowed to use the 'offline_access' scope."); return; } - // To prevent downgrade attacks, ensure that authorization requests returning a token directly from - // the authorization endpoint are rejected if the client_id corresponds to a confidential application. - // Note: when using the authorization code grant, ValidateTokenRequest is responsible of rejecting - // the token request if the client_id corresponds to an unauthenticated confidential client. - if (await Applications.IsConfidentialAsync(application, context.HttpContext.RequestAborted) && - (context.Request.HasResponseType(OpenIdConnectConstants.ResponseTypes.IdToken) || - context.Request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token))) + // Ensure that the specified redirect_uri is valid and is associated with the client application. + if (!await Applications.ValidateRedirectUriAsync(application, context.RedirectUri, context.HttpContext.RequestAborted)) { + Logger.LogError("The authorization request was rejected because the redirect_uri " + + "was invalid: '{RedirectUri}'.", context.RedirectUri); + context.Reject( error: OpenIdConnectConstants.Errors.InvalidRequest, - description: "The specified 'response_type' parameter is not valid for this client application."); + description: "The specified 'redirect_uri' parameter is not valid for this client application."); return; } diff --git a/src/OpenIddict/OpenIddictProvider.Exchange.cs b/src/OpenIddict/OpenIddictProvider.Exchange.cs index 300d2b62..fc01c73f 100644 --- a/src/OpenIddict/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict/OpenIddictProvider.Exchange.cs @@ -25,8 +25,8 @@ namespace OpenIddict // Reject token requests that don't specify a supported grant type. if (!options.GrantTypes.Contains(context.Request.GrantType)) { - Logger.LogError("The token request was rejected because the '{Grant}' " + - "grant is not supported.", context.Request.GrantType); + Logger.LogError("The token request was rejected because the '{GrantType}' " + + "grant type is not supported.", context.Request.GrantType); context.Reject( error: OpenIdConnectConstants.Errors.UnsupportedGrantType, @@ -135,6 +135,34 @@ namespace OpenIddict // from the other provider methods without having to call the store twice. context.Request.SetProperty($"{OpenIddictConstants.Properties.Application}:{context.ClientId}", application); + // Reject the request if the application is not allowed to use the token endpoint. + if (!await Applications.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, context.HttpContext.RequestAborted)) + { + Logger.LogError("The token request was rejected because the application '{ClientId}' " + + "was not allowed to use the token endpoint.", context.ClientId); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnauthorizedClient, + description: "This client application is not allowed to use the token endpoint."); + + return; + } + + // Reject the request if the application is not allowed to use the specified grant type. + if (!await Applications.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Prefixes.GrantType + context.Request.GrantType, context.HttpContext.RequestAborted)) + { + Logger.LogError("The token request was rejected because the application '{ClientId}' was not allowed to " + + "use the specified grant type: {GrantType}.", context.ClientId, context.Request.GrantType); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnauthorizedClient, + description: "This client application is not allowed to use the specified grant type."); + + return; + } + if (await Applications.IsPublicAsync(application, context.HttpContext.RequestAborted)) { // Note: public applications are not allowed to use the client credentials grant. diff --git a/src/OpenIddict/OpenIddictProvider.Introspection.cs b/src/OpenIddict/OpenIddictProvider.Introspection.cs index 292d677d..e066d458 100644 --- a/src/OpenIddict/OpenIddictProvider.Introspection.cs +++ b/src/OpenIddict/OpenIddictProvider.Introspection.cs @@ -68,6 +68,20 @@ namespace OpenIddict // from the other provider methods without having to call the store twice. context.Request.SetProperty($"{OpenIddictConstants.Properties.Application}:{context.ClientId}", application); + // Reject the request if the application is not allowed to use the introspection endpoint. + if (!await Applications.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, context.HttpContext.RequestAborted)) + { + Logger.LogError("The introspection request was rejected because the application '{ClientId}' " + + "was not allowed to use the introspection endpoint.", context.ClientId); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnauthorizedClient, + description: "This client application is not allowed to use the introspection endpoint."); + + return; + } + // Reject introspection requests sent by public applications. if (await Applications.IsPublicAsync(application, context.HttpContext.RequestAborted)) { diff --git a/src/OpenIddict/OpenIddictProvider.Revocation.cs b/src/OpenIddict/OpenIddictProvider.Revocation.cs index 945153ca..9c0af97e 100644 --- a/src/OpenIddict/OpenIddictProvider.Revocation.cs +++ b/src/OpenIddict/OpenIddictProvider.Revocation.cs @@ -94,6 +94,20 @@ namespace OpenIddict // from the other provider methods without having to call the store twice. context.Request.SetProperty($"{OpenIddictConstants.Properties.Application}:{context.ClientId}", application); + // Reject the request if the application is not allowed to use the revocation endpoint. + if (!await Applications.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Revocation, context.HttpContext.RequestAborted)) + { + Logger.LogError("The revocation request was rejected because the application '{ClientId}' " + + "was not allowed to use the revocation endpoint.", context.ClientId); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnauthorizedClient, + description: "This client application is not allowed to use the revocation endpoint."); + + return; + } + // Reject revocation requests containing a client_secret if the application is a public client. if (await Applications.IsPublicAsync(application, context.HttpContext.RequestAborted)) { diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs index b1a43e75..fba20152 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs @@ -344,8 +344,53 @@ namespace OpenIddict.Tests Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); } + [Theory] + [InlineData("code id_token token")] + [InlineData("code token")] + [InlineData("id_token")] + [InlineData("id_token token")] + [InlineData("token")] + public async Task ValidateAuthorizationRequest_ImplicitOrHybridRequestIsRejectedWhenClientIsConfidential(string type) + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(instance => + { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .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.UnsupportedResponseType, response.Error); + Assert.Equal("The specified 'response_type' parameter is not valid for this client application.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); + } + [Fact] - public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenRedirectUriIsInvalid() + public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() { // Arrange var application = new OpenIddictApplication(); @@ -355,7 +400,8 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) .ReturnsAsync(false); }); @@ -375,20 +421,45 @@ namespace OpenIddict.Tests }); // Assert - Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); - Assert.Equal("The specified 'redirect_uri' parameter is not valid for this client application.", response.ErrorDescription); + Assert.Equal(OpenIdConnectConstants.Errors.UnauthorizedClient, response.Error); + Assert.Equal("This client application is not allowed to use the authorization endpoint.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny()), Times.Once()); } [Theory] - [InlineData("code id_token token")] - [InlineData("code token")] - [InlineData("id_token")] - [InlineData("id_token token")] - [InlineData("token")] - public async Task ValidateAuthorizationRequest_ImplicitOrHybridRequestIsRejectedWhenClientIsConfidential(string type) + [InlineData( + "code", + new[] { OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode }, + "The client application is not allowed to use the authorization code flow.")] + [InlineData( + "code id_token", + new[] { OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, OpenIddictConstants.Permissions.GrantTypes.Implicit }, + "The client application is not allowed to use the hybrid flow.")] + [InlineData( + "code id_token token", + new[] { OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, OpenIddictConstants.Permissions.GrantTypes.Implicit }, + "The client application is not allowed to use the hybrid flow.")] + [InlineData( + "code token", + new[] { OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, OpenIddictConstants.Permissions.GrantTypes.Implicit }, + "The client application is not allowed to use the hybrid flow.")] + [InlineData( + "id_token", + new[] { OpenIddictConstants.Permissions.GrantTypes.Implicit }, + "The client application is not allowed to use the implicit flow.")] + [InlineData( + "id_token token", + new[] { OpenIddictConstants.Permissions.GrantTypes.Implicit }, + "The client application is not allowed to use the implicit flow.")] + [InlineData( + "token", + new[] { OpenIddictConstants.Permissions.GrantTypes.Implicit }, + "The client application is not allowed to use the implicit flow.")] + public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenGrantTypePermissionIsNotGranted( + string type, string[] permissions, string description) { // Arrange var application = new OpenIddictApplication(); @@ -398,11 +469,15 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) .ReturnsAsync(true); - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + foreach (var permission in permissions) + { + instance.Setup(mock => mock.HasPermissionAsync(application, permission, It.IsAny())) + .ReturnsAsync(false); + } }); var server = CreateAuthorizationServer(builder => @@ -422,13 +497,64 @@ namespace OpenIddict.Tests Scope = OpenIdConnectConstants.Scopes.OpenId }); + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.UnauthorizedClient, response.Error); + Assert.Equal(description, response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, permissions[0], It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenRedirectUriIsInvalid() + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(instance => + { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) + .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.InvalidRequest, response.Error); - Assert.Equal("The specified 'response_type' parameter is not valid for this client application.", response.ErrorDescription); + Assert.Equal("The specified 'redirect_uri' parameter is not valid for this client application.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); } [Fact] @@ -447,6 +573,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Implicit, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); @@ -506,6 +640,18 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Implicit, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); @@ -582,6 +728,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Implicit, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); @@ -616,20 +770,6 @@ namespace OpenIddict.Tests // Arrange var server = CreateAuthorizationServer(builder => { - builder.Services.AddSingleton(CreateApplicationManager(instance => - { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(true); - - instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) - .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); - })); - builder.EnableAuthorizationEndpoint("/authorize-status-code-middleware"); }); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs index 96d1baea..b2c0753c 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs @@ -200,6 +200,94 @@ namespace OpenIddict.Tests Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); } + [Fact] + public async Task ValidateTokenRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(instance => + { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .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", + GrantType = OpenIdConnectConstants.GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.UnauthorizedClient, response.Error); + Assert.Equal("This client application is not allowed to use the token endpoint.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateTokenRequest_RequestIsRejectedWhenGrantTypePermissionIsNotGranted() + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(instance => + { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny())) + .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", + GrantType = OpenIdConnectConstants.GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.UnauthorizedClient, response.Error); + Assert.Equal("This client application is not allowed to use the specified grant type.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny()), Times.Once()); + } + [Fact] public async Task ValidateTokenRequest_ClientCredentialsRequestFromPublicClientIsRejected() { @@ -211,6 +299,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.ClientCredentials, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); }); @@ -235,6 +331,10 @@ namespace OpenIddict.Tests Assert.Equal("The specified 'grant_type' parameter is not valid for this client application.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.ClientCredentials, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); } @@ -249,6 +349,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); }); @@ -275,6 +383,10 @@ namespace OpenIddict.Tests Assert.Equal("The 'client_secret' parameter is not valid for this client application.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); } @@ -289,6 +401,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); }); @@ -315,6 +435,10 @@ namespace OpenIddict.Tests Assert.Equal("The 'client_secret' parameter required for this client application is missing.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); } @@ -329,6 +453,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Hybrid); }); @@ -355,6 +487,10 @@ namespace OpenIddict.Tests Assert.Equal("The 'client_secret' parameter required for this client application is missing.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); } @@ -369,6 +505,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -398,6 +542,10 @@ namespace OpenIddict.Tests Assert.Equal("The specified client credentials are invalid.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); } @@ -429,6 +577,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); @@ -481,6 +637,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); @@ -538,6 +702,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); @@ -597,6 +769,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); @@ -663,6 +843,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); @@ -731,6 +919,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); @@ -793,6 +989,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); @@ -876,6 +1080,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); @@ -973,6 +1185,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); @@ -1062,6 +1282,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); @@ -1143,6 +1371,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); @@ -1215,6 +1451,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); @@ -1300,6 +1544,30 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.ClientCredentials, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + "gt:urn:ietf:params:oauth:grant-type:custom_grant", It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs index cb9c5f3d..a1150489 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs @@ -97,6 +97,46 @@ namespace OpenIddict.Tests Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); } + [Fact] + public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(instance => + { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .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.UnauthorizedClient, response.Error); + Assert.Equal("This client application is not allowed to use the introspection endpoint.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny()), Times.Once()); + } + [Fact] public async Task ValidateIntrospectionRequest_RequestsSentByPublicClientsAreRejected() { @@ -108,6 +148,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); }); @@ -132,6 +176,8 @@ namespace OpenIddict.Tests Assert.Equal("This client application is not allowed to use the introspection endpoint.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); } @@ -146,6 +192,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -173,6 +223,8 @@ namespace OpenIddict.Tests Assert.Equal("The specified client credentials are invalid.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); } @@ -207,6 +259,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -261,6 +317,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -318,6 +378,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -368,6 +432,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -447,6 +515,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -514,6 +586,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -586,6 +662,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -651,6 +731,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -726,6 +810,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs index ec0a7cfb..e55604a3 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs @@ -99,6 +99,47 @@ namespace OpenIddict.Tests Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); } + [Fact] + public async Task ValidateRevocationRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(instance => + { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Revocation, It.IsAny())) + .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.UnauthorizedClient, response.Error); + Assert.Equal("This client application is not allowed to use the revocation endpoint.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Revocation, It.IsAny()), Times.Once()); + } + [Fact] public async Task ValidateRevocationRequest_ClientSecretCannotBeUsedByPublicClients() { @@ -110,6 +151,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Revocation, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); }); @@ -135,6 +180,8 @@ namespace OpenIddict.Tests Assert.Equal("The 'client_secret' parameter is not valid for this client application.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Revocation, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); } @@ -149,6 +196,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Revocation, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); }); @@ -174,6 +225,8 @@ namespace OpenIddict.Tests Assert.Equal("The 'client_secret' parameter required for this client application is missing.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Revocation, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); } @@ -188,6 +241,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Revocation, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Hybrid); }); @@ -213,6 +270,8 @@ namespace OpenIddict.Tests Assert.Equal("The 'client_secret' parameter required for this client application is missing.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Revocation, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); } @@ -227,6 +286,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Revocation, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs index 0740aa22..09ce5023 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs @@ -40,6 +40,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -113,6 +117,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -167,6 +175,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -225,6 +237,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -288,6 +304,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -376,6 +396,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -434,6 +458,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -509,6 +537,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -563,6 +595,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -621,6 +657,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -684,6 +724,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -770,6 +814,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -838,6 +886,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -882,6 +934,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -958,6 +1014,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -1013,6 +1073,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -1088,6 +1152,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -1142,6 +1210,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -1200,6 +1272,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -1263,6 +1339,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -1349,6 +1429,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -1417,6 +1501,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -1461,6 +1549,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -1537,6 +1629,10 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); @@ -1695,6 +1791,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); @@ -1796,6 +1900,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); @@ -1857,6 +1969,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); @@ -1931,6 +2051,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); @@ -2003,6 +2131,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); @@ -2061,6 +2197,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); @@ -2350,6 +2494,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.cs index e8a104d2..13860dd2 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.cs @@ -58,6 +58,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); @@ -234,6 +242,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); @@ -425,6 +441,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); @@ -497,6 +521,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); @@ -1093,6 +1125,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); @@ -1149,6 +1189,14 @@ namespace OpenIddict.Tests instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); From 98651b9e34ed5e6103bd30db7a57ee5ce54e0f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 29 Jan 2018 19:52:34 +0100 Subject: [PATCH 4/4] Update OpenIddictApplication.PostLogoutRedirectUris/RedirectUris and OpenIddictAuthorization.Scopes to be stored as JSON arrays --- .../Stores/OpenIddictApplicationStore.cs | 98 +++++-------------- .../Stores/OpenIddictAuthorizationStore.cs | 18 +--- .../OpenIddictApplication.cs | 10 +- .../OpenIddictAuthorization.cs | 4 +- 4 files changed, 30 insertions(+), 100 deletions(-) diff --git a/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs index 1e9002a9..7596af58 100644 --- a/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs @@ -140,40 +140,27 @@ namespace OpenIddict.Core throw new ArgumentException("The address cannot be null or empty.", nameof(address)); } - // To optimize the efficiency of the query, only applications whose stringified - // LogoutRedirectUris property contains the specified address are returned. Once the - // applications are retrieved, the LogoutRedirectUri property is manually split. + // To optimize the efficiency of the query a bit, only applications whose stringified + // PostLogoutRedirectUris contains the specified URL are returned. Once the applications + // are retrieved, a second pass is made to ensure only valid elements are returned. + // Implementers that use this method in a hot path may want to override this method + // to use SQL Server 2016 functions like JSON_VALUE to make the query more efficient. IQueryable Query(IQueryable applications, string uri) => from application in applications where application.PostLogoutRedirectUris.Contains(uri) select application; - var candidates = await ListAsync((applications, uri) => Query(applications, uri), address, cancellationToken); - if (candidates.IsDefaultOrEmpty) - { - return ImmutableArray.Create(); - } + var builder = ImmutableArray.CreateBuilder(); - var builder = ImmutableArray.CreateBuilder(0); - - foreach (var candidate in candidates) + foreach (var application in await ListAsync((applications, uri) => Query(applications, uri), address, cancellationToken)) { - var uris = candidate.PostLogoutRedirectUris?.Split( - new[] { OpenIddictConstants.Separators.Space }, - StringSplitOptions.RemoveEmptyEntries); - - if (uris == null) - { - continue; - } - - foreach (var uri in uris) + foreach (var uri in await GetPostLogoutRedirectUrisAsync(application, cancellationToken)) { // Note: the post_logout_redirect_uri must be compared // using case-sensitive "Simple String Comparison". if (string.Equals(uri, address, StringComparison.Ordinal)) { - builder.Add(candidate); + builder.Add(application); break; } @@ -199,40 +186,27 @@ namespace OpenIddict.Core throw new ArgumentException("The address cannot be null or empty.", nameof(address)); } - // To optimize the efficiency of the query, only applications whose stringified - // RedirectUris property contains the specified address are returned. Once the - // applications are retrieved, the RedirectUri property is manually split. + // To optimize the efficiency of the query a bit, only applications whose stringified + // RedirectUris property contains the specified URL are returned. Once the applications + // are retrieved, a second pass is made to ensure only valid elements are returned. + // Implementers that use this method in a hot path may want to override this method + // to use SQL Server 2016 functions like JSON_VALUE to make the query more efficient. IQueryable Query(IQueryable applications, string uri) => from application in applications where application.RedirectUris.Contains(uri) select application; - var candidates = await ListAsync((applications, uri) => Query(applications, uri), address, cancellationToken); - if (candidates.IsDefaultOrEmpty) - { - return ImmutableArray.Create(); - } + var builder = ImmutableArray.CreateBuilder(); - var builder = ImmutableArray.CreateBuilder(0); - - foreach (var candidate in candidates) + foreach (var application in await ListAsync((applications, uri) => Query(applications, uri), address, cancellationToken)) { - var uris = candidate.RedirectUris?.Split( - new[] { OpenIddictConstants.Separators.Space }, - StringSplitOptions.RemoveEmptyEntries); - - if (uris == null) - { - continue; - } - - foreach (var uri in uris) + foreach (var uri in await GetRedirectUrisAsync(application, cancellationToken)) { // Note: the redirect_uri must be compared using case-sensitive "Simple String Comparison". // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information. if (string.Equals(uri, address, StringComparison.Ordinal)) { - builder.Add(candidate); + builder.Add(application); break; } @@ -400,11 +374,7 @@ namespace OpenIddict.Core return Task.FromResult(ImmutableArray.Create()); } - var uris = application.PostLogoutRedirectUris.Split( - new[] { OpenIddictConstants.Separators.Space }, - StringSplitOptions.RemoveEmptyEntries); - - return Task.FromResult(ImmutableArray.Create(uris)); + return Task.FromResult(JArray.Parse(application.PostLogoutRedirectUris).Select(element => (string) element).ToImmutableArray()); } /// @@ -452,11 +422,7 @@ namespace OpenIddict.Core return Task.FromResult(ImmutableArray.Create()); } - var uris = application.RedirectUris.Split( - new[] { OpenIddictConstants.Separators.Space }, - StringSplitOptions.RemoveEmptyEntries); - - return Task.FromResult(ImmutableArray.Create(uris)); + return Task.FromResult(JArray.Parse(application.RedirectUris).Select(element => (string) element).ToImmutableArray()); } /// @@ -659,17 +625,7 @@ namespace OpenIddict.Core return Task.CompletedTask; } - if (addresses.Any(address => string.IsNullOrEmpty(address))) - { - throw new ArgumentException("Callback addresses cannot be null or empty.", nameof(addresses)); - } - - if (addresses.Any(address => address.Contains(OpenIddictConstants.Separators.Space))) - { - throw new ArgumentException("Callback addresses cannot contain spaces.", nameof(addresses)); - } - - application.PostLogoutRedirectUris = string.Join(OpenIddictConstants.Separators.Space, addresses); + application.PostLogoutRedirectUris = new JArray(addresses.ToArray()).ToString(Formatting.None); return Task.CompletedTask; } @@ -726,17 +682,7 @@ namespace OpenIddict.Core return Task.CompletedTask; } - if (addresses.Any(address => string.IsNullOrEmpty(address))) - { - throw new ArgumentException("Callback addresses cannot be null or empty.", nameof(addresses)); - } - - if (addresses.Any(address => address.Contains(OpenIddictConstants.Separators.Space))) - { - throw new ArgumentException("Callback addresses cannot contain spaces.", nameof(addresses)); - } - - application.RedirectUris = string.Join(OpenIddictConstants.Separators.Space, addresses); + application.RedirectUris = new JArray(addresses.ToArray()).ToString(Formatting.None); return Task.CompletedTask; } diff --git a/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs index da5493fd..520053ba 100644 --- a/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs @@ -246,11 +246,7 @@ namespace OpenIddict.Core return Task.FromResult(ImmutableArray.Create()); } - var scopes = authorization.Scopes.Split( - new[] { OpenIddictConstants.Separators.Space }, - StringSplitOptions.RemoveEmptyEntries); - - return Task.FromResult(ImmutableArray.Create(scopes)); + return Task.FromResult(JArray.Parse(authorization.Scopes).Select(element => (string) element).ToImmutableArray()); } /// @@ -465,17 +461,7 @@ namespace OpenIddict.Core return Task.CompletedTask; } - if (scopes.Any(scope => string.IsNullOrEmpty(scope))) - { - throw new ArgumentException("Scopes cannot be null or empty.", nameof(authorization)); - } - - if (scopes.Any(scope => scope.Contains(OpenIddictConstants.Separators.Space))) - { - throw new ArgumentException("Scopes cannot contain spaces.", nameof(authorization)); - } - - authorization.Scopes = string.Join(OpenIddictConstants.Separators.Space, scopes); + authorization.Scopes = new JArray(scopes.ToArray()).ToString(Formatting.None); return Task.CompletedTask; } diff --git a/src/OpenIddict.Models/OpenIddictApplication.cs b/src/OpenIddict.Models/OpenIddictApplication.cs index f5d6b0d6..99ae4588 100644 --- a/src/OpenIddict.Models/OpenIddictApplication.cs +++ b/src/OpenIddict.Models/OpenIddictApplication.cs @@ -75,9 +75,8 @@ namespace OpenIddict.Models public virtual string Permissions { get; set; } /// - /// Gets or sets the logout callback URLs - /// associated with the current application, - /// stored as a unique space-separated string. + /// Gets or sets the logout callback URLs associated with + /// the current application, serialized as a JSON array. /// public virtual string PostLogoutRedirectUris { get; set; } @@ -88,9 +87,8 @@ namespace OpenIddict.Models public virtual string Properties { get; set; } /// - /// Gets or sets the callback URLs - /// associated with the current application, - /// stored as a unique space-separated string. + /// Gets or sets the callback URLs associated with the + /// current application, serialized as a JSON array. /// public virtual string RedirectUris { get; set; } diff --git a/src/OpenIddict.Models/OpenIddictAuthorization.cs b/src/OpenIddict.Models/OpenIddictAuthorization.cs index 273b43a1..b975e9be 100644 --- a/src/OpenIddict.Models/OpenIddictAuthorization.cs +++ b/src/OpenIddict.Models/OpenIddictAuthorization.cs @@ -56,8 +56,8 @@ namespace OpenIddict.Models public virtual string Properties { get; set; } /// - /// Gets or sets the space-delimited scopes - /// associated with the current authorization. + /// Gets or sets the scopes associated with the current + /// authorization, serialized as a JSON array. /// public virtual string Scopes { get; set; }