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