Add support for proof of key for code exchange
This commit is contained in:
parent
be8232d3f9
commit
6a3fca86b5
|
|
@ -29,5 +29,7 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
public const string GrantedToken = "g_token";
|
public const string GrantedToken = "g_token";
|
||||||
public const string TenantId = "tid";
|
public const string TenantId = "tid";
|
||||||
public const string Resource = "rid";
|
public const string Resource = "rid";
|
||||||
|
public const string CodeChallenge = "c_chall";
|
||||||
|
public const string CodeChallengeMethod = "c_chall_m";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity.Service
|
||||||
|
{
|
||||||
|
public static class ProofOfKeyForCodeExchangeChallengeMethods
|
||||||
|
{
|
||||||
|
public const string Plain = "plain";
|
||||||
|
public const string SHA256 = "S256";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity.Service
|
||||||
|
{
|
||||||
|
public static class ProofOfKeyForCodeExchangeParameterNames
|
||||||
|
{
|
||||||
|
public const string CodeChallengeMethod = "code_challenge_method";
|
||||||
|
public const string CodeChallenge = "code_challenge";
|
||||||
|
public const string CodeVerifier = "code_verifier";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Identity.Service
|
namespace Microsoft.AspNetCore.Identity.Service
|
||||||
{
|
{
|
||||||
|
[DebuggerDisplay("{DebuggerDisplay(),nq}")]
|
||||||
public class AuthorizationRequestError
|
public class AuthorizationRequestError
|
||||||
{
|
{
|
||||||
public AuthorizationRequestError(OpenIdConnectMessage error, string redirectUri, string responseMode)
|
public AuthorizationRequestError(OpenIdConnectMessage error, string redirectUri, string responseMode)
|
||||||
|
|
@ -19,5 +21,8 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
public string RedirectUri { get; set; }
|
public string RedirectUri { get; set; }
|
||||||
|
|
||||||
public string ResponseMode { get; set; }
|
public string ResponseMode { get; set; }
|
||||||
|
|
||||||
|
private string DebuggerDisplay() =>
|
||||||
|
$"{Message.Error} - {Message.ErrorDescription} - {RedirectUri} - {ResponseMode} - {Message.State}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
public string ClientId => GetClaimValue(IdentityServiceClaimTypes.ClientId);
|
public string ClientId => GetClaimValue(IdentityServiceClaimTypes.ClientId);
|
||||||
public string Resource => GetClaimValue(IdentityServiceClaimTypes.Resource);
|
public string Resource => GetClaimValue(IdentityServiceClaimTypes.Resource);
|
||||||
public string RedirectUri => GetClaimValue(IdentityServiceClaimTypes.RedirectUri);
|
public string RedirectUri => GetClaimValue(IdentityServiceClaimTypes.RedirectUri);
|
||||||
|
public string CodeChallenge => GetClaimValue(IdentityServiceClaimTypes.CodeChallenge);
|
||||||
|
public string CodeChallengeMethod => GetClaimValue(IdentityServiceClaimTypes.CodeChallengeMethod);
|
||||||
public IEnumerable<string> Scopes => GetClaimValuesOrEmpty(IdentityServiceClaimTypes.Scope);
|
public IEnumerable<string> Scopes => GetClaimValuesOrEmpty(IdentityServiceClaimTypes.Scope);
|
||||||
public IEnumerable<string> GrantedTokens => GetClaimValuesOrEmpty(IdentityServiceClaimTypes.GrantedToken);
|
public IEnumerable<string> GrantedTokens => GetClaimValuesOrEmpty(IdentityServiceClaimTypes.GrantedToken);
|
||||||
public string Nonce => GetClaimValue(IdentityServiceClaimTypes.Nonce);
|
public string Nonce => GetClaimValue(IdentityServiceClaimTypes.Nonce);
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,43 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (codeChallenge, codeChallengeError) = RequestParametersHelper.ValidateOptionalParameterIsUnique(requestParameters, ProofOfKeyForCodeExchangeParameterNames.CodeChallenge, _errorProvider);
|
||||||
|
if (codeChallengeError != null)
|
||||||
|
{
|
||||||
|
codeChallengeError.State = state;
|
||||||
|
return AuthorizationRequest.Invalid(new AuthorizationRequestError(codeChallengeError, redirectUri, responseMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codeChallenge != null)
|
||||||
|
{
|
||||||
|
// The code challenge needs to be 43 characters long as its the result of Base64URLEncode(SHA256(code_verifier)).
|
||||||
|
// We do this check here because the code challenge might get saved in the serialized authorization code and we
|
||||||
|
// want to prevent it from getting unnecessarily big.
|
||||||
|
if (codeChallenge.Length != 43)
|
||||||
|
{
|
||||||
|
var invalidCodeChallenge = _errorProvider.InvalidCodeChallenge();
|
||||||
|
invalidCodeChallenge.State = state;
|
||||||
|
return AuthorizationRequest.Invalid(new AuthorizationRequestError(
|
||||||
|
invalidCodeChallenge,
|
||||||
|
redirectUri,
|
||||||
|
responseMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
var (codeChallengeMethod, codeChallengeMethodError) = RequestParametersHelper.ValidateParameterIsUnique(requestParameters, ProofOfKeyForCodeExchangeParameterNames.CodeChallengeMethod, _errorProvider);
|
||||||
|
if (codeChallengeMethodError != null)
|
||||||
|
{
|
||||||
|
codeChallengeMethodError.State = state;
|
||||||
|
return AuthorizationRequest.Invalid(new AuthorizationRequestError(codeChallengeMethodError, redirectUri, responseMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!codeChallengeMethod.Equals(ProofOfKeyForCodeExchangeChallengeMethods.SHA256, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var invalidChallengeMethod = _errorProvider.InvalidCodeChallengeMethod(codeChallengeMethod);
|
||||||
|
invalidChallengeMethod.State = state;
|
||||||
|
return AuthorizationRequest.Invalid(new AuthorizationRequestError(invalidChallengeMethod, redirectUri, responseMode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var result = new OpenIdConnectMessage(requestParameters);
|
var result = new OpenIdConnectMessage(requestParameters);
|
||||||
result.RequestType = OpenIdConnectRequestType.Authentication;
|
result.RequestType = OpenIdConnectRequestType.Authentication;
|
||||||
|
|
||||||
|
|
@ -394,5 +431,6 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
|
|
||||||
return (clientId, resolvedUriResult.Uri, null);
|
return (clientId, resolvedUriResult.Uri, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Identity.Service.Claims;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity.Service.Core.Claims
|
||||||
|
{
|
||||||
|
public class ProofOfKeyForCodeExchangeTokenClaimsProvider : ITokenClaimsProvider
|
||||||
|
{
|
||||||
|
public int Order => 100;
|
||||||
|
|
||||||
|
public Task OnGeneratingClaims(TokenGeneratingContext context)
|
||||||
|
{
|
||||||
|
if(context.IsContextForTokenTypes(TokenTypes.AuthorizationCode) &&
|
||||||
|
context.RequestParameters.Parameters.ContainsKey(ProofOfKeyForCodeExchangeParameterNames.CodeChallenge))
|
||||||
|
{
|
||||||
|
context.AddClaimToCurrentToken(
|
||||||
|
IdentityServiceClaimTypes.CodeChallenge,
|
||||||
|
context.RequestParameters.Parameters[ProofOfKeyForCodeExchangeParameterNames.CodeChallenge]);
|
||||||
|
|
||||||
|
context.AddClaimToCurrentToken(
|
||||||
|
IdentityServiceClaimTypes.CodeChallengeMethod,
|
||||||
|
context.RequestParameters.Parameters[ProofOfKeyForCodeExchangeParameterNames.CodeChallengeMethod]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Identity.Service
|
namespace Microsoft.AspNetCore.Identity.Service
|
||||||
|
|
@ -78,6 +79,13 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
$"The token is not yet active or it has expired.");
|
$"The token is not yet active or it has expired.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual OpenIdConnectMessage InvalidCodeVerifier()
|
||||||
|
{
|
||||||
|
return CreateError(
|
||||||
|
IdentityServiceErrorCodes.InvalidRequest,
|
||||||
|
$"The code_verifier is missing or invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
public virtual OpenIdConnectMessage MultipleResourcesNotSupported(string resourceName, string name)
|
public virtual OpenIdConnectMessage MultipleResourcesNotSupported(string resourceName, string name)
|
||||||
{
|
{
|
||||||
return CreateError(
|
return CreateError(
|
||||||
|
|
@ -143,6 +151,20 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
$"The prompt value 'none' can't be used in conjunction with other prompt values '{promptValue}'");
|
$"The prompt value 'none' can't be used in conjunction with other prompt values '{promptValue}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual OpenIdConnectMessage InvalidCodeChallengeMethod(string challengeMethod)
|
||||||
|
{
|
||||||
|
return CreateError(
|
||||||
|
IdentityServiceErrorCodes.InvalidRequest,
|
||||||
|
$"The code challenge method '{challengeMethod ?? "plain"}' is invalid. Only S256 is supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual OpenIdConnectMessage InvalidCodeChallenge()
|
||||||
|
{
|
||||||
|
return CreateError(
|
||||||
|
IdentityServiceErrorCodes.InvalidRequest,
|
||||||
|
$"The provided code challenge must be 43 characters long.");
|
||||||
|
}
|
||||||
|
|
||||||
private OpenIdConnectMessage CreateError(string code, string description, string uri = null) =>
|
private OpenIdConnectMessage CreateError(string code, string description, string uri = null) =>
|
||||||
new OpenIdConnectMessage
|
new OpenIdConnectMessage
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,17 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Identity.Service
|
namespace Microsoft.AspNetCore.Identity.Service
|
||||||
{
|
{
|
||||||
public class TokenRequestFactory : ITokenRequestFactory
|
public class TokenRequestFactory : ITokenRequestFactory
|
||||||
{
|
{
|
||||||
|
private static bool[] ValidCodeVerifierCharacters = CreateCodeVerifierValidCharacters();
|
||||||
|
|
||||||
private readonly IClientIdValidator _clientIdValidator;
|
private readonly IClientIdValidator _clientIdValidator;
|
||||||
private readonly ITokenManager _tokenManager;
|
private readonly ITokenManager _tokenManager;
|
||||||
private readonly IRedirectUriResolver _redirectUriValidator;
|
private readonly IRedirectUriResolver _redirectUriValidator;
|
||||||
|
|
@ -185,16 +189,18 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
string clientId,
|
string clientId,
|
||||||
AuthorizationGrant consentGrant)
|
AuthorizationGrant consentGrant)
|
||||||
{
|
{
|
||||||
|
if (!(consentGrant.Token is AuthorizationCode code))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Granted token must be an authorization code.");
|
||||||
|
}
|
||||||
|
|
||||||
var (redirectUri, redirectUriError) = RequestParametersHelper.ValidateOptionalParameterIsUnique(requestParameters, OpenIdConnectParameterNames.RedirectUri, _errorProvider);
|
var (redirectUri, redirectUriError) = RequestParametersHelper.ValidateOptionalParameterIsUnique(requestParameters, OpenIdConnectParameterNames.RedirectUri, _errorProvider);
|
||||||
if (redirectUriError != null)
|
if (redirectUriError != null)
|
||||||
{
|
{
|
||||||
return redirectUriError;
|
return redirectUriError;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenRedirectUri = consentGrant
|
var tokenRedirectUri = code.RedirectUri;
|
||||||
.Token.SingleOrDefault(c =>
|
|
||||||
string.Equals(c.Type, IdentityServiceClaimTypes.RedirectUri, StringComparison.Ordinal))?.Value;
|
|
||||||
|
|
||||||
if (redirectUri == null && tokenRedirectUri != null)
|
if (redirectUri == null && tokenRedirectUri != null)
|
||||||
{
|
{
|
||||||
return _errorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.RedirectUri);
|
return _errorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.RedirectUri);
|
||||||
|
|
@ -211,8 +217,48 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
return _errorProvider.InvalidRedirectUri(redirectUri);
|
return _errorProvider.InvalidRedirectUri(redirectUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (code.CodeChallenge != null)
|
||||||
|
{
|
||||||
|
if (!ProofOfKeyForCodeExchangeChallengeMethods.SHA256.Equals(code.CodeChallengeMethod, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Unsupported code challenge method.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var (verifier, verifierError) = RequestParametersHelper.ValidateParameterIsUnique(requestParameters, ProofOfKeyForCodeExchangeParameterNames.CodeVerifier, _errorProvider);
|
||||||
|
if (verifierError != null)
|
||||||
|
{
|
||||||
|
return verifierError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// code-verifier = [a-zA-Z0-9\-._~]{43,128}
|
||||||
|
if (verifier.Length < 43 || verifier.Length > 128)
|
||||||
|
{
|
||||||
|
return _errorProvider.InvalidCodeVerifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < verifier.Length; i++)
|
||||||
|
{
|
||||||
|
if (verifier[i] > 127 || !ValidCodeVerifierCharacters[verifier[i]])
|
||||||
|
{
|
||||||
|
return _errorProvider.InvalidCodeVerifier();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(code.CodeChallenge, GetComputedChallenge(verifier), StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return _errorProvider.InvalidCodeVerifier();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
private string GetComputedChallenge(string verifier)
|
||||||
|
{
|
||||||
|
using (var hash = CryptographyHelpers.CreateSHA256())
|
||||||
|
{
|
||||||
|
return Base64UrlEncoder.Encode(hash.ComputeHash(Encoding.ASCII.GetBytes(verifier)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string GetGrantTypeParameter(IDictionary<string, string[]> parameters, string grantType)
|
private string GetGrantTypeParameter(IDictionary<string, string[]> parameters, string grantType)
|
||||||
{
|
{
|
||||||
|
|
@ -226,5 +272,32 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~-._"
|
||||||
|
private static bool[] CreateCodeVerifierValidCharacters()
|
||||||
|
{
|
||||||
|
var result = new bool[128];
|
||||||
|
for (var i = 0x41; i <= 0x5A; i++)
|
||||||
|
{
|
||||||
|
result[i] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0x61; i <= 0x7A; i++)
|
||||||
|
{
|
||||||
|
result[i] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0x30; i <= 0x39; i++)
|
||||||
|
{
|
||||||
|
result[i] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
result['-'] = true;
|
||||||
|
result['.'] = true;
|
||||||
|
result['_'] = true;
|
||||||
|
result['~'] = true;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Identity.Service;
|
||||||
using Microsoft.AspNetCore.Identity.Service.Claims;
|
using Microsoft.AspNetCore.Identity.Service.Claims;
|
||||||
using Microsoft.AspNetCore.Identity.Service.Configuration;
|
using Microsoft.AspNetCore.Identity.Service.Configuration;
|
||||||
using Microsoft.AspNetCore.Identity.Service.Core;
|
using Microsoft.AspNetCore.Identity.Service.Core;
|
||||||
|
using Microsoft.AspNetCore.Identity.Service.Core.Claims;
|
||||||
using Microsoft.AspNetCore.Identity.Service.Metadata;
|
using Microsoft.AspNetCore.Identity.Service.Metadata;
|
||||||
using Microsoft.AspNetCore.Identity.Service.Serialization;
|
using Microsoft.AspNetCore.Identity.Service.Serialization;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
@ -82,6 +83,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.AddSingleton<ITokenClaimsProvider, ScopesTokenClaimsProvider>();
|
services.AddSingleton<ITokenClaimsProvider, ScopesTokenClaimsProvider>();
|
||||||
services.AddSingleton<ITokenClaimsProvider, TimestampsTokenClaimsProvider>();
|
services.AddSingleton<ITokenClaimsProvider, TimestampsTokenClaimsProvider>();
|
||||||
services.AddSingleton<ITokenClaimsProvider, TokenHashTokenClaimsProvider>();
|
services.AddSingleton<ITokenClaimsProvider, TokenHashTokenClaimsProvider>();
|
||||||
|
services.AddSingleton<ITokenClaimsProvider, ProofOfKeyForCodeExchangeTokenClaimsProvider>();
|
||||||
services.AddSingleton<ProtocolErrorProvider>();
|
services.AddSingleton<ProtocolErrorProvider>();
|
||||||
services.AddSingleton<ISigningCredentialsSource, DeveloperCertificateSigningCredentialsSource>();
|
services.AddSingleton<ISigningCredentialsSource, DeveloperCertificateSigningCredentialsSource>();
|
||||||
services.AddSingleton<DeveloperCertificateSigningCredentialsSource>();
|
services.AddSingleton<DeveloperCertificateSigningCredentialsSource>();
|
||||||
|
|
|
||||||
|
|
@ -718,6 +718,165 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
Assert.True(result.IsValid);
|
Assert.True(result.IsValid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FailsToCreateAuthorizationRequest_CodeChallenge_HasMultipleValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var parameters = new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[OpenIdConnectParameterNames.ClientId] = new[] { "a" },
|
||||||
|
[OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" },
|
||||||
|
[OpenIdConnectParameterNames.ResponseType] = new[] { "code" },
|
||||||
|
[OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" },
|
||||||
|
[OpenIdConnectParameterNames.Nonce] = new[] { "asdf" },
|
||||||
|
[OpenIdConnectParameterNames.Scope] = new[] { " openid profile " },
|
||||||
|
[OpenIdConnectParameterNames.State] = new[] { "state" },
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeChallenge] = new[] { "challenge1", "challenge2" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.TooManyParameters(ProofOfKeyForCodeExchangeParameterNames.CodeChallenge), null, null);
|
||||||
|
expectedError.Message.State = "state";
|
||||||
|
|
||||||
|
var factory = CreateAuthorizationRequestFactory();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await factory.CreateAuthorizationRequestAsync(parameters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.IsValid);
|
||||||
|
Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance);
|
||||||
|
Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri);
|
||||||
|
Assert.Equal(OpenIdConnectResponseMode.FormPost, result.Error.ResponseMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("tooshort")]
|
||||||
|
[InlineData("toolong_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")]
|
||||||
|
public async Task FailsToCreateAuthorizationRequest_CodeChallenge_DoesNotHave43Characters(string challenge)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var parameters = new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[OpenIdConnectParameterNames.ClientId] = new[] { "a" },
|
||||||
|
[OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" },
|
||||||
|
[OpenIdConnectParameterNames.ResponseType] = new[] { "code" },
|
||||||
|
[OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" },
|
||||||
|
[OpenIdConnectParameterNames.Nonce] = new[] { "asdf" },
|
||||||
|
[OpenIdConnectParameterNames.Scope] = new[] { " openid profile " },
|
||||||
|
[OpenIdConnectParameterNames.State] = new[] { "state" },
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeChallenge] = new[] { challenge }
|
||||||
|
};
|
||||||
|
|
||||||
|
var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.InvalidCodeChallenge(), "http://www.example.com/callback", "form_post");
|
||||||
|
expectedError.Message.State = "state";
|
||||||
|
|
||||||
|
var factory = CreateAuthorizationRequestFactory();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await factory.CreateAuthorizationRequestAsync(parameters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.IsValid);
|
||||||
|
Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance);
|
||||||
|
Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri);
|
||||||
|
Assert.Equal(OpenIdConnectResponseMode.FormPost, result.Error.ResponseMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FailsToCreateAuthorizationRequest_CodeChallengeMethod_IsMissing()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var parameters = new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[OpenIdConnectParameterNames.ClientId] = new[] { "a" },
|
||||||
|
[OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" },
|
||||||
|
[OpenIdConnectParameterNames.ResponseType] = new[] { "code" },
|
||||||
|
[OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" },
|
||||||
|
[OpenIdConnectParameterNames.Nonce] = new[] { "asdf" },
|
||||||
|
[OpenIdConnectParameterNames.Scope] = new[] { " openid profile " },
|
||||||
|
[OpenIdConnectParameterNames.State] = new[] { "state" },
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeChallenge] = new[] { "0123456789012345678901234567890123456789012" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.MissingRequiredParameter(ProofOfKeyForCodeExchangeParameterNames.CodeChallengeMethod), "http://www.example.com/callback", "form_post");
|
||||||
|
expectedError.Message.State = "state";
|
||||||
|
|
||||||
|
var factory = CreateAuthorizationRequestFactory();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await factory.CreateAuthorizationRequestAsync(parameters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.IsValid);
|
||||||
|
Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance);
|
||||||
|
Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri);
|
||||||
|
Assert.Equal(OpenIdConnectResponseMode.FormPost, result.Error.ResponseMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FailsToCreateAuthorizationRequest_CodeChallengeMethod_HasMultipleValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var parameters = new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[OpenIdConnectParameterNames.ClientId] = new[] { "a" },
|
||||||
|
[OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" },
|
||||||
|
[OpenIdConnectParameterNames.ResponseType] = new[] { "code" },
|
||||||
|
[OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" },
|
||||||
|
[OpenIdConnectParameterNames.Nonce] = new[] { "asdf" },
|
||||||
|
[OpenIdConnectParameterNames.Scope] = new[] { " openid profile " },
|
||||||
|
[OpenIdConnectParameterNames.State] = new[] { "state" },
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeChallenge] = new[] { "0123456789012345678901234567890123456789012" },
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeChallengeMethod] = new[] { "S256", "plain" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.TooManyParameters(ProofOfKeyForCodeExchangeParameterNames.CodeChallengeMethod), "http://www.example.com/callback", "form_post");
|
||||||
|
expectedError.Message.State = "state";
|
||||||
|
|
||||||
|
var factory = CreateAuthorizationRequestFactory();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await factory.CreateAuthorizationRequestAsync(parameters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.IsValid);
|
||||||
|
Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance);
|
||||||
|
Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri);
|
||||||
|
Assert.Equal(OpenIdConnectResponseMode.FormPost, result.Error.ResponseMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FailsToCreateAuthorizationRequest_CodeChallengeMethod_IsNotSHA256()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var parameters = new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[OpenIdConnectParameterNames.ClientId] = new[] { "a" },
|
||||||
|
[OpenIdConnectParameterNames.RedirectUri] = new[] { "http://www.example.com/callback" },
|
||||||
|
[OpenIdConnectParameterNames.ResponseType] = new[] { "code" },
|
||||||
|
[OpenIdConnectParameterNames.ResponseMode] = new[] { "form_post" },
|
||||||
|
[OpenIdConnectParameterNames.Nonce] = new[] { "asdf" },
|
||||||
|
[OpenIdConnectParameterNames.Scope] = new[] { " openid profile " },
|
||||||
|
[OpenIdConnectParameterNames.State] = new[] { "state" },
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeChallenge] = new[] { "0123456789012345678901234567890123456789012" },
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeChallengeMethod] = new[] { "plain" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var expectedError = new AuthorizationRequestError(ProtocolErrorProvider.InvalidCodeChallengeMethod("plain"), null, null);
|
||||||
|
expectedError.Message.State = "state";
|
||||||
|
|
||||||
|
var factory = CreateAuthorizationRequestFactory();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await factory.CreateAuthorizationRequestAsync(parameters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.IsValid);
|
||||||
|
Assert.Equal(expectedError, result.Error, IdentityServiceErrorComparer.Instance);
|
||||||
|
Assert.Equal("http://www.example.com/callback", result.Error.RedirectUri);
|
||||||
|
Assert.Equal(OpenIdConnectResponseMode.FormPost, result.Error.ResponseMode);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CreatesAnAuthorizationRequest_IfAllParameters_AreCorrect()
|
public async Task CreatesAnAuthorizationRequest_IfAllParameters_AreCorrect()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity.Service.Core.Claims
|
||||||
|
{
|
||||||
|
public class ProofOfKeyForCodeExchangeTokenClaimsProviderTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task OnGeneratingClaims_AddsCodeChallengeAndChallengeMethod_ToTheAuthorizationCode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = new TokenGeneratingContext(
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
new OpenIdConnectMessage(new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeChallenge] = new[] { "challenge" },
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeChallengeMethod] = new[] { "S256" },
|
||||||
|
}),
|
||||||
|
new RequestGrants());
|
||||||
|
|
||||||
|
context.InitializeForToken(TokenTypes.AuthorizationCode);
|
||||||
|
|
||||||
|
var provider = new ProofOfKeyForCodeExchangeTokenClaimsProvider();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await provider.OnGeneratingClaims(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains(context.CurrentClaims, c => c.Type == IdentityServiceClaimTypes.CodeChallenge && c.Value == "challenge");
|
||||||
|
Assert.Contains(context.CurrentClaims, c => c.Type == IdentityServiceClaimTypes.CodeChallengeMethod && c.Value == "S256");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(TokenTypes.AccessToken)]
|
||||||
|
[InlineData(TokenTypes.IdToken)]
|
||||||
|
[InlineData(TokenTypes.RefreshToken)]
|
||||||
|
public async Task OnGeneratingClaims_DoesNothing_ForOtherTokenTypes(string tokenType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = new TokenGeneratingContext(
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
new OpenIdConnectMessage(new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeChallenge] = new[] { "challenge" },
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeChallengeMethod] = new[] { "S256" },
|
||||||
|
}),
|
||||||
|
new RequestGrants());
|
||||||
|
|
||||||
|
context.InitializeForToken(tokenType);
|
||||||
|
|
||||||
|
var provider = new ProofOfKeyForCodeExchangeTokenClaimsProvider();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await provider.OnGeneratingClaims(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(context.CurrentClaims);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task OnGeneratingClaims_DoesNothing_IfChallengeNotPresent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = new TokenGeneratingContext(
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
new OpenIdConnectMessage(new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeChallengeMethod] = new[] { "S256" },
|
||||||
|
}),
|
||||||
|
new RequestGrants());
|
||||||
|
|
||||||
|
context.InitializeForToken(TokenTypes.AuthorizationCode);
|
||||||
|
|
||||||
|
var provider = new ProofOfKeyForCodeExchangeTokenClaimsProvider();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await provider.OnGeneratingClaims(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(context.CurrentClaims);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -144,7 +144,7 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
Mock.Of<IClientIdValidator>(),
|
Mock.Of<IClientIdValidator>(),
|
||||||
Mock.Of<IRedirectUriResolver>(), Mock.Of<IScopeResolver>(),
|
Mock.Of<IRedirectUriResolver>(), Mock.Of<IScopeResolver>(),
|
||||||
Enumerable.Empty<ITokenRequestValidator>(),
|
Enumerable.Empty<ITokenRequestValidator>(),
|
||||||
GetTestTokenManager(GetValidToken()),
|
GetTestTokenManager(GetValidAuthorizationCode()),
|
||||||
new TimeStampManager(), new ProtocolErrorProvider());
|
new TimeStampManager(), new ProtocolErrorProvider());
|
||||||
|
|
||||||
var expectedError = ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.ClientId);
|
var expectedError = ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.ClientId);
|
||||||
|
|
@ -173,7 +173,7 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true),
|
GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true),
|
||||||
Mock.Of<IRedirectUriResolver>(), Mock.Of<IScopeResolver>(),
|
Mock.Of<IRedirectUriResolver>(), Mock.Of<IScopeResolver>(),
|
||||||
Enumerable.Empty<ITokenRequestValidator>(),
|
Enumerable.Empty<ITokenRequestValidator>(),
|
||||||
GetTestTokenManager(GetValidToken()),
|
GetTestTokenManager(GetValidAuthorizationCode()),
|
||||||
new TimeStampManager(), new ProtocolErrorProvider());
|
new TimeStampManager(), new ProtocolErrorProvider());
|
||||||
|
|
||||||
var expectedError = ProtocolErrorProvider.InvalidGrant();
|
var expectedError = ProtocolErrorProvider.InvalidGrant();
|
||||||
|
|
@ -200,7 +200,7 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
GetClientIdValidator(isClientIdValid: false),
|
GetClientIdValidator(isClientIdValid: false),
|
||||||
Mock.Of<IRedirectUriResolver>(), Mock.Of<IScopeResolver>(),
|
Mock.Of<IRedirectUriResolver>(), Mock.Of<IScopeResolver>(),
|
||||||
Enumerable.Empty<ITokenRequestValidator>(),
|
Enumerable.Empty<ITokenRequestValidator>(),
|
||||||
GetTestTokenManager(GetValidToken()),
|
GetTestTokenManager(GetValidAuthorizationCode()),
|
||||||
new TimeStampManager(), new ProtocolErrorProvider());
|
new TimeStampManager(), new ProtocolErrorProvider());
|
||||||
|
|
||||||
var expectedError = ProtocolErrorProvider.InvalidClientId("clientId");
|
var expectedError = ProtocolErrorProvider.InvalidClientId("clientId");
|
||||||
|
|
@ -227,7 +227,7 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: false),
|
GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: false),
|
||||||
Mock.Of<IRedirectUriResolver>(), Mock.Of<IScopeResolver>(),
|
Mock.Of<IRedirectUriResolver>(), Mock.Of<IScopeResolver>(),
|
||||||
Enumerable.Empty<ITokenRequestValidator>(),
|
Enumerable.Empty<ITokenRequestValidator>(),
|
||||||
GetTestTokenManager(GetValidToken()),
|
GetTestTokenManager(GetValidAuthorizationCode()),
|
||||||
new TimeStampManager(), new ProtocolErrorProvider());
|
new TimeStampManager(), new ProtocolErrorProvider());
|
||||||
|
|
||||||
var expectedError = ProtocolErrorProvider.InvalidClientCredentials();
|
var expectedError = ProtocolErrorProvider.InvalidClientCredentials();
|
||||||
|
|
@ -258,7 +258,7 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
Mock.Of<IRedirectUriResolver>(),
|
Mock.Of<IRedirectUriResolver>(),
|
||||||
Mock.Of<IScopeResolver>(),
|
Mock.Of<IScopeResolver>(),
|
||||||
Enumerable.Empty<ITokenRequestValidator>(),
|
Enumerable.Empty<ITokenRequestValidator>(),
|
||||||
GetTestTokenManager(GetValidToken()),
|
GetTestTokenManager(GetValidAuthorizationCode()),
|
||||||
new TimeStampManager(), new ProtocolErrorProvider());
|
new TimeStampManager(), new ProtocolErrorProvider());
|
||||||
|
|
||||||
var expectedError = ProtocolErrorProvider.TooManyParameters(OpenIdConnectParameterNames.Scope);
|
var expectedError = ProtocolErrorProvider.TooManyParameters(OpenIdConnectParameterNames.Scope);
|
||||||
|
|
@ -289,7 +289,7 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
Mock.Of<IRedirectUriResolver>(),
|
Mock.Of<IRedirectUriResolver>(),
|
||||||
GetScopeResolver(hasInvalidScopes: true),
|
GetScopeResolver(hasInvalidScopes: true),
|
||||||
Enumerable.Empty<ITokenRequestValidator>(),
|
Enumerable.Empty<ITokenRequestValidator>(),
|
||||||
GetTestTokenManager(GetValidToken(),null,null,Enumerable.Empty<string>(), new[] { "openid" }),
|
GetTestTokenManager(GetValidAuthorizationCode(), null, null, Enumerable.Empty<string>(), new[] { "openid" }),
|
||||||
new TimeStampManager(), new ProtocolErrorProvider());
|
new TimeStampManager(), new ProtocolErrorProvider());
|
||||||
|
|
||||||
var expectedError = ProtocolErrorProvider.InvalidScope("invalid");
|
var expectedError = ProtocolErrorProvider.InvalidScope("invalid");
|
||||||
|
|
@ -320,7 +320,7 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
Mock.Of<IRedirectUriResolver>(),
|
Mock.Of<IRedirectUriResolver>(),
|
||||||
GetScopeResolver(hasInvalidScopes: false),
|
GetScopeResolver(hasInvalidScopes: false),
|
||||||
Enumerable.Empty<ITokenRequestValidator>(),
|
Enumerable.Empty<ITokenRequestValidator>(),
|
||||||
GetTestTokenManager(GetValidToken(), null, null, Enumerable.Empty<string>(), new[] { "openid" }),
|
GetTestTokenManager(GetValidAuthorizationCode(), null, null, Enumerable.Empty<string>(), new[] { "openid" }),
|
||||||
new TimeStampManager(), new ProtocolErrorProvider());
|
new TimeStampManager(), new ProtocolErrorProvider());
|
||||||
|
|
||||||
var expectedError = ProtocolErrorProvider.UnauthorizedScope();
|
var expectedError = ProtocolErrorProvider.UnauthorizedScope();
|
||||||
|
|
@ -350,7 +350,7 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
GetRedirectUriValidator(isRedirectUriValid: false),
|
GetRedirectUriValidator(isRedirectUriValid: false),
|
||||||
Mock.Of<IScopeResolver>(),
|
Mock.Of<IScopeResolver>(),
|
||||||
Enumerable.Empty<ITokenRequestValidator>(),
|
Enumerable.Empty<ITokenRequestValidator>(),
|
||||||
GetTestTokenManager(GetValidToken()),
|
GetTestTokenManager(GetValidAuthorizationCode()),
|
||||||
new TimeStampManager(), new ProtocolErrorProvider());
|
new TimeStampManager(), new ProtocolErrorProvider());
|
||||||
|
|
||||||
var expectedError = ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.RedirectUri);
|
var expectedError = ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.RedirectUri);
|
||||||
|
|
@ -364,6 +364,214 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance);
|
Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateTokenRequestAsyncFails_IfCodeVerifierIsMissing()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var requestParameters = new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[OpenIdConnectParameterNames.GrantType] = new[] { "authorization_code" },
|
||||||
|
[OpenIdConnectParameterNames.Code] = new[] { "valid" },
|
||||||
|
[OpenIdConnectParameterNames.ClientId] = new[] { "clientId" },
|
||||||
|
[OpenIdConnectParameterNames.RedirectUri] = new[] { "https://www.example.com" },
|
||||||
|
};
|
||||||
|
|
||||||
|
var tokenRequestFactory = new TokenRequestFactory(
|
||||||
|
GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true),
|
||||||
|
GetRedirectUriValidator(isRedirectUriValid: true),
|
||||||
|
Mock.Of<IScopeResolver>(),
|
||||||
|
Enumerable.Empty<ITokenRequestValidator>(),
|
||||||
|
GetTestTokenManager(GetValidAuthorizationCode(new[] {
|
||||||
|
new Claim(IdentityServiceClaimTypes.CodeChallenge,"challenge"),
|
||||||
|
new Claim(IdentityServiceClaimTypes.CodeChallengeMethod, ProofOfKeyForCodeExchangeChallengeMethods.SHA256),
|
||||||
|
})),
|
||||||
|
new TimeStampManager(), new ProtocolErrorProvider());
|
||||||
|
|
||||||
|
var expectedError = ProtocolErrorProvider.MissingRequiredParameter(ProofOfKeyForCodeExchangeParameterNames.CodeVerifier);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(tokenRequest);
|
||||||
|
Assert.False(tokenRequest.IsValid);
|
||||||
|
Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateTokenRequestAsyncFails_IfCodeVerifier_HasMultipleValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var requestParameters = new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[OpenIdConnectParameterNames.GrantType] = new[] { "authorization_code" },
|
||||||
|
[OpenIdConnectParameterNames.Code] = new[] { "valid" },
|
||||||
|
[OpenIdConnectParameterNames.ClientId] = new[] { "clientId" },
|
||||||
|
[OpenIdConnectParameterNames.RedirectUri] = new[] { "https://www.example.com" },
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeVerifier] = new[] { "value1", "value2" },
|
||||||
|
};
|
||||||
|
|
||||||
|
var tokenRequestFactory = new TokenRequestFactory(
|
||||||
|
GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true),
|
||||||
|
GetRedirectUriValidator(isRedirectUriValid: true),
|
||||||
|
Mock.Of<IScopeResolver>(),
|
||||||
|
Enumerable.Empty<ITokenRequestValidator>(),
|
||||||
|
GetTestTokenManager(GetValidAuthorizationCode(new[] {
|
||||||
|
new Claim(IdentityServiceClaimTypes.CodeChallenge,"challenge"),
|
||||||
|
new Claim(IdentityServiceClaimTypes.CodeChallengeMethod, ProofOfKeyForCodeExchangeChallengeMethods.SHA256),
|
||||||
|
})),
|
||||||
|
new TimeStampManager(), new ProtocolErrorProvider());
|
||||||
|
|
||||||
|
var expectedError = ProtocolErrorProvider.TooManyParameters(ProofOfKeyForCodeExchangeParameterNames.CodeVerifier);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(tokenRequest);
|
||||||
|
Assert.False(tokenRequest.IsValid);
|
||||||
|
Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateTokenRequestAsyncFails_IfCodeVerifierIsInvalid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var requestParameters = new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[OpenIdConnectParameterNames.GrantType] = new[] { "authorization_code" },
|
||||||
|
[OpenIdConnectParameterNames.Code] = new[] { "valid" },
|
||||||
|
[OpenIdConnectParameterNames.ClientId] = new[] { "clientId" },
|
||||||
|
[OpenIdConnectParameterNames.RedirectUri] = new[] { "https://www.example.com" },
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeVerifier] = new[] { "@" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var tokenRequestFactory = new TokenRequestFactory(
|
||||||
|
GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true),
|
||||||
|
GetRedirectUriValidator(isRedirectUriValid: true),
|
||||||
|
Mock.Of<IScopeResolver>(),
|
||||||
|
Enumerable.Empty<ITokenRequestValidator>(),
|
||||||
|
GetTestTokenManager(GetValidAuthorizationCode(new[] {
|
||||||
|
new Claim(IdentityServiceClaimTypes.CodeChallenge,"challenge"),
|
||||||
|
new Claim(IdentityServiceClaimTypes.CodeChallengeMethod, ProofOfKeyForCodeExchangeChallengeMethods.SHA256),
|
||||||
|
})),
|
||||||
|
new TimeStampManager(), new ProtocolErrorProvider());
|
||||||
|
|
||||||
|
var expectedError = ProtocolErrorProvider.InvalidCodeVerifier();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(tokenRequest);
|
||||||
|
Assert.False(tokenRequest.IsValid);
|
||||||
|
Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("tooShort")]
|
||||||
|
[InlineData("tooLooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong")]
|
||||||
|
public async Task CreateTokenRequestAsyncFails_IfTooShortOrTooLong(string verifier)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var requestParameters = new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[OpenIdConnectParameterNames.GrantType] = new[] { "authorization_code" },
|
||||||
|
[OpenIdConnectParameterNames.Code] = new[] { "valid" },
|
||||||
|
[OpenIdConnectParameterNames.ClientId] = new[] { "clientId" },
|
||||||
|
[OpenIdConnectParameterNames.RedirectUri] = new[] { "https://www.example.com" },
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeVerifier] = new[] { verifier }
|
||||||
|
};
|
||||||
|
|
||||||
|
var tokenRequestFactory = new TokenRequestFactory(
|
||||||
|
GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true),
|
||||||
|
GetRedirectUriValidator(isRedirectUriValid: true),
|
||||||
|
Mock.Of<IScopeResolver>(),
|
||||||
|
Enumerable.Empty<ITokenRequestValidator>(),
|
||||||
|
GetTestTokenManager(GetValidAuthorizationCode(new[] {
|
||||||
|
new Claim(IdentityServiceClaimTypes.CodeChallenge,"challenge"),
|
||||||
|
new Claim(IdentityServiceClaimTypes.CodeChallengeMethod, ProofOfKeyForCodeExchangeChallengeMethods.SHA256),
|
||||||
|
})),
|
||||||
|
new TimeStampManager(), new ProtocolErrorProvider());
|
||||||
|
|
||||||
|
var expectedError = ProtocolErrorProvider.InvalidCodeVerifier();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(tokenRequest);
|
||||||
|
Assert.False(tokenRequest.IsValid);
|
||||||
|
Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateTokenRequestAsyncFails_IfCodeVerifierDoesNotMatchChallenge()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var requestParameters = new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[OpenIdConnectParameterNames.GrantType] = new[] { "authorization_code" },
|
||||||
|
[OpenIdConnectParameterNames.Code] = new[] { "valid" },
|
||||||
|
[OpenIdConnectParameterNames.ClientId] = new[] { "clientId" },
|
||||||
|
[OpenIdConnectParameterNames.RedirectUri] = new[] { "https://www.example.com" },
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeVerifier] = new[] { "0123456789012345678901234567890123456789012" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var tokenRequestFactory = new TokenRequestFactory(
|
||||||
|
GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true),
|
||||||
|
GetRedirectUriValidator(isRedirectUriValid: true),
|
||||||
|
Mock.Of<IScopeResolver>(),
|
||||||
|
Enumerable.Empty<ITokenRequestValidator>(),
|
||||||
|
GetTestTokenManager(GetValidAuthorizationCode(new[] {
|
||||||
|
new Claim(IdentityServiceClaimTypes.CodeChallenge,"challenge"),
|
||||||
|
new Claim(IdentityServiceClaimTypes.CodeChallengeMethod, ProofOfKeyForCodeExchangeChallengeMethods.SHA256),
|
||||||
|
})),
|
||||||
|
new TimeStampManager(), new ProtocolErrorProvider());
|
||||||
|
|
||||||
|
var expectedError = ProtocolErrorProvider.InvalidCodeVerifier();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(tokenRequest);
|
||||||
|
Assert.False(tokenRequest.IsValid);
|
||||||
|
Assert.Equal(expectedError, tokenRequest.Error, IdentityServiceErrorComparer.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateTokenRequestSucceeds_IfCodeVerifier_MatchesChallenge()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var requestParameters = new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
[OpenIdConnectParameterNames.GrantType] = new[] { "authorization_code" },
|
||||||
|
[OpenIdConnectParameterNames.Code] = new[] { "valid" },
|
||||||
|
[OpenIdConnectParameterNames.ClientId] = new[] { "clientId" },
|
||||||
|
[OpenIdConnectParameterNames.RedirectUri] = new[] { "https://www.example.com" },
|
||||||
|
[ProofOfKeyForCodeExchangeParameterNames.CodeVerifier] = new[] { "0123456789012345678901234567890123456789012" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var tokenRequestFactory = new TokenRequestFactory(
|
||||||
|
GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true),
|
||||||
|
GetRedirectUriValidator(isRedirectUriValid: true),
|
||||||
|
Mock.Of<IScopeResolver>(),
|
||||||
|
Enumerable.Empty<ITokenRequestValidator>(),
|
||||||
|
GetTestTokenManager(GetValidAuthorizationCode(new[] {
|
||||||
|
new Claim(IdentityServiceClaimTypes.CodeChallenge,"_RpfHqw8pAZIomzVUE7sjRmHSM543WVdC4o-Kc4_3C0"),
|
||||||
|
new Claim(IdentityServiceClaimTypes.CodeChallengeMethod, ProofOfKeyForCodeExchangeChallengeMethods.SHA256),
|
||||||
|
})),
|
||||||
|
new TimeStampManager(), new ProtocolErrorProvider());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tokenRequest = await tokenRequestFactory.CreateTokenRequestAsync(requestParameters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(tokenRequest);
|
||||||
|
Assert.True(tokenRequest.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
private IRedirectUriResolver GetRedirectUriValidator(bool isRedirectUriValid)
|
private IRedirectUriResolver GetRedirectUriValidator(bool isRedirectUriValid)
|
||||||
{
|
{
|
||||||
var mock = new Mock<IRedirectUriResolver>();
|
var mock = new Mock<IRedirectUriResolver>();
|
||||||
|
|
@ -390,7 +598,7 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true),
|
GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true),
|
||||||
Mock.Of<IRedirectUriResolver>(), Mock.Of<IScopeResolver>(),
|
Mock.Of<IRedirectUriResolver>(), Mock.Of<IScopeResolver>(),
|
||||||
Enumerable.Empty<ITokenRequestValidator>(),
|
Enumerable.Empty<ITokenRequestValidator>(),
|
||||||
GetTestTokenManager(GetValidToken()),
|
GetTestTokenManager(GetValidAuthorizationCode()),
|
||||||
new TimeStampManager(), new ProtocolErrorProvider());
|
new TimeStampManager(), new ProtocolErrorProvider());
|
||||||
|
|
||||||
var expectedError = ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.RedirectUri);
|
var expectedError = ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.RedirectUri);
|
||||||
|
|
@ -419,21 +627,25 @@ namespace Microsoft.AspNetCore.Identity.Service
|
||||||
return clientIdValidator.Object;
|
return clientIdValidator.Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Token GetValidToken()
|
private Token GetValidAuthorizationCode(IEnumerable<Claim> additionalClaims = null)
|
||||||
{
|
{
|
||||||
var notBefore = EpochTime.GetIntDate(DateTime.UtcNow - TimeSpan.FromMinutes(20)).ToString();
|
var notBefore = EpochTime.GetIntDate(DateTime.UtcNow - TimeSpan.FromMinutes(20)).ToString();
|
||||||
var expires = EpochTime.GetIntDate(DateTime.UtcNow + TimeSpan.FromMinutes(10)).ToString();
|
var expires = EpochTime.GetIntDate(DateTime.UtcNow + TimeSpan.FromMinutes(10)).ToString();
|
||||||
var issuedAt = EpochTime.GetIntDate(DateTime.UtcNow).ToString();
|
var issuedAt = EpochTime.GetIntDate(DateTime.UtcNow).ToString();
|
||||||
var authorizedParty = "clientId";
|
|
||||||
return new TestToken(new Claim[]
|
return new AuthorizationCode(new Claim[]
|
||||||
{
|
{
|
||||||
new Claim(IdentityServiceClaimTypes.TokenUniqueId, Guid.NewGuid().ToString()),
|
new Claim(IdentityServiceClaimTypes.TokenUniqueId, Guid.NewGuid().ToString()),
|
||||||
new Claim(IdentityServiceClaimTypes.RedirectUri, "https://www.example.com"),
|
|
||||||
new Claim(IdentityServiceClaimTypes.NotBefore,notBefore),
|
new Claim(IdentityServiceClaimTypes.NotBefore,notBefore),
|
||||||
new Claim(IdentityServiceClaimTypes.Expires,expires),
|
new Claim(IdentityServiceClaimTypes.Expires,expires),
|
||||||
new Claim(IdentityServiceClaimTypes.IssuedAt,issuedAt),
|
new Claim(IdentityServiceClaimTypes.IssuedAt,issuedAt),
|
||||||
new Claim(IdentityServiceClaimTypes.AuthorizedParty, authorizedParty)
|
new Claim(IdentityServiceClaimTypes.UserId,"userId"),
|
||||||
});
|
new Claim(IdentityServiceClaimTypes.ClientId,"clientId"),
|
||||||
|
new Claim(IdentityServiceClaimTypes.RedirectUri, "https://www.example.com"),
|
||||||
|
new Claim(IdentityServiceClaimTypes.Scope, "openid"),
|
||||||
|
new Claim(IdentityServiceClaimTypes.GrantedToken, "id_token")
|
||||||
|
}
|
||||||
|
.Concat(additionalClaims ?? Enumerable.Empty<Claim>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ITokenManager GetTestTokenManager(
|
private ITokenManager GetTestTokenManager(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue