Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Implement PKCE for OIDC #7734
  • Loading branch information
Tratcher committed Jun 4, 2019
commit 8be27083f68cb0bc8b807cde8f70e098855d1991
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
Expand All @@ -30,8 +31,11 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
public class OpenIdConnectHandler : RemoteAuthenticationHandler<OpenIdConnectOptions>, IAuthenticationSignOutHandler
{
private const string NonceProperty = "N";

private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT";
private const string CodeVerifierKey = "code_verifier";
private const string CodeChallengeKey = "code_challenge";
private const string CodeChallengeMethodKey = "code_challenge_method";
private const string CodeChallengeMethodS256 = "S256";

private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();

Expand Down Expand Up @@ -366,6 +370,24 @@ private async Task HandleChallengeAsyncInternal(AuthenticationProperties propert
Scope = string.Join(" ", properties.GetParameter<ICollection<string>>(OpenIdConnectParameterNames.Scope) ?? Options.Scope),
};

// https://tools.ietf.org/html/rfc7636
if (Options.UsePkse)
{
var verifierBytes = new byte[32];
CryptoRandom.GetBytes(verifierBytes);
var codeVerifier = WebEncoders.Base64UrlEncode(verifierBytes);

// Store this for use during the code redemption. See RunAuthorizationCodeReceivedEventAsync.
properties.Items.Add(CodeVerifierKey, codeVerifier);

using var sha256 = SHA256.Create();
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
var codeChallenge = WebEncoders.Base64UrlEncode(challengeBytes);

message.Parameters.Add(CodeChallengeKey, codeChallenge);
message.Parameters.Add(CodeChallengeMethodKey, CodeChallengeMethodS256);
}

// Add the 'max_age' parameter to the authentication request if MaxAge is not null.
// See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
var maxAge = properties.GetParameter<TimeSpan?>(OpenIdConnectParameterNames.MaxAge) ?? Options.MaxAge;
Expand Down Expand Up @@ -1097,6 +1119,12 @@ private async Task<AuthorizationCodeReceivedContext> RunAuthorizationCodeReceive
RedirectUri = properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey]
};

// PKCE https://tools.ietf.org/html/rfc7636#section-4.5, see HandleChallengeAsyncInternal
if (properties.Items.TryGetValue(CodeVerifierKey, out var codeVerifier))
{
tokenEndpointRequest.Parameters.Add(CodeVerifierKey, codeVerifier);
}

var context = new AuthorizationCodeReceivedContext(Context, Scheme, Options, properties)
{
ProtocolMessage = authorizationResponse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,13 @@ public CookieBuilder NonceCookie
set => _nonceCookieBuilder = value ?? throw new ArgumentNullException(nameof(value));
}

/// <summary>
/// Enables or disables the use of the Proof Key for Code Exchange (PKCE) standard.
/// See https://tools.ietf.org/html/rfc7636.
/// The default value is `true`.
/// </summary>
public bool UsePkse { get; set; } = true;

private class OpenIdConnectNonceCookieBuilder : RequestPathBaseCookieBuilder
{
private readonly OpenIdConnectOptions _options;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,39 @@ public async Task ChallengeRedirectIsIssuedCorrectly()
OpenIdConnectParameterNames.VersionTelemetry);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task ChallengeIncludesPkceIfRequested(bool include)
{
var settings = new TestSettings(
opt =>
{
opt.Authority = TestServerBuilder.DefaultAuthority;
opt.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
opt.ClientId = "Test Id";
opt.UsePkse = include;
});

var server = settings.CreateTestServer();
var transaction = await server.SendAsync(ChallengeEndpoint);

var res = transaction.Response;
Assert.Equal(HttpStatusCode.Redirect, res.StatusCode);
Assert.NotNull(res.Headers.Location);

if (include)
{
Assert.Contains("code_challenge=", res.Headers.Location.Query);
Assert.Contains("code_challenge_method=S256", res.Headers.Location.Query);
}
else
{
Assert.DoesNotContain("code_challenge=", res.Headers.Location.Query);
Assert.DoesNotContain("code_challenge_method=", res.Headers.Location.Query);
}
}

[Fact]
public async Task AuthorizationRequestDoesNotIncludeTelemetryParametersWhenDisabled()
{
Expand Down Expand Up @@ -613,4 +646,4 @@ public async Task Challenge_HasOverwrittenMaxAgeParaFromBaseAuthenticationProper
Assert.Contains("max_age=1234", res.Headers.Location.Query);
}
}
}
}