Browse Source

Add integration tests for the PKCE enforcement feature

pull/899/head
Kévin Chalet 6 years ago
parent
commit
dae66ef974
  1. 96
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
  2. 106
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  3. 145
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs
  4. 352
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs

96
src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs

@ -54,8 +54,8 @@ namespace OpenIddict.Server
ValidateScopes.Descriptor, ValidateScopes.Descriptor,
ValidateEndpointPermissions.Descriptor, ValidateEndpointPermissions.Descriptor,
ValidateGrantTypePermissions.Descriptor, ValidateGrantTypePermissions.Descriptor,
ValidateProofKeyForCodeExchangeRequirement.Descriptor,
ValidateScopePermissions.Descriptor, ValidateScopePermissions.Descriptor,
ValidateProofKeyForCodeExchangeRequirement.Descriptor,
/* /*
* Authorization response processing: * Authorization response processing:
@ -1469,15 +1469,14 @@ namespace OpenIddict.Server
} }
/// <summary> /// <summary>
/// Contains the logic responsible of rejecting authorization requests made by /// Contains the logic responsible of rejecting authorization requests made by unauthorized applications.
/// applications for which proof key for code exchange (PKCE) was enforced. /// Note: this handler is not used when the degraded mode is enabled or when scope permissions are disabled.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary> /// </summary>
public class ValidateProofKeyForCodeExchangeRequirement : IOpenIddictServerHandler<ValidateAuthorizationRequestContext> public class ValidateScopePermissions : IOpenIddictServerHandler<ValidateAuthorizationRequestContext>
{ {
private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictApplicationManager _applicationManager;
public ValidateProofKeyForCodeExchangeRequirement() => throw new InvalidOperationException(new StringBuilder() public ValidateScopePermissions() => throw new InvalidOperationException(new StringBuilder()
.AppendLine("The core services must be registered when enabling the OpenIddict server feature.") .AppendLine("The core services must be registered when enabling the OpenIddict server feature.")
.Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
.AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
@ -1485,7 +1484,7 @@ namespace OpenIddict.Server
.Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.")
.ToString()); .ToString());
public ValidateProofKeyForCodeExchangeRequirement([NotNull] IOpenIddictApplicationManager applicationManager) public ValidateScopePermissions([NotNull] IOpenIddictApplicationManager applicationManager)
=> _applicationManager = applicationManager; => _applicationManager = applicationManager;
/// <summary> /// <summary>
@ -1493,8 +1492,9 @@ namespace OpenIddict.Server
/// </summary> /// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.AddFilter<RequireScopePermissionsEnabled>()
.AddFilter<RequireDegradedModeDisabled>() .AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler<ValidateProofKeyForCodeExchangeRequirement>() .UseScopedHandler<ValidateScopePermissions>()
.SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000) .SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000)
.Build(); .Build();
@ -1512,42 +1512,47 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
// If a code_challenge was provided, the request is always considered valid,
// whether the proof key for code exchange requirement is enforced or not.
if (!string.IsNullOrEmpty(context.Request.CodeChallenge))
{
return;
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId); var application = await _applicationManager.FindByClientIdAsync(context.ClientId);
if (application == null) if (application == null)
{ {
throw new InvalidOperationException("The client application details cannot be found in the database."); throw new InvalidOperationException("The client application details cannot be found in the database.");
} }
if (await _applicationManager.HasRequirementAsync(application, Requirements.Features.ProofKeyForCodeExchange)) foreach (var scope in context.Request.GetScopes())
{ {
context.Logger.LogError("The authorization request was rejected because the " + // Avoid validating the "openid" and "offline_access" scopes as they represent protocol scopes.
"required 'code_challenge' parameter was missing."); if (string.Equals(scope, Scopes.OfflineAccess, StringComparison.Ordinal) ||
string.Equals(scope, Scopes.OpenId, StringComparison.Ordinal))
{
continue;
}
context.Reject( // Reject the request if the application is not allowed to use the iterated scope.
error: Errors.InvalidRequest, if (!await _applicationManager.HasPermissionAsync(application, Permissions.Prefixes.Scope + scope))
description: "The mandatory 'code_challenge' parameter is missing."); {
context.Logger.LogError("The authorization request was rejected because the application '{ClientId}' " +
"was not allowed to use the scope {Scope}.", context.ClientId, scope);
return; context.Reject(
error: Errors.InvalidRequest,
description: "This client application is not allowed to use the specified scope.");
return;
}
} }
} }
} }
/// <summary> /// <summary>
/// Contains the logic responsible of rejecting authorization requests made by unauthorized applications. /// Contains the logic responsible of rejecting authorization requests made by
/// Note: this handler is not used when the degraded mode is enabled or when scope permissions are disabled. /// applications for which proof key for code exchange (PKCE) was enforced.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary> /// </summary>
public class ValidateScopePermissions : IOpenIddictServerHandler<ValidateAuthorizationRequestContext> public class ValidateProofKeyForCodeExchangeRequirement : IOpenIddictServerHandler<ValidateAuthorizationRequestContext>
{ {
private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictApplicationManager _applicationManager;
public ValidateScopePermissions() => throw new InvalidOperationException(new StringBuilder() public ValidateProofKeyForCodeExchangeRequirement() => throw new InvalidOperationException(new StringBuilder()
.AppendLine("The core services must be registered when enabling the OpenIddict server feature.") .AppendLine("The core services must be registered when enabling the OpenIddict server feature.")
.Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
.AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
@ -1555,7 +1560,7 @@ namespace OpenIddict.Server
.Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.")
.ToString()); .ToString());
public ValidateScopePermissions([NotNull] IOpenIddictApplicationManager applicationManager) public ValidateProofKeyForCodeExchangeRequirement([NotNull] IOpenIddictApplicationManager applicationManager)
=> _applicationManager = applicationManager; => _applicationManager = applicationManager;
/// <summary> /// <summary>
@ -1563,10 +1568,9 @@ namespace OpenIddict.Server
/// </summary> /// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.AddFilter<RequireScopePermissionsEnabled>()
.AddFilter<RequireDegradedModeDisabled>() .AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler<ValidateScopePermissions>() .UseScopedHandler<ValidateProofKeyForCodeExchangeRequirement>()
.SetOrder(ValidateProofKeyForCodeExchangeRequirement.Descriptor.Order + 1_000) .SetOrder(ValidateScopePermissions.Descriptor.Order + 1_000)
.Build(); .Build();
/// <summary> /// <summary>
@ -1583,33 +1587,29 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
// If a code_challenge was provided, the request is always considered valid,
// whether the proof key for code exchange requirement is enforced or not.
if (!string.IsNullOrEmpty(context.Request.CodeChallenge))
{
return;
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId); var application = await _applicationManager.FindByClientIdAsync(context.ClientId);
if (application == null) if (application == null)
{ {
throw new InvalidOperationException("The client application details cannot be found in the database."); throw new InvalidOperationException("The client application details cannot be found in the database.");
} }
foreach (var scope in context.Request.GetScopes()) if (await _applicationManager.HasRequirementAsync(application, Requirements.Features.ProofKeyForCodeExchange))
{ {
// Avoid validating the "openid" and "offline_access" scopes as they represent protocol scopes. context.Logger.LogError("The authorization request was rejected because the " +
if (string.Equals(scope, Scopes.OfflineAccess, StringComparison.Ordinal) || "required 'code_challenge' parameter was missing.");
string.Equals(scope, Scopes.OpenId, StringComparison.Ordinal))
{
continue;
}
// Reject the request if the application is not allowed to use the iterated scope.
if (!await _applicationManager.HasPermissionAsync(application, Permissions.Prefixes.Scope + scope))
{
context.Logger.LogError("The authorization request was rejected because the application '{ClientId}' " +
"was not allowed to use the scope {Scope}.", context.ClientId, scope);
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidRequest,
description: "This client application is not allowed to use the specified scope."); description: "The mandatory 'code_challenge' parameter is missing.");
return; return;
}
} }
} }
} }

106
src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs

@ -54,8 +54,8 @@ namespace OpenIddict.Server
ValidateClientSecret.Descriptor, ValidateClientSecret.Descriptor,
ValidateEndpointPermissions.Descriptor, ValidateEndpointPermissions.Descriptor,
ValidateGrantTypePermissions.Descriptor, ValidateGrantTypePermissions.Descriptor,
ValidateProofKeyForCodeExchangeRequirement.Descriptor,
ValidateScopePermissions.Descriptor, ValidateScopePermissions.Descriptor,
ValidateProofKeyForCodeExchangeRequirement.Descriptor,
ValidateToken.Descriptor, ValidateToken.Descriptor,
ValidatePresenters.Descriptor, ValidatePresenters.Descriptor,
ValidateRedirectUri.Descriptor, ValidateRedirectUri.Descriptor,
@ -1171,15 +1171,15 @@ namespace OpenIddict.Server
} }
/// <summary> /// <summary>
/// Contains the logic responsible of rejecting token requests made by /// Contains the logic responsible of rejecting token requests made by applications
/// applications for which proof key for code exchange (PKCE) was enforced. /// that haven't been granted the appropriate grant type permission.
/// Note: this handler is not used when the degraded mode is enabled. /// Note: this handler is not used when the degraded mode is enabled.
/// </summary> /// </summary>
public class ValidateProofKeyForCodeExchangeRequirement : IOpenIddictServerHandler<ValidateTokenRequestContext> public class ValidateScopePermissions : IOpenIddictServerHandler<ValidateTokenRequestContext>
{ {
private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictApplicationManager _applicationManager;
public ValidateProofKeyForCodeExchangeRequirement() => throw new InvalidOperationException(new StringBuilder() public ValidateScopePermissions() => throw new InvalidOperationException(new StringBuilder()
.AppendLine("The core services must be registered when enabling the OpenIddict server feature.") .AppendLine("The core services must be registered when enabling the OpenIddict server feature.")
.Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
.AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
@ -1187,7 +1187,7 @@ namespace OpenIddict.Server
.Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.")
.ToString()); .ToString());
public ValidateProofKeyForCodeExchangeRequirement([NotNull] IOpenIddictApplicationManager applicationManager) public ValidateScopePermissions([NotNull] IOpenIddictApplicationManager applicationManager)
=> _applicationManager = applicationManager; => _applicationManager = applicationManager;
/// <summary> /// <summary>
@ -1197,7 +1197,8 @@ namespace OpenIddict.Server
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.AddFilter<RequireClientIdParameter>() .AddFilter<RequireClientIdParameter>()
.AddFilter<RequireDegradedModeDisabled>() .AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler<ValidateProofKeyForCodeExchangeRequirement>() .AddFilter<RequireScopePermissionsEnabled>()
.UseScopedHandler<ValidateScopePermissions>()
.SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000) .SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000)
.Build(); .Build();
@ -1215,48 +1216,47 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (!context.Request.IsAuthorizationCodeGrantType())
{
return;
}
// If a code_verifier was provided, the request is always considered valid,
// whether the proof key for code exchange requirement is enforced or not.
if (!string.IsNullOrEmpty(context.Request.CodeVerifier))
{
return;
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId); var application = await _applicationManager.FindByClientIdAsync(context.ClientId);
if (application == null) if (application == null)
{ {
throw new InvalidOperationException("The client application details cannot be found in the database."); throw new InvalidOperationException("The client application details cannot be found in the database.");
} }
if (await _applicationManager.HasRequirementAsync(application, Requirements.Features.ProofKeyForCodeExchange)) foreach (var scope in context.Request.GetScopes())
{ {
context.Logger.LogError("The token request was rejected because the " + // Avoid validating the "openid" and "offline_access" scopes as they represent protocol scopes.
"required 'code_verifier' parameter was missing."); if (string.Equals(scope, Scopes.OfflineAccess, StringComparison.Ordinal) ||
string.Equals(scope, Scopes.OpenId, StringComparison.Ordinal))
{
continue;
}
context.Reject( // Reject the request if the application is not allowed to use the iterated scope.
error: Errors.InvalidRequest, if (!await _applicationManager.HasPermissionAsync(application, Permissions.Prefixes.Scope + scope))
description: "The mandatory 'code_verifier' parameter is missing."); {
context.Logger.LogError("The token request was rejected because the application '{ClientId}' " +
"was not allowed to use the scope {Scope}.", context.ClientId, scope);
return; context.Reject(
error: Errors.InvalidRequest,
description: "This client application is not allowed to use the specified scope.");
return;
}
} }
} }
} }
/// <summary> /// <summary>
/// Contains the logic responsible of rejecting token requests made by applications /// Contains the logic responsible of rejecting token requests made by
/// that haven't been granted the appropriate grant type permission. /// applications for which proof key for code exchange (PKCE) was enforced.
/// Note: this handler is not used when the degraded mode is enabled. /// Note: this handler is not used when the degraded mode is enabled.
/// </summary> /// </summary>
public class ValidateScopePermissions : IOpenIddictServerHandler<ValidateTokenRequestContext> public class ValidateProofKeyForCodeExchangeRequirement : IOpenIddictServerHandler<ValidateTokenRequestContext>
{ {
private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictApplicationManager _applicationManager;
public ValidateScopePermissions() => throw new InvalidOperationException(new StringBuilder() public ValidateProofKeyForCodeExchangeRequirement() => throw new InvalidOperationException(new StringBuilder()
.AppendLine("The core services must be registered when enabling the OpenIddict server feature.") .AppendLine("The core services must be registered when enabling the OpenIddict server feature.")
.Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
.AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
@ -1264,7 +1264,7 @@ namespace OpenIddict.Server
.Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.")
.ToString()); .ToString());
public ValidateScopePermissions([NotNull] IOpenIddictApplicationManager applicationManager) public ValidateProofKeyForCodeExchangeRequirement([NotNull] IOpenIddictApplicationManager applicationManager)
=> _applicationManager = applicationManager; => _applicationManager = applicationManager;
/// <summary> /// <summary>
@ -1274,9 +1274,8 @@ namespace OpenIddict.Server
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.AddFilter<RequireClientIdParameter>() .AddFilter<RequireClientIdParameter>()
.AddFilter<RequireDegradedModeDisabled>() .AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireScopePermissionsEnabled>() .UseScopedHandler<ValidateProofKeyForCodeExchangeRequirement>()
.UseScopedHandler<ValidateScopePermissions>() .SetOrder(ValidateScopePermissions.Descriptor.Order + 1_000)
.SetOrder(ValidateProofKeyForCodeExchangeRequirement.Descriptor.Order + 1_000)
.Build(); .Build();
/// <summary> /// <summary>
@ -1293,33 +1292,34 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (!context.Request.IsAuthorizationCodeGrantType())
{
return;
}
// If a code_verifier was provided, the request is always considered valid,
// whether the proof key for code exchange requirement is enforced or not.
if (!string.IsNullOrEmpty(context.Request.CodeVerifier))
{
return;
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId); var application = await _applicationManager.FindByClientIdAsync(context.ClientId);
if (application == null) if (application == null)
{ {
throw new InvalidOperationException("The client application details cannot be found in the database."); throw new InvalidOperationException("The client application details cannot be found in the database.");
} }
foreach (var scope in context.Request.GetScopes()) if (await _applicationManager.HasRequirementAsync(application, Requirements.Features.ProofKeyForCodeExchange))
{ {
// Avoid validating the "openid" and "offline_access" scopes as they represent protocol scopes. context.Logger.LogError("The token request was rejected because the " +
if (string.Equals(scope, Scopes.OfflineAccess, StringComparison.Ordinal) || "required 'code_verifier' parameter was missing.");
string.Equals(scope, Scopes.OpenId, StringComparison.Ordinal))
{
continue;
}
// Reject the request if the application is not allowed to use the iterated scope.
if (!await _applicationManager.HasPermissionAsync(application, Permissions.Prefixes.Scope + scope))
{
context.Logger.LogError("The token request was rejected because the application '{ClientId}' " +
"was not allowed to use the scope {Scope}.", context.ClientId, scope);
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidRequest,
description: "This client application is not allowed to use the specified scope."); description: "The mandatory 'code_verifier' parameter is missing.");
return; return;
}
} }
} }
} }
@ -1341,7 +1341,7 @@ namespace OpenIddict.Server
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.UseScopedHandler<ValidateToken>() .UseScopedHandler<ValidateToken>()
.SetOrder(ValidateScopePermissions.Descriptor.Order + 1_000) .SetOrder(ValidateProofKeyForCodeExchangeRequirement.Descriptor.Order + 1_000)
.Build(); .Build();
/// <summary> /// <summary>

145
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs

@ -1283,6 +1283,151 @@ namespace OpenIddict.Server.FunctionalTests
Permissions.Prefixes.Scope + Scopes.Email, It.IsAny<CancellationToken>()), Times.Once()); Permissions.Prefixes.Scope + Scopes.Email, It.IsAny<CancellationToken>()), Times.Once());
} }
[Fact]
public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenCodeChallengeIsMissingWithPkceFeatureEnforced()
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(mock =>
{
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasRequirementAsync(application,
Requirements.Features.ProofKeyForCodeExchange, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
var client = CreateClient(options =>
{
options.Services.AddSingleton(manager);
});
// Act
var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
{
ClientId = "Fabrikam",
CodeChallenge = null,
CodeChallengeMethod = null,
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = ResponseTypes.Code
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal("The mandatory 'code_challenge' parameter is missing.", response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasRequirementAsync(application,
Requirements.Features.ProofKeyForCodeExchange, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateAuthorizationRequest_RequestIsValidatedWhenCodeChallengeIsMissingWithPkceFeatureNotEnforced()
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(mock =>
{
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasRequirementAsync(application,
Requirements.Features.ProofKeyForCodeExchange, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
var client = CreateClient(options =>
{
options.Services.AddSingleton(manager);
options.AddEventHandler<HandleAuthorizationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetClaim(Claims.Subject, "Bob le Magnifique");
return default;
}));
});
// Act
var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
{
ClientId = "Fabrikam",
CodeChallenge = null,
CodeChallengeMethod = null,
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = ResponseTypes.Code
});
// Assert
Assert.NotNull(response.Code);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasRequirementAsync(application,
Requirements.Features.ProofKeyForCodeExchange, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateAuthorizationRequest_RequestIsValidatedWhenCodeChallengeIsPresentWithPkceFeatureEnforced()
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(mock =>
{
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasRequirementAsync(application,
Requirements.Features.ProofKeyForCodeExchange, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
var client = CreateClient(options =>
{
options.Services.AddSingleton(manager);
options.AddEventHandler<HandleAuthorizationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetClaim(Claims.Subject, "Bob le Magnifique");
return default;
}));
});
// Act
var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
{
ClientId = "Fabrikam",
CodeChallenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
CodeChallengeMethod = CodeChallengeMethods.Sha256,
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = ResponseTypes.Code
});
// Assert
Assert.NotNull(response.Code);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasRequirementAsync(application,
Requirements.Features.ProofKeyForCodeExchange, It.IsAny<CancellationToken>()), Times.Never());
}
[Theory] [Theory]
[InlineData("custom_error", null, null)] [InlineData("custom_error", null, null)]
[InlineData("custom_error", "custom_description", null)] [InlineData("custom_error", "custom_description", null)]

352
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs

@ -1192,7 +1192,7 @@ namespace OpenIddict.Server.FunctionalTests
} }
[Fact] [Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() public async Task ValidateTokenRequest_ClientCredentialsRequestFromPublicClientIsRejected()
{ {
// Arrange // Arrange
var application = new OpenIddictApplication(); var application = new OpenIddictApplication();
@ -1202,38 +1202,71 @@ namespace OpenIddict.Server.FunctionalTests
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .ReturnsAsync(application);
mock.Setup(manager => manager.HasPermissionAsync(application, mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
Permissions.Endpoints.Token, It.IsAny<CancellationToken>())) .ReturnsAsync(ClientTypes.Public);
.ReturnsAsync(false);
}); });
var client = CreateClient(options => var client = CreateClient(options =>
{ {
options.Services.AddSingleton(manager); options.Services.AddSingleton(manager);
});
options.Configure(options => options.IgnoreEndpointPermissions = false); // Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientId = "Fabrikam",
ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
GrantType = GrantTypes.ClientCredentials
});
// Assert
Assert.Equal(Errors.UnauthorizedClient, response.Error);
Assert.Equal("The specified 'grant_type' parameter is not valid for this client application.", response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateTokenRequest_ClientSecretCannotBeUsedByPublicClients()
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(mock =>
{
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(ClientTypes.Public);
});
var client = CreateClient(options =>
{
options.Services.AddSingleton(manager);
}); });
// Act // Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{ {
ClientId = "Fabrikam", ClientId = "Fabrikam",
ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
GrantType = GrantTypes.Password, GrantType = GrantTypes.Password,
Username = "johndoe", Username = "johndoe",
Password = "A3ddj3w" Password = "A3ddj3w"
}); });
// Assert // Assert
Assert.Equal(Errors.UnauthorizedClient, response.Error); Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal("This client application is not allowed to use the token endpoint.", response.ErrorDescription); Assert.Equal("The 'client_secret' parameter is not valid for this client application.", response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()), Times.Once());
Permissions.Endpoints.Token, It.IsAny<CancellationToken>()), Times.Once());
} }
[Fact] [Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenGrantTypePermissionIsNotGranted() public async Task ValidateTokenRequest_ClientSecretIsRequiredForConfidentialClients()
{ {
// Arrange // Arrange
var application = new OpenIddictApplication(); var application = new OpenIddictApplication();
@ -1243,38 +1276,73 @@ namespace OpenIddict.Server.FunctionalTests
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .ReturnsAsync(application);
mock.Setup(manager => manager.HasPermissionAsync(application, mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
Permissions.GrantTypes.Password, It.IsAny<CancellationToken>())) .ReturnsAsync(ClientTypes.Confidential);
.ReturnsAsync(false);
}); });
var client = CreateClient(options => var client = CreateClient(options =>
{ {
options.Services.AddSingleton(manager); options.Services.AddSingleton(manager);
});
options.Configure(options => options.IgnoreGrantTypePermissions = false); // Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientId = "Fabrikam",
ClientSecret = null,
GrantType = GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w"
});
// Assert
Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal("The 'client_secret' parameter required for this client application is missing.", response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateTokenRequest_ClientSecretIsRequiredForHybridClients()
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(mock =>
{
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(ClientTypes.Hybrid);
});
var client = CreateClient(options =>
{
options.Services.AddSingleton(manager);
}); });
// Act // Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{ {
ClientId = "Fabrikam", ClientId = "Fabrikam",
ClientSecret = null,
GrantType = GrantTypes.Password, GrantType = GrantTypes.Password,
Username = "johndoe", Username = "johndoe",
Password = "A3ddj3w" Password = "A3ddj3w"
}); });
// Assert // Assert
Assert.Equal(Errors.UnauthorizedClient, response.Error); Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal("This client application is not allowed to use the specified grant type.", response.ErrorDescription); Assert.Equal("The 'client_secret' parameter required for this client application is missing.", response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()), Times.Once());
Permissions.GrantTypes.Password, It.IsAny<CancellationToken>()), Times.Once());
} }
[Fact] [Fact]
public async Task ValidateTokenRequest_RequestWithOfflineAccessScopeIsRejectedWhenRefreshTokenPermissionIsNotGranted() public async Task ValidateTokenRequest_RequestIsRejectedWhenClientCredentialsAreInvalid()
{ {
// Arrange // Arrange
var application = new OpenIddictApplication(); var application = new OpenIddictApplication();
@ -1284,12 +1352,50 @@ namespace OpenIddict.Server.FunctionalTests
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .ReturnsAsync(application);
mock.Setup(manager => manager.HasPermissionAsync(application, mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
Permissions.GrantTypes.Password, It.IsAny<CancellationToken>())) .ReturnsAsync(ClientTypes.Confidential);
.ReturnsAsync(true);
mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
var client = CreateClient(options =>
{
options.Services.AddSingleton(manager);
});
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientId = "Fabrikam",
ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
GrantType = GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w"
});
// Assert
Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal("The specified client credentials are invalid.", response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted()
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(mock =>
{
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasPermissionAsync(application, mock.Setup(manager => manager.HasPermissionAsync(application,
Permissions.GrantTypes.RefreshToken, It.IsAny<CancellationToken>())) Permissions.Endpoints.Token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false); .ReturnsAsync(false);
}); });
@ -1297,7 +1403,7 @@ namespace OpenIddict.Server.FunctionalTests
{ {
options.Services.AddSingleton(manager); options.Services.AddSingleton(manager);
options.Configure(options => options.IgnoreGrantTypePermissions = false); options.Configure(options => options.IgnoreEndpointPermissions = false);
}); });
// Act // Act
@ -1306,20 +1412,20 @@ namespace OpenIddict.Server.FunctionalTests
ClientId = "Fabrikam", ClientId = "Fabrikam",
GrantType = GrantTypes.Password, GrantType = GrantTypes.Password,
Username = "johndoe", Username = "johndoe",
Password = "A3ddj3w", Password = "A3ddj3w"
Scope = Scopes.OfflineAccess
}); });
// Assert // Assert
Assert.Equal(Errors.InvalidRequest, response.Error); Assert.Equal(Errors.UnauthorizedClient, response.Error);
Assert.Equal("The client application is not allowed to use the 'offline_access' scope.", response.ErrorDescription); Assert.Equal("This client application is not allowed to use the token endpoint.", response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Permissions.GrantTypes.RefreshToken, It.IsAny<CancellationToken>()), Times.Once()); Permissions.Endpoints.Token, It.IsAny<CancellationToken>()), Times.Once());
} }
[Fact] [Fact]
public async Task ValidateTokenRequest_ClientCredentialsRequestFromPublicClientIsRejected() public async Task ValidateTokenRequest_RequestIsRejectedWhenGrantTypePermissionIsNotGranted()
{ {
// Arrange // Arrange
var application = new OpenIddictApplication(); var application = new OpenIddictApplication();
@ -1329,33 +1435,38 @@ namespace OpenIddict.Server.FunctionalTests
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .ReturnsAsync(application);
mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>())) mock.Setup(manager => manager.HasPermissionAsync(application,
.ReturnsAsync(ClientTypes.Public); Permissions.GrantTypes.Password, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
}); });
var client = CreateClient(options => var client = CreateClient(options =>
{ {
options.Services.AddSingleton(manager); options.Services.AddSingleton(manager);
options.Configure(options => options.IgnoreGrantTypePermissions = false);
}); });
// Act // Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{ {
ClientId = "Fabrikam", ClientId = "Fabrikam",
ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", GrantType = GrantTypes.Password,
GrantType = GrantTypes.ClientCredentials Username = "johndoe",
Password = "A3ddj3w"
}); });
// Assert // Assert
Assert.Equal(Errors.UnauthorizedClient, response.Error); Assert.Equal(Errors.UnauthorizedClient, response.Error);
Assert.Equal("The specified 'grant_type' parameter is not valid for this client application.", response.ErrorDescription); Assert.Equal("This client application is not allowed to use the specified grant type.", response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Permissions.GrantTypes.Password, It.IsAny<CancellationToken>()), Times.Once());
} }
[Fact] [Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenScopePermissionIsNotGranted() public async Task ValidateTokenRequest_RequestWithOfflineAccessScopeIsRejectedWhenRefreshTokenPermissionIsNotGranted()
{ {
// Arrange // Arrange
var application = new OpenIddictApplication(); var application = new OpenIddictApplication();
@ -1365,15 +1476,12 @@ namespace OpenIddict.Server.FunctionalTests
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>())) mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application); .ReturnsAsync(application);
mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(ClientTypes.Public);
mock.Setup(manager => manager.HasPermissionAsync(application, mock.Setup(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Scope + Scopes.Profile, It.IsAny<CancellationToken>())) Permissions.GrantTypes.Password, It.IsAny<CancellationToken>()))
.ReturnsAsync(true); .ReturnsAsync(true);
mock.Setup(manager => manager.HasPermissionAsync(application, mock.Setup(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Scope + Scopes.Email, It.IsAny<CancellationToken>())) Permissions.GrantTypes.RefreshToken, It.IsAny<CancellationToken>()))
.ReturnsAsync(false); .ReturnsAsync(false);
}); });
@ -1381,8 +1489,7 @@ namespace OpenIddict.Server.FunctionalTests
{ {
options.Services.AddSingleton(manager); options.Services.AddSingleton(manager);
options.RegisterScopes(Scopes.Email, Scopes.Profile); options.Configure(options => options.IgnoreGrantTypePermissions = false);
options.Configure(options => options.IgnoreScopePermissions = false);
}); });
// Act // Act
@ -1392,25 +1499,19 @@ namespace OpenIddict.Server.FunctionalTests
GrantType = GrantTypes.Password, GrantType = GrantTypes.Password,
Username = "johndoe", Username = "johndoe",
Password = "A3ddj3w", Password = "A3ddj3w",
Scope = "openid offline_access profile email" Scope = Scopes.OfflineAccess
}); });
// Assert // Assert
Assert.Equal(Errors.InvalidRequest, response.Error); Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal("This client application is not allowed to use the specified scope.", response.ErrorDescription); Assert.Equal("The client application is not allowed to use the 'offline_access' scope.", response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Scope + Scopes.OpenId, It.IsAny<CancellationToken>()), Times.Never()); Permissions.GrantTypes.RefreshToken, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Scope + Scopes.OfflineAccess, It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Scope + Scopes.Profile, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Scope + Scopes.Email, It.IsAny<CancellationToken>()), Times.Once());
} }
[Fact] [Fact]
public async Task ValidateTokenRequest_ClientSecretCannotBeUsedByPublicClients() public async Task ValidateTokenRequest_RequestIsRejectedWhenScopePermissionIsNotGranted()
{ {
// Arrange // Arrange
var application = new OpenIddictApplication(); var application = new OpenIddictApplication();
@ -1422,33 +1523,50 @@ namespace OpenIddict.Server.FunctionalTests
mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>())) mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(ClientTypes.Public); .ReturnsAsync(ClientTypes.Public);
mock.Setup(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Scope + Scopes.Profile, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Scope + Scopes.Email, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
}); });
var client = CreateClient(options => var client = CreateClient(options =>
{ {
options.Services.AddSingleton(manager); options.Services.AddSingleton(manager);
options.RegisterScopes(Scopes.Email, Scopes.Profile);
options.Configure(options => options.IgnoreScopePermissions = false);
}); });
// Act // Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{ {
ClientId = "Fabrikam", ClientId = "Fabrikam",
ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw",
GrantType = GrantTypes.Password, GrantType = GrantTypes.Password,
Username = "johndoe", Username = "johndoe",
Password = "A3ddj3w" Password = "A3ddj3w",
Scope = "openid offline_access profile email"
}); });
// Assert // Assert
Assert.Equal(Errors.InvalidRequest, response.Error); Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal("The 'client_secret' parameter is not valid for this client application.", response.ErrorDescription); Assert.Equal("This client application is not allowed to use the specified scope.", response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()), Times.Once()); Permissions.Prefixes.Scope + Scopes.OpenId, It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Scope + Scopes.OfflineAccess, It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Scope + Scopes.Profile, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Scope + Scopes.Email, It.IsAny<CancellationToken>()), Times.Once());
} }
[Fact] [Fact]
public async Task ValidateTokenRequest_ClientSecretIsRequiredForConfidentialClients() public async Task ValidateTokenRequest_RequestIsRejectedWhenCodeVerifierIsMissingWithPkceFeatureEnforced()
{ {
// Arrange // Arrange
var application = new OpenIddictApplication(); var application = new OpenIddictApplication();
@ -1459,7 +1577,11 @@ namespace OpenIddict.Server.FunctionalTests
.ReturnsAsync(application); .ReturnsAsync(application);
mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>())) mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(ClientTypes.Confidential); .ReturnsAsync(ClientTypes.Public);
mock.Setup(manager => manager.HasRequirementAsync(application,
Requirements.Features.ProofKeyForCodeExchange, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}); });
var client = CreateClient(options => var client = CreateClient(options =>
@ -1471,22 +1593,22 @@ namespace OpenIddict.Server.FunctionalTests
var response = await client.PostAsync("/connect/token", new OpenIddictRequest var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{ {
ClientId = "Fabrikam", ClientId = "Fabrikam",
ClientSecret = null, Code = "SplxlOBeZQQYbYS6WxSbIA",
GrantType = GrantTypes.Password, CodeVerifier = null,
Username = "johndoe", GrantType = GrantTypes.AuthorizationCode,
Password = "A3ddj3w" RedirectUri = "http://www.fabrikam.com/path"
}); });
// Assert // Assert
Assert.Equal(Errors.InvalidClient, response.Error); Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal("The 'client_secret' parameter required for this client application is missing.", response.ErrorDescription); Assert.Equal("The mandatory 'code_verifier' parameter is missing.", response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.HasRequirementAsync(application,
Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()), Times.Once()); Requirements.Features.ProofKeyForCodeExchange, It.IsAny<CancellationToken>()), Times.Once());
} }
[Fact] [Fact]
public async Task ValidateTokenRequest_ClientSecretIsRequiredForHybridClients() public async Task ValidateTokenRequest_RequestIsValidatedWhenCodeVerifierIsMissingWithPkceFeatureNotEnforced()
{ {
// Arrange // Arrange
var application = new OpenIddictApplication(); var application = new OpenIddictApplication();
@ -1497,34 +1619,59 @@ namespace OpenIddict.Server.FunctionalTests
.ReturnsAsync(application); .ReturnsAsync(application);
mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>())) mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(ClientTypes.Hybrid); .ReturnsAsync(ClientTypes.Public);
mock.Setup(manager => manager.HasRequirementAsync(application,
Requirements.Features.ProofKeyForCodeExchange, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
}); });
var client = CreateClient(options => var client = CreateClient(options =>
{ {
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token);
Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetPresenters("Fabrikam")
.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56")
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(manager); options.Services.AddSingleton(manager);
options.SetRevocationEndpointUris(Array.Empty<Uri>());
options.DisableTokenStorage();
options.DisableSlidingExpiration();
}); });
// Act // Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{ {
ClientId = "Fabrikam", ClientId = "Fabrikam",
ClientSecret = null, Code = "SplxlOBeZQQYbYS6WxSbIA",
GrantType = GrantTypes.Password, CodeVerifier = null,
Username = "johndoe", GrantType = GrantTypes.AuthorizationCode,
Password = "A3ddj3w" RedirectUri = "http://www.fabrikam.com/path"
}); });
// Assert // Assert
Assert.Equal(Errors.InvalidClient, response.Error); Assert.NotNull(response.AccessToken);
Assert.Equal("The 'client_secret' parameter required for this client application is missing.", response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.HasRequirementAsync(application,
Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()), Times.Once()); Requirements.Features.ProofKeyForCodeExchange, It.IsAny<CancellationToken>()), Times.Once());
} }
[Fact] [Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenClientCredentialsAreInvalid() public async Task ValidateTokenRequest_RequestIsValidatedWhenCodeVerifierIsPresentWithPkceFeatureEnforced()
{ {
// Arrange // Arrange
var application = new OpenIddictApplication(); var application = new OpenIddictApplication();
@ -1535,34 +1682,57 @@ namespace OpenIddict.Server.FunctionalTests
.ReturnsAsync(application); .ReturnsAsync(application);
mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>())) mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.ReturnsAsync(ClientTypes.Confidential); .ReturnsAsync(ClientTypes.Public);
mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>())) mock.Setup(manager => manager.HasRequirementAsync(application,
Requirements.Features.ProofKeyForCodeExchange, It.IsAny<CancellationToken>()))
.ReturnsAsync(false); .ReturnsAsync(false);
}); });
var client = CreateClient(options => var client = CreateClient(options =>
{ {
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token);
Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetPresenters("Fabrikam")
.SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56")
.SetClaim(Claims.Subject, "Bob le Bricoleur")
.SetClaim(Claims.Private.CodeChallenge, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM")
.SetClaim(Claims.Private.CodeChallengeMethod, CodeChallengeMethods.Sha256);
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(manager); options.Services.AddSingleton(manager);
options.SetRevocationEndpointUris(Array.Empty<Uri>());
options.DisableTokenStorage();
options.DisableSlidingExpiration();
}); });
// Act // Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{ {
ClientId = "Fabrikam", ClientId = "Fabrikam",
ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", Code = "SplxlOBeZQQYbYS6WxSbIA",
GrantType = GrantTypes.Password, CodeVerifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
Username = "johndoe", GrantType = GrantTypes.AuthorizationCode,
Password = "A3ddj3w" RedirectUri = "http://www.fabrikam.com/path"
}); });
// Assert // Assert
Assert.Equal(Errors.InvalidClient, response.Error); Assert.NotNull(response.AccessToken);
Assert.Equal("The specified client credentials are invalid.", response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.HasRequirementAsync(application,
Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny<CancellationToken>()), Times.AtLeastOnce()); Requirements.Features.ProofKeyForCodeExchange, It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()), Times.Once());
} }
[Theory] [Theory]

Loading…
Cancel
Save