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 diff --git a/build/dependencies.props b/build/dependencies.props index a88b44d4..4c9da5ec 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -6,8 +6,9 @@ 1.0.2 2.0.0 6.1.3 - 10.3.0 1.2.0 + 10.3.0 + 9.0.1 1.6.0 4.7.63 1.0.0 diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index 3e4eeb3a..fd57406f 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -192,7 +192,15 @@ namespace Mvc.Server ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", DisplayName = "MVC client application", PostLogoutRedirectUris = { new Uri("http://localhost:53507/") }, - 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); @@ -213,7 +221,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 8982adef..553a1000 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -418,6 +418,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. /// @@ -456,6 +475,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. /// @@ -669,6 +705,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. @@ -784,6 +822,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". @@ -865,6 +909,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/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index f469652e..763991fa 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 b6214fec..99b71120 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/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 78a99c69..8c87415c 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 { @@ -178,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. /// @@ -189,6 +201,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. /// @@ -284,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. /// @@ -296,6 +330,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 d84e89e9..5739ac7a 100644 --- a/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.Core/Stores/OpenIddictApplicationStore.cs @@ -12,6 +12,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 @@ -139,40 +141,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(0); + var builder = ImmutableArray.CreateBuilder(); - 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; } @@ -198,40 +187,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(0); + var builder = ImmutableArray.CreateBuilder(); - 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; } @@ -354,6 +330,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. /// @@ -375,11 +375,31 @@ namespace OpenIddict.Core return Task.FromResult(ImmutableArray.Create()); } - var uris = application.PostLogoutRedirectUris.Split( - new[] { OpenIddictConstants.Separators.Space }, - StringSplitOptions.RemoveEmptyEntries); + return Task.FromResult(JArray.Parse(application.PostLogoutRedirectUris).Select(element => (string) element).ToImmutableArray()); + } + + /// + /// 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)); + } - return Task.FromResult(ImmutableArray.Create(uris)); + if (string.IsNullOrEmpty(application.Properties)) + { + return Task.FromResult(new JObject()); + } + + return Task.FromResult(JObject.Parse(application.Properties)); } /// @@ -403,11 +423,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()); } /// @@ -560,6 +576,34 @@ namespace OpenIddict.Core return Task.FromResult(0); } + /// + /// 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.FromResult(0); + } + + application.Permissions = new JArray(permissions.ToArray()).ToString(Formatting.None); + + return Task.FromResult(0); + } + /// /// Sets the logout callback addresses associated with an application. /// @@ -584,17 +628,35 @@ namespace OpenIddict.Core return Task.FromResult(0); } - if (addresses.Any(address => string.IsNullOrEmpty(address))) + application.PostLogoutRedirectUris = new JArray(addresses.ToArray()).ToString(Formatting.None); + + return Task.FromResult(0); + } + + /// + /// 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 ArgumentException("Callback addresses cannot be null or empty.", nameof(addresses)); + throw new ArgumentNullException(nameof(application)); } - if (addresses.Any(address => address.Contains(OpenIddictConstants.Separators.Space))) + if (properties == null) { - throw new ArgumentException("Callback addresses cannot contain spaces.", nameof(addresses)); + application.Properties = null; + + return Task.FromResult(0); } - application.PostLogoutRedirectUris = string.Join(OpenIddictConstants.Separators.Space, addresses); + application.Properties = properties.ToString(Formatting.None); return Task.FromResult(0); } @@ -623,17 +685,7 @@ namespace OpenIddict.Core return Task.FromResult(0); } - 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.FromResult(0); } diff --git a/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs index 8f1c1784..f096d33f 100644 --- a/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.Core/Stores/OpenIddictAuthorizationStore.cs @@ -12,6 +12,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 @@ -200,6 +202,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. /// @@ -221,11 +247,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()); } /// @@ -393,40 +415,58 @@ namespace OpenIddict.Core [CanBeNull] string identifier, CancellationToken cancellationToken); /// - /// Sets the scopes associated with an authorization. + /// Sets the additional properties associated with an authorization. /// /// The authorization. - /// The scopes associated with 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 SetScopesAsync([NotNull] TAuthorization authorization, - ImmutableArray scopes, CancellationToken cancellationToken) + public virtual Task SetPropertiesAsync([NotNull] TAuthorization authorization, [CanBeNull] JObject properties, CancellationToken cancellationToken) { if (authorization == null) { throw new ArgumentNullException(nameof(authorization)); } - if (scopes.IsDefaultOrEmpty) + if (properties == null) { - authorization.Scopes = null; + authorization.Properties = null; return Task.FromResult(0); } - if (scopes.Any(scope => string.IsNullOrEmpty(scope))) + authorization.Properties = properties.ToString(Formatting.None); + + return Task.FromResult(0); + } + + /// + /// Sets the scopes associated with an authorization. + /// + /// The authorization. + /// The scopes 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 SetScopesAsync([NotNull] TAuthorization authorization, + ImmutableArray scopes, CancellationToken cancellationToken) + { + if (authorization == null) { - throw new ArgumentException("Scopes cannot be null or empty.", nameof(authorization)); + throw new ArgumentNullException(nameof(authorization)); } - if (scopes.Any(scope => scope.Contains(OpenIddictConstants.Separators.Space))) + if (scopes.IsDefaultOrEmpty) { - throw new ArgumentException("Scopes cannot contain spaces.", nameof(authorization)); + authorization.Scopes = null; + + return Task.FromResult(0); } - authorization.Scopes = string.Join(OpenIddictConstants.Separators.Space, scopes); + authorization.Scopes = new JArray(scopes.ToArray()).ToString(Formatting.None); return Task.FromResult(0); } diff --git a/src/OpenIddict.Core/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.Core/Stores/OpenIddictScopeStore.cs index fd68d000..228495ba 100644 --- a/src/OpenIddict.Core/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.Core/Stores/OpenIddictScopeStore.cs @@ -12,6 +12,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 @@ -168,6 +170,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. /// @@ -270,6 +296,34 @@ namespace OpenIddict.Core return Task.FromResult(0); } + /// + /// 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.FromResult(0); + } + + scope.Properties = properties.ToString(Formatting.None); + + return Task.FromResult(0); + } + /// /// Updates an existing scope. /// diff --git a/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs index af1f5e86..936343c0 100644 --- a/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.Core/Stores/OpenIddictTokenStore.cs @@ -12,6 +12,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 @@ -348,6 +350,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, @@ -612,6 +638,34 @@ namespace OpenIddict.Core return Task.FromResult(0); } + /// + /// 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.FromResult(0); + } + + token.Properties = properties.ToString(Formatting.None); + + return Task.FromResult(0); + } + /// /// 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..99ae4588 100644 --- a/src/OpenIddict.Models/OpenIddictApplication.cs +++ b/src/OpenIddict.Models/OpenIddictApplication.cs @@ -69,16 +69,26 @@ namespace OpenIddict.Models public virtual TKey Id { 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 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, serialized as a JSON array. /// public virtual string PostLogoutRedirectUris { get; set; } /// - /// Gets or sets the callback URLs - /// associated with the current application, - /// stored as a unique space-separated string. + /// 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, 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 cd4eaca8..b975e9be 100644 --- a/src/OpenIddict.Models/OpenIddictAuthorization.cs +++ b/src/OpenIddict.Models/OpenIddictAuthorization.cs @@ -50,8 +50,14 @@ namespace OpenIddict.Models public virtual TKey Id { get; set; } /// - /// Gets or sets the space-delimited scopes - /// associated with the current authorization. + /// 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 scopes associated with the current + /// authorization, serialized as a JSON array. /// public virtual string Scopes { get; set; } 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. diff --git a/src/OpenIddict/OpenIddictProvider.Authentication.cs b/src/OpenIddict/OpenIddictProvider.Authentication.cs index 488cb36b..a3ef1360 100644 --- a/src/OpenIddict/OpenIddictProvider.Authentication.cs +++ b/src/OpenIddict/OpenIddictProvider.Authentication.cs @@ -277,30 +277,106 @@ 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 c048c55c..807e8063 100644 --- a/src/OpenIddict/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict/OpenIddictProvider.Exchange.cs @@ -29,8 +29,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, @@ -139,6 +139,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 66c4c640..4479d85a 100644 --- a/src/OpenIddict/OpenIddictProvider.Introspection.cs +++ b/src/OpenIddict/OpenIddictProvider.Introspection.cs @@ -72,6 +72,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 f7ec5989..a534d9e9 100644 --- a/src/OpenIddict/OpenIddictProvider.Revocation.cs +++ b/src/OpenIddict/OpenIddictProvider.Revocation.cs @@ -98,6 +98,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 92e3833c..5daa00cc 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs @@ -345,8 +345,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(); @@ -356,7 +401,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); }); @@ -376,20 +422,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(); @@ -399,11 +470,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 => @@ -423,13 +498,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] @@ -448,6 +574,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); @@ -581,6 +727,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); @@ -615,20 +769,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 7ad0af8f..5e778314 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs @@ -202,6 +202,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() { @@ -213,6 +301,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); }); @@ -237,6 +333,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()); } @@ -251,6 +351,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); }); @@ -277,6 +385,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()); } @@ -291,6 +403,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); }); @@ -317,6 +437,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()); } @@ -331,6 +455,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); }); @@ -357,6 +489,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()); } @@ -371,6 +507,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); @@ -400,6 +544,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()); } @@ -431,6 +579,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); })); @@ -483,6 +639,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); })); @@ -540,6 +704,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); })); @@ -599,6 +771,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); })); @@ -665,6 +845,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); })); @@ -733,6 +921,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); })); @@ -795,6 +991,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); })); @@ -878,6 +1082,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); })); @@ -975,6 +1187,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); })); @@ -1064,6 +1284,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); })); @@ -1145,6 +1373,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); })); @@ -1217,6 +1453,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); })); @@ -1302,6 +1546,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 67743228..23e92e59 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs @@ -99,6 +99,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() { @@ -110,6 +150,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); }); @@ -134,6 +178,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()); } @@ -148,6 +194,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); @@ -175,6 +225,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()); } @@ -209,6 +261,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); @@ -263,6 +319,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); @@ -320,6 +380,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); @@ -370,6 +434,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); @@ -449,6 +517,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); @@ -516,6 +588,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); @@ -588,6 +664,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); @@ -653,6 +733,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); @@ -728,6 +812,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 3f2bef1b..48c93acc 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs @@ -101,6 +101,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() { @@ -112,6 +153,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); }); @@ -137,6 +182,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()); } @@ -151,6 +198,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); }); @@ -176,6 +227,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()); } @@ -190,6 +243,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); }); @@ -215,6 +272,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()); } @@ -229,6 +288,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 3992b22b..2a8eacef 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs @@ -42,6 +42,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); @@ -115,6 +119,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); @@ -169,6 +177,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); @@ -227,6 +239,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); @@ -290,6 +306,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); @@ -378,6 +398,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); @@ -436,6 +460,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); @@ -511,6 +539,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); @@ -565,6 +597,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); @@ -623,6 +659,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); @@ -686,6 +726,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); @@ -772,6 +816,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); @@ -840,6 +888,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); @@ -884,6 +936,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); @@ -960,6 +1016,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); @@ -1015,6 +1075,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); @@ -1090,6 +1154,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); @@ -1144,6 +1212,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); @@ -1202,6 +1274,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); @@ -1265,6 +1341,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); @@ -1351,6 +1431,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); @@ -1419,6 +1503,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); @@ -1463,6 +1551,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); @@ -1539,6 +1631,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); @@ -1697,6 +1793,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); @@ -1798,6 +1902,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); @@ -1859,6 +1971,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); @@ -1933,6 +2053,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); @@ -2005,6 +2133,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); @@ -2063,6 +2199,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); @@ -2352,6 +2496,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 10b4674e..b373345a 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.cs @@ -59,6 +59,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); @@ -235,6 +243,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); })); @@ -426,6 +442,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); })); @@ -498,6 +522,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); })); @@ -1094,6 +1126,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); @@ -1150,6 +1190,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);