Browse Source

Introduce scope permissions, add opt-in scope validation support and rework existing permissions

pull/555/head
Kévin Chalet 8 years ago
parent
commit
cb05ebc769
  1. 105
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  2. 105
      src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs
  3. 1
      src/OpenIddict.Core/OpenIddictConstants.cs
  4. 22
      src/OpenIddict.Core/Stores/IOpenIddictScopeStore.cs
  5. 49
      src/OpenIddict.Core/Stores/OpenIddictScopeStore.cs
  6. 16
      src/OpenIddict/OpenIddictExtensions.cs
  7. 6
      src/OpenIddict/OpenIddictOptions.cs
  8. 40
      src/OpenIddict/OpenIddictProvider.Authentication.cs
  9. 42
      src/OpenIddict/OpenIddictProvider.Exchange.cs
  10. 171
      test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs
  11. 153
      test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs
  12. 12
      test/OpenIddict.Tests/OpenIddictProviderTests.cs

105
src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs

@ -483,7 +483,110 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(application));
}
return (await Store.GetPermissionsAsync(application, cancellationToken)).Contains(permission, StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrEmpty(permission))
{
throw new ArgumentException("The permission name cannot be null or empty.", nameof(permission));
}
var permissions = await Store.GetPermissionsAsync(application, cancellationToken);
bool HasPermission(string name)
{
if (permissions.IsEmpty)
{
return false;
}
return permissions.Contains(name);
}
bool HasEndpointPermission(string name)
{
// If the requested permission is an "endpoint" permission, return true if it has been
// explicitly granted OR if no other endpoint permission has been explicitly registered.
if (permissions.IsEmpty || HasPermission(name))
{
return true;
}
if (permissions.Any(element => element.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint)))
{
return false;
}
return true;
}
bool HasGrantTypePermission(string name)
{
// If the requested permission is a "grant_type" permission, return true if it has been
// explicitly granted OR if the application is allowed to use the corresponding endpoint
// AND no other grant type permission has been explicitly registered.
if (permissions.IsEmpty || HasPermission(name))
{
return true;
}
if (permissions.Any(element => element.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType)))
{
return false;
}
switch (permission)
{
case OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode:
return HasEndpointPermission(OpenIddictConstants.Permissions.Endpoints.Authorization) &&
HasEndpointPermission(OpenIddictConstants.Permissions.Endpoints.Token);
case OpenIddictConstants.Permissions.GrantTypes.Implicit:
return HasEndpointPermission(OpenIddictConstants.Permissions.Endpoints.Authorization);
default:
case OpenIddictConstants.Permissions.GrantTypes.ClientCredentials:
case OpenIddictConstants.Permissions.GrantTypes.Password:
case OpenIddictConstants.Permissions.GrantTypes.RefreshToken:
return HasEndpointPermission(OpenIddictConstants.Permissions.Endpoints.Token);
}
}
bool HasScopePermission(string name)
{
// If the requested permission is a "scope" permission, return true if it has been
// explicitly granted OR if the application is allowed to use the authorization or
// token endpoints AND no other scope permission has been explicitly registered.
if (permissions.IsEmpty || HasPermission(name))
{
return true;
}
if (permissions.Any(element => element.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope)))
{
return false;
}
return HasEndpointPermission(OpenIddictConstants.Permissions.Endpoints.Authorization) ||
HasEndpointPermission(OpenIddictConstants.Permissions.Endpoints.Token);
}
if (permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Endpoint))
{
return HasEndpointPermission(permission);
}
if (permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType))
{
return HasGrantTypePermission(permission);
}
if (permission.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope))
{
return HasScopePermission(permission);
}
return HasPermission(permission);
}
/// <summary>

105
src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs

@ -162,6 +162,45 @@ namespace OpenIddict.Core
return Store.FindByIdAsync(identifier, cancellationToken);
}
/// <summary>
/// Retrieves a scope using its name.
/// </summary>
/// <param name="name">The name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the specified name.
/// </returns>
public virtual Task<TScope> FindByNameAsync([NotNull] string name, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("The scope name cannot be null or empty.", nameof(name));
}
return Store.FindByNameAsync(name, cancellationToken);
}
/// <summary>
/// Retrieves a list of scopes using their name.
/// </summary>
/// <param name="names">The names associated with the scopes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes corresponding to the specified names.
/// </returns>
public virtual Task<ImmutableArray<TScope>> FindByNamesAsync(
ImmutableArray<string> names, CancellationToken cancellationToken = default)
{
if (names.Any(name => string.IsNullOrEmpty(name)))
{
throw new ArgumentException("Scope names cannot be null or empty.", nameof(names));
}
return Store.FindByNamesAsync(names, cancellationToken);
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
@ -202,6 +241,25 @@ namespace OpenIddict.Core
return Store.GetAsync(query, state, cancellationToken);
}
/// <summary>
/// Retrieves the description associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the description associated with the specified scope.
/// </returns>
public virtual Task<string> GetDescriptionAsync([NotNull] TScope scope, CancellationToken cancellationToken = default)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return Store.GetDescriptionAsync(scope, cancellationToken);
}
/// <summary>
/// Retrieves the unique identifier associated with a scope.
/// </summary>
@ -221,6 +279,25 @@ namespace OpenIddict.Core
return Store.GetIdAsync(scope, cancellationToken);
}
/// <summary>
/// Retrieves the name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the name associated with the specified scope.
/// </returns>
public virtual Task<string> GetNameAsync([NotNull] TScope scope, CancellationToken cancellationToken = default)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return Store.GetIdAsync(scope, cancellationToken);
}
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
@ -356,6 +433,34 @@ namespace OpenIddict.Core
return results.ToImmutable();
}
/// <summary>
/// Validates the list of scopes to ensure they correspond to existing elements in the database.
/// </summary>
/// <param name="scopes">The scopes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the list of scopes is valid, <c>false</c> otherwise.</returns>
public virtual async Task<bool> ValidateScopesAsync(ImmutableArray<string> scopes, CancellationToken cancellationToken = default)
{
if (scopes.Length == 0)
{
return true;
}
async Task<ImmutableHashSet<string>> GetScopesAsync()
{
var names = ImmutableHashSet.CreateBuilder(StringComparer.Ordinal);
foreach (var scope in await FindByNamesAsync(scopes, cancellationToken))
{
names.Add(await GetNameAsync(scope, cancellationToken));
}
return names.ToImmutable();
}
return (await GetScopesAsync()).IsSupersetOf(scopes);
}
/// <summary>
/// Populates the scope using the specified descriptor.
/// </summary>

1
src/OpenIddict.Core/OpenIddictConstants.cs

@ -61,6 +61,7 @@ namespace OpenIddict.Core
{
public const string Endpoint = "ept:";
public const string GrantType = "gt:";
public const string Scope = "scp:";
}
}

22
src/OpenIddict.Core/Stores/IOpenIddictScopeStore.cs

@ -73,6 +73,28 @@ namespace OpenIddict.Core
/// </returns>
Task<TScope> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves a scope using its name.
/// </summary>
/// <param name="name">The name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the specified name.
/// </returns>
Task<TScope> FindByNameAsync([NotNull] string name, CancellationToken cancellationToken);
/// <summary>
/// Retrieves a list of scopes using their name.
/// </summary>
/// <param name="names">The names associated with the scopes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes corresponding to the specified names.
/// </returns>
Task<ImmutableArray<TScope>> FindByNamesAsync(ImmutableArray<string> names, CancellationToken cancellationToken);
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>

49
src/OpenIddict.Core/Stores/OpenIddictScopeStore.cs

