Add support for proof of key for code exchange

This commit is contained in:
Javier Calvarro Nelson 2017-05-22 16:51:48 -07:00
parent be8232d3f9
commit 6a3fca86b5
13 changed files with 679 additions and 19 deletions

View File

@ -29,5 +29,7 @@ namespace Microsoft.AspNetCore.Identity.Service
public const string GrantedToken = "g_token";
public const string TenantId = "tid";
public const string Resource = "rid";
public const string CodeChallenge = "c_chall";
public const string CodeChallengeMethod = "c_chall_m";
}
}

View File

@ -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";
}
}

View File

@ -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";
}
}

View File

@ -1,10 +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.
using System.Diagnostics;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Microsoft.AspNetCore.Identity.Service
{
[DebuggerDisplay("{DebuggerDisplay(),nq}")]
public class AuthorizationRequestError
{
public AuthorizationRequestError(OpenIdConnectMessage error, string redirectUri, string responseMode)
@ -19,5 +21,8 @@ namespace Microsoft.AspNetCore.Identity.Service
public string RedirectUri { get; set; }
public string ResponseMode { get; set; }
private string DebuggerDisplay() =>
$"{Message.Error} - {Message.ErrorDescription} - {RedirectUri} - {ResponseMode} - {Message.State}";
}
}

View File

@ -29,6 +29,8 @@ namespace Microsoft.AspNetCore.Identity.Service
public string ClientId => GetClaimValue(IdentityServiceClaimTypes.ClientId);
public string Resource => GetClaimValue(IdentityServiceClaimTypes.Resource);
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> GrantedTokens => GetClaimValuesOrEmpty(IdentityServiceClaimTypes.GrantedToken);
public string Nonce => GetClaimValue(IdentityServiceClaimTypes.Nonce);

View File

@ -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);
result.RequestType = OpenIdConnectRequestType.Authentication;
@ -394,5 +431,6 @@ namespace Microsoft.AspNetCore.Identity.Service
return (clientId, resolvedUriResult.Uri, null);
}
}
}

View File

@ -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;
}
}
}

View File

@ -1,6 +1,7 @@
// 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;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Microsoft.AspNetCore.Identity.Service
@ -78,6 +79,13 @@ namespace Microsoft.AspNetCore.Identity.Service
$"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)
{
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}'");
}
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) =>
new OpenIdConnectMessage
{

View File

@ -4,13 +4,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
namespace Microsoft.AspNetCore.Identity.Service
{
public class TokenRequestFactory : ITokenRequestFactory
{
private static bool[] ValidCodeVerifierCharacters = CreateCodeVerifierValidCharacters();
private readonly IClientIdValidator _clientIdValidator;
private readonly ITokenManager _tokenManager;
private readonly IRedirectUriResolver _redirectUriValidator;
@ -185,16 +189,18 @@ namespace Microsoft.AspNetCore.Identity.Service
string clientId,
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);
if (redirectUriError != null)
{
return redirectUriError;
}
var tokenRedirectUri = consentGrant
.Token.SingleOrDefault(c =>
string.Equals(c.Type, IdentityServiceClaimTypes.RedirectUri, StringComparison.Ordinal))?.Value;
var tokenRedirectUri = code.RedirectUri;
if (redirectUri == null && tokenRedirectUri != null)
{
return _errorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.RedirectUri);
@ -211,8 +217,48 @@ namespace Microsoft.AspNetCore.Identity.Service
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;
}
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)
{
@ -226,5 +272,32 @@ namespace Microsoft.AspNetCore.Identity.Service
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;
}
}
}

View File

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Identity.Service;
using Microsoft.AspNetCore.Identity.Service.Claims;
using Microsoft.AspNetCore.Identity.Service.Configuration;
using Microsoft.AspNetCore.Identity.Service.Core;
using Microsoft.AspNetCore.Identity.Service.Core.Claims;
using Microsoft.AspNetCore.Identity.Service.Metadata;
using Microsoft.AspNetCore.Identity.Service.Serialization;
using Microsoft.Extensions.DependencyInjection.Extensions;
@ -82,6 +83,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddSingleton<ITokenClaimsProvider, ScopesTokenClaimsProvider>();
services.AddSingleton<ITokenClaimsProvider, TimestampsTokenClaimsProvider>();
services.AddSingleton<ITokenClaimsProvider, TokenHashTokenClaimsProvider>();
services.AddSingleton<ITokenClaimsProvider, ProofOfKeyForCodeExchangeTokenClaimsProvider>();
services.AddSingleton<ProtocolErrorProvider>();
services.AddSingleton<ISigningCredentialsSource, DeveloperCertificateSigningCredentialsSource>();
services.AddSingleton<DeveloperCertificateSigningCredentialsSource>();

View File

@ -718,6 +718,165 @@ namespace Microsoft.AspNetCore.Identity.Service
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]
public async Task CreatesAnAuthorizationRequest_IfAllParameters_AreCorrect()
{

View File

@ -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);
}
}
}

View File

@ -144,7 +144,7 @@ namespace Microsoft.AspNetCore.Identity.Service
Mock.Of<IClientIdValidator>(),
Mock.Of<IRedirectUriResolver>(), Mock.Of<IScopeResolver>(),
Enumerable.Empty<ITokenRequestValidator>(),
GetTestTokenManager(GetValidToken()),
GetTestTokenManager(GetValidAuthorizationCode()),
new TimeStampManager(), new ProtocolErrorProvider());
var expectedError = ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.ClientId);
@ -173,7 +173,7 @@ namespace Microsoft.AspNetCore.Identity.Service
GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true),
Mock.Of<IRedirectUriResolver>(), Mock.Of<IScopeResolver>(),
Enumerable.Empty<ITokenRequestValidator>(),
GetTestTokenManager(GetValidToken()),
GetTestTokenManager(GetValidAuthorizationCode()),
new TimeStampManager(), new ProtocolErrorProvider());
var expectedError = ProtocolErrorProvider.InvalidGrant();
@ -200,7 +200,7 @@ namespace Microsoft.AspNetCore.Identity.Service
GetClientIdValidator(isClientIdValid: false),
Mock.Of<IRedirectUriResolver>(), Mock.Of<IScopeResolver>(),
Enumerable.Empty<ITokenRequestValidator>(),
GetTestTokenManager(GetValidToken()),
GetTestTokenManager(GetValidAuthorizationCode()),
new TimeStampManager(), new ProtocolErrorProvider());
var expectedError = ProtocolErrorProvider.InvalidClientId("clientId");
@ -227,7 +227,7 @@ namespace Microsoft.AspNetCore.Identity.Service
GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: false),
Mock.Of<IRedirectUriResolver>(), Mock.Of<IScopeResolver>(),
Enumerable.Empty<ITokenRequestValidator>(),
GetTestTokenManager(GetValidToken()),
GetTestTokenManager(GetValidAuthorizationCode()),
new TimeStampManager(), new ProtocolErrorProvider());
var expectedError = ProtocolErrorProvider.InvalidClientCredentials();
@ -258,7 +258,7 @@ namespace Microsoft.AspNetCore.Identity.Service
Mock.Of<IRedirectUriResolver>(),
Mock.Of<IScopeResolver>(),
Enumerable.Empty<ITokenRequestValidator>(),
GetTestTokenManager(GetValidToken()),
GetTestTokenManager(GetValidAuthorizationCode()),
new TimeStampManager(), new ProtocolErrorProvider());
var expectedError = ProtocolErrorProvider.TooManyParameters(OpenIdConnectParameterNames.Scope);
@ -289,7 +289,7 @@ namespace Microsoft.AspNetCore.Identity.Service
Mock.Of<IRedirectUriResolver>(),
GetScopeResolver(hasInvalidScopes: true),
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());
var expectedError = ProtocolErrorProvider.InvalidScope("invalid");
@ -320,7 +320,7 @@ namespace Microsoft.AspNetCore.Identity.Service
Mock.Of<IRedirectUriResolver>(),
GetScopeResolver(hasInvalidScopes: false),
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());
var expectedError = ProtocolErrorProvider.UnauthorizedScope();
@ -350,7 +350,7 @@ namespace Microsoft.AspNetCore.Identity.Service
GetRedirectUriValidator(isRedirectUriValid: false),
Mock.Of<IScopeResolver>(),
Enumerable.Empty<ITokenRequestValidator>(),
GetTestTokenManager(GetValidToken()),
GetTestTokenManager(GetValidAuthorizationCode()),
new TimeStampManager(), new ProtocolErrorProvider());
var expectedError = ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.RedirectUri);
@ -364,6 +364,214 @@ namespace Microsoft.AspNetCore.Identity.Service
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)
{
var mock = new Mock<IRedirectUriResolver>();
@ -390,7 +598,7 @@ namespace Microsoft.AspNetCore.Identity.Service
GetClientIdValidator(isClientIdValid: true, areClientCredentialsValid: true),
Mock.Of<IRedirectUriResolver>(), Mock.Of<IScopeResolver>(),
Enumerable.Empty<ITokenRequestValidator>(),
GetTestTokenManager(GetValidToken()),
GetTestTokenManager(GetValidAuthorizationCode()),
new TimeStampManager(), new ProtocolErrorProvider());
var expectedError = ProtocolErrorProvider.MissingRequiredParameter(OpenIdConnectParameterNames.RedirectUri);
@ -419,21 +627,25 @@ namespace Microsoft.AspNetCore.Identity.Service
return clientIdValidator.Object;
}
private Token GetValidToken()
private Token GetValidAuthorizationCode(IEnumerable<Claim> additionalClaims = null)
{
var notBefore = EpochTime.GetIntDate(DateTime.UtcNow - TimeSpan.FromMinutes(20)).ToString();
var expires = EpochTime.GetIntDate(DateTime.UtcNow + TimeSpan.FromMinutes(10)).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.RedirectUri, "https://www.example.com"),
new Claim(IdentityServiceClaimTypes.NotBefore,notBefore),
new Claim(IdentityServiceClaimTypes.Expires,expires),
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(