@ -96,6 +96,55 @@ namespace OpenIddict.Core
return GetAsync((scopes, key) => Query(scopes, key), ConvertIdentifierFromString(identifier), cancellationToken);
}
/// <summary>
/// Retrieves a scope using its name.
/// </summary>
/// <param name="name">The name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the specified name.
/// </returns>
public virtual Task<TScope> FindByNameAsync([NotNull] string name, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("The scope name cannot be null or empty.", nameof(name));
}
IQueryable<TScope> Query(IQueryable<TScope> scopes, string state)
=> from scope in scopes
where scope.Name == state
select scope;
return GetAsync((scopes, state) => Query(scopes, state), name, cancellationToken);
}
/// <summary>
/// Retrieves a list of scopes using their name.
/// </summary>
/// <param name="names">The names associated with the scopes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes corresponding to the specified names.
/// </returns>
public virtual Task<ImmutableArray<TScope>> FindByNamesAsync(
ImmutableArray<string> names, CancellationToken cancellationToken)
{
if (names.Any(name => string.IsNullOrEmpty(name)))
{
throw new ArgumentException("Scope names cannot be null or empty.", nameof(names));
}
IQueryable<TScope> Query(IQueryable<TScope> scopes, string[] values)
=> from scope in scopes
where values.Contains(scope.Name)
select scope;
return ListAsync((scopes, values) => Query(scopes, values), names.ToArray(), cancellationToken);
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>

16
src/OpenIddict/OpenIddictExtensions.cs

@ -21,6 +21,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict;
using OpenIddict.Core;
namespace Microsoft.Extensions.DependencyInjection
{
@ -730,6 +731,21 @@ namespace Microsoft.Extensions.DependencyInjection
return builder.Configure(options => options.UserinfoEndpointPath = path);
}
/// <summary>
/// Rejects authorization and token requests that specify scopes that have not been
/// registered in the database using <see cref="OpenIddictScopeManager{TScope}"/>.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
public static OpenIddictBuilder ValidateScopes([NotNull] this OpenIddictBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
return builder.Configure(options => options.ValidateScopes = true);
}
/// <summary>
/// Makes client identification mandatory so that token and revocation
/// requests that don't specify a client_id are automatically rejected.

6
src/OpenIddict/OpenIddictOptions.cs

@ -73,6 +73,12 @@ namespace OpenIddict
/// </summary>
public RandomNumberGenerator RandomNumberGenerator { get; set; } = RandomNumberGenerator.Create();
/// <summary>
/// Gets or sets a boolean indicating whether scopes that are not explicitly registered
/// in the database are automatically rejected. This option is not enabled by default.
/// </summary>
public bool ValidateScopes { get; set; }
/// <summary>
/// Gets or sets a boolean determining whether client identification is required.
/// Enabling this option requires registering a client application and sending a

40
src/OpenIddict/OpenIddictProvider.Authentication.cs

@ -5,6 +5,7 @@
*/
using System;
using System.Collections.Immutable;
using System.IO;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
@ -165,6 +166,22 @@ namespace OpenIddict
return;
}
// If the corresponding option was enabled, reject the request if scopes can't be validated.
if (options.ValidateScopes && !await Scopes.ValidateScopesAsync(
context.Request.GetScopes()
.ToImmutableArray()
.Remove(OpenIdConnectConstants.Scopes.OfflineAccess)
.Remove(OpenIdConnectConstants.Scopes.OpenId)))
{
Logger.LogError("The authorization request was rejected because an unregistered scope was specified.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The specified 'scope' parameter is not valid.");
return;
}
// Reject authorization requests that specify scope=offline_access if the refresh token flow is not enabled.
if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) &&
!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))
@ -369,6 +386,29 @@ namespace OpenIddict
return;
}
foreach (var scope in context.Request.GetScopes())
{
// Avoid validating the "openid" and "offline_access" scopes as they represent protocol scopes.
if (string.Equals(scope, OpenIdConnectConstants.Scopes.OfflineAccess, StringComparison.Ordinal) ||
string.Equals(scope, OpenIdConnectConstants.Scopes.OpenId, StringComparison.Ordinal))
{
continue;
}
// Reject the request if the application is not allowed to use the iterated scope.
if (!await Applications.HasPermissionAsync(application, OpenIddictConstants.Permissions.Prefixes.Scope + scope))
{
Logger.LogError("The authorization request was rejected because the application '{ClientId}' " +
"was not allowed to use the scope {Scope}.", context.ClientId, scope);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "This client application is not allowed to use the specified scope.");
return;
}
}
context.Validate();
}

42
src/OpenIddict/OpenIddictProvider.Exchange.cs

@ -4,6 +4,8 @@
* the license and the contributors participating to this project.
*/
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
@ -60,6 +62,22 @@ namespace OpenIddict
return;
}
// If the corresponding option was enabled, reject the request if scopes can't be validated.
if (options.ValidateScopes && !await Scopes.ValidateScopesAsync(
context.Request.GetScopes()
.ToImmutableArray()
.Remove(OpenIdConnectConstants.Scopes.OfflineAccess)
.Remove(OpenIdConnectConstants.Scopes.OpenId)))
{
Logger.LogError("The token request was rejected because an unregistered scope was specified.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The specified 'scope' parameter is not valid.");
return;
}
// Note: the OpenID Connect server middleware allows returning a refresh token with grant_type=client_credentials,
// though it's usually not recommended by the OAuth2 specification. To encourage developers to make a new
// grant_type=client_credentials request instead of using refresh tokens, OpenIddict uses a stricter policy
@ -226,6 +244,30 @@ namespace OpenIddict
return;
}
foreach (var scope in context.Request.GetScopes())
{
// Avoid validating the "openid" and "offline_access" scopes as they represent protocol scopes.
if (string.Equals(scope, OpenIdConnectConstants.Scopes.OfflineAccess, StringComparison.Ordinal) ||
string.Equals(scope, OpenIdConnectConstants.Scopes.OpenId, StringComparison.Ordinal))
{
continue;
}
// Reject the request if the application is not allowed to use the iterated scope.
if (!await Applications.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Prefixes.Scope + scope))
{
Logger.LogError("The token request was rejected because the application '{ClientId}' " +
"was not allowed to use the scope {Scope}.", context.ClientId, scope);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "This client application is not allowed to use the specified scope.");
return;
}
}
context.Validate();
}

171
test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs

@ -4,6 +4,7 @@
* the license and the contributors participating to this project.
*/
using System.Collections.Immutable;
using System.IO;
using System.Security.Cryptography;
using System.Threading;
@ -192,6 +193,105 @@ namespace OpenIddict.Tests
Assert.Equal("The specified 'response_type' parameter is not allowed.", response.ErrorDescription);
}
[Fact]
public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenUnregisteredScopeIsSpecified()
{
// Arrange
var manager = CreateScopeManager(instance =>
{
instance.Setup(mock => mock.ValidateScopesAsync(
It.Is<ImmutableArray<string>>(scopes => scopes[0] == "unregistered_scope"),
It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.ValidateScopes();
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest
{
ClientId = "Fabrikam",
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = OpenIdConnectConstants.ResponseTypes.Code,
Scope = "unregistered_scope"
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error);
Assert.Equal("The specified 'scope' parameter is not valid.", response.ErrorDescription);
}
[Fact]
public async Task ValidateAuthorizationRequest_RequestIsValidatedWhenRegisteredScopeIsSpecified()
{
// Arrange
var manager = CreateScopeManager(instance =>
{
instance.Setup(mock => mock.ValidateScopesAsync(
It.Is<ImmutableArray<string>>(scopes => scopes[0] == "registered_scope"),
It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(OpenIddictConstants.ClientTypes.Public);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.GrantTypes.Implicit, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Prefixes.Scope + "registered_scope", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
builder.Services.AddSingleton(manager);
builder.ValidateScopes();
});
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 = OpenIdConnectConstants.ResponseTypes.Token,
Scope = "registered_scope"
});
// Assert
Assert.Null(response.Error);
Assert.Null(response.ErrorDescription);
Assert.Null(response.ErrorUri);
Assert.NotNull(response.AccessToken);
}
[Fact]
public async Task ValidateAuthorizationRequest_RequestWithOfflineAccessScopeIsRejectedWhenRefreshTokenFlowIsDisabled()
{
@ -578,6 +678,77 @@ namespace OpenIddict.Tests
Mock.Get(manager).Verify(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenScopePermissionIsNotGranted()
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(instance =>
{
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Prefixes.Scope +
OpenIdConnectConstants.Scopes.Profile, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Prefixes.Scope +
OpenIdConnectConstants.Scopes.Email, It.IsAny<CancellationToken>()))
.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,
Scope = "openid offline_access profile email"
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error);
Assert.Equal("This client application is not allowed to use the specified scope.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Prefixes.Scope +
OpenIdConnectConstants.Scopes.OpenId, It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Prefixes.Scope +
OpenIdConnectConstants.Scopes.OfflineAccess, It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Prefixes.Scope +
OpenIdConnectConstants.Scopes.Profile, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Prefixes.Scope +
OpenIdConnectConstants.Scopes.Email, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleAuthorizationRequest_RequestIsPersistedInDistributedCache()
{

153
test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs

@ -101,6 +101,68 @@ namespace OpenIddict.Tests
Assert.Equal("The mandatory 'redirect_uri' parameter is missing.", response.ErrorDescription);
}
[Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenUnregisteredScopeIsSpecified()
{
// Arrange
var server = CreateAuthorizationServer(options =>
{
options.ValidateScopes();
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w",
Scope = "unregistered_scope"
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error);
Assert.Equal("The specified 'scope' parameter is not valid.", response.ErrorDescription);
}
[Fact]
public async Task ValidateTokenRequest_RequestIsValidatedWhenRegisteredScopeIsSpecified()
{
// Arrange
var manager = CreateScopeManager(instance =>
{
instance.Setup(mock => mock.ValidateScopesAsync(
It.Is<ImmutableArray<string>>(scopes => scopes[0] == "registered_scope"),
It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.ValidateScopes();
builder.RegisterScopes("registered_scope");
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
GrantType = OpenIdConnectConstants.GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w",
Scope = "registered_scope"
});
// Assert
Assert.Null(response.Error);
Assert.Null(response.ErrorDescription);
Assert.Null(response.ErrorUri);
Assert.NotNull(response.AccessToken);
}
[Fact]
public async Task ValidateTokenRequest_ClientCredentialsRequestWithOfflineAccessScopeIsRejected()
{
@ -550,6 +612,85 @@ namespace OpenIddict.Tests
Mock.Get(manager).Verify(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenScopePermissionIsNotGranted()
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(instance =>
{
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.GrantTypes.Password, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential);
instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Prefixes.Scope +
OpenIdConnectConstants.Scopes.Profile, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Prefixes.Scope +
OpenIdConnectConstants.Scopes.Email, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(manager);
});
var client = new OpenIdConnectClient(server.CreateClient());
// Act
var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest
{
ClientId = "Fabrikam",
ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
GrantType = OpenIdConnectConstants.GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w",
Scope = "openid offline_access profile email"
});
// Assert
Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error);
Assert.Equal("This client application is not allowed to use the specified scope.", response.ErrorDescription);
Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Prefixes.Scope +
OpenIdConnectConstants.Scopes.OpenId, It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Prefixes.Scope +
OpenIdConnectConstants.Scopes.OfflineAccess, It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Prefixes.Scope +
OpenIdConnectConstants.Scopes.Profile, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Prefixes.Scope +
OpenIdConnectConstants.Scopes.Email, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task HandleTokenRequest_AuthorizationCodeRevocationIsIgnoredWhenTokenRevocationIsDisabled()
{
@ -1166,6 +1307,12 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.GetIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
instance.Setup(mock => mock.GetIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("481FCAC6-06BC-43EE-92DB-37A78AA09B59");
instance.Setup(mock => mock.GetIdAsync(tokens[2], It.IsAny<CancellationToken>()))
.ReturnsAsync("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8");
instance.Setup(mock => mock.GetAuthorizationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
@ -1263,6 +1410,12 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.GetIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
instance.Setup(mock => mock.GetIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("481FCAC6-06BC-43EE-92DB-37A78AA09B59");
instance.Setup(mock => mock.GetIdAsync(tokens[2], It.IsAny<CancellationToken>()))
.ReturnsAsync("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8");
instance.Setup(mock => mock.GetAuthorizationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");

12
test/OpenIddict.Tests/OpenIddictProviderTests.cs

@ -789,6 +789,12 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.GetIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
instance.Setup(mock => mock.GetIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("481FCAC6-06BC-43EE-92DB-37A78AA09B59");
instance.Setup(mock => mock.GetIdAsync(tokens[2], It.IsAny<CancellationToken>()))
.ReturnsAsync("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8");
instance.Setup(mock => mock.GetAuthorizationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
@ -864,6 +870,12 @@ namespace OpenIddict.Tests
instance.Setup(mock => mock.GetIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
instance.Setup(mock => mock.GetIdAsync(tokens[1], It.IsAny<CancellationToken>()))
.ReturnsAsync("481FCAC6-06BC-43EE-92DB-37A78AA09B59");
instance.Setup(mock => mock.GetIdAsync(tokens[2], It.IsAny<CancellationToken>()))
.ReturnsAsync("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8");
instance.Setup(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(false);

Loading…
Cancel
Save