Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support PKCE #2435

Merged
merged 4 commits into from
Jun 20, 2023
Merged

feat: Support PKCE #2435

merged 4 commits into from
Jun 20, 2023

Conversation

amanda-tarafa
Copy link
Contributor

FYI: @clundin25

@amanda-tarafa amanda-tarafa requested a review from jskeet June 19, 2023 14:08
@amanda-tarafa amanda-tarafa requested a review from a team as a code owner June 19, 2023 14:08
@amanda-tarafa
Copy link
Contributor Author

@jskeet I will ad a few tests to this etc. but just wanted to check with you that this approach looks good.

Copy link
Collaborator

@jskeet jskeet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I basically follow this. Various comments, but nothing huge.

/// The redirect URI for the authorization code request.
/// </param>
/// <param name="codeVerifier">
/// The code verifier associated to the code challenge that should be included
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe highlight that this is an out parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

/// in the returned <see cref="AuthorizationCodeRequestUrl"/>.
/// </param>
/// <returns>An <see cref="AuthorizationCodeRequestUrl"/> subclass instance that includes the code challenge
/// and code challange method associated to <paramref name="codeVerifier"/>.</returns>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

challange => challenge and possibly "associated to" => "associated with"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, both.

/// and code challange method associated to <paramref name="codeVerifier"/>.</returns>
AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri, out string codeVerifier);

/// <summary>Asynchronously exchanges code with a token.</summary>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"code with a token" => "an authorization code for an access token"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here, and from where I copied this.

/// <param name="code">Authorization code received from the authorization server.</param>
/// <param name="codeVerifier">
/// The PKCE code verifier to send along the exchange request.
/// You obtained the code verifier when calling <see cref="CreateAuthorizationCodeRequest(string, out string)"/>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's revisit this sentence when the rest is ready to go.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'm adding a TODO so we don't forget.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, how about:

The PKCE code verifier to include in the exchange request. When called by the authentication library, this will be the same value specified by the <code>codeVerifier</code> out parameter in an earlier call to CreateAuthorizationCodeRequest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, better, done.

@@ -0,0 +1,62 @@
/*
Copyright 2013 Google Inc
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2023, here and below

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

{
byte[] codeVerifierAsciiBytes = Encoding.ASCII.GetBytes(codeVerifier);
byte[] hashedCodeVerifier;
using (var sha256 = SHA256.Create())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify the using statement? (That would allow us to declare hashedCodeVerifier at the point of assignment.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, done

request.CodeChallengeMethod = effectiveChallengeMethod;
}

private static string Base64UrlEncode(byte[] data)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checking that we don't have this anywhere else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually we do, and it did ring a bell when I wrote it.

private static string GenerateCodeVerifier()
{
byte[] data = new byte[CodeVerifierRandomNumberLengthBytes];
using(RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: space after using, or better, let's just use a simplified using statement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, just we really don't need it until the end, but that shouldn't be a problem really.

/// The scopes which indicate the Google API access your application is requesting.
/// </param>
/// <param name="user">The user to authorize.</param>
/// <param name="disablePkce">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to have usePkce if possible - 1) it avoids the term "disable", and 2) it's more positive. I'm okay with this if it's for consistency with other languages' already-released packages though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's fine, I'll swap it.

My intention was mostly to make it explicit that not using PKCE should be the exemption.

@@ -86,8 +103,8 @@ public class GoogleAuthorizationCodeRequestUrl : AuthorizationCodeRequestUrl
/// The name of this parameter is used only for the constructor and will not end up in the resultant query
/// string.
/// </remarks>
[Google.Apis.Util.RequestParameterAttribute("user_defined_query_params",
Google.Apis.Util.RequestParameterType.UserDefinedQueries)]
[RequestParameter("user_defined_query_params",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unwrap line?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor Author

@amanda-tarafa amanda-tarafa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All comments addressed and tests added.
I've also split in 4 commits to make it easier to review.
@jskeet PTAL, thanks!

@@ -0,0 +1,62 @@
/*
Copyright 2013 Google Inc
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

/// The redirect URI for the authorization code request.
/// </param>
/// <param name="codeVerifier">
/// The code verifier associated to the code challenge that should be included
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

/// in the returned <see cref="AuthorizationCodeRequestUrl"/>.
/// </param>
/// <returns>An <see cref="AuthorizationCodeRequestUrl"/> subclass instance that includes the code challenge
/// and code challange method associated to <paramref name="codeVerifier"/>.</returns>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, both.

/// and code challange method associated to <paramref name="codeVerifier"/>.</returns>
AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri, out string codeVerifier);

/// <summary>Asynchronously exchanges code with a token.</summary>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here, and from where I copied this.

/// <param name="code">Authorization code received from the authorization server.</param>
/// <param name="codeVerifier">
/// The PKCE code verifier to send along the exchange request.
/// You obtained the code verifier when calling <see cref="CreateAuthorizationCodeRequest(string, out string)"/>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'm adding a TODO so we don't forget.

{
byte[] codeVerifierAsciiBytes = Encoding.ASCII.GetBytes(codeVerifier);
byte[] hashedCodeVerifier;
using (var sha256 = SHA256.Create())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, done

effectiveChallenge = Base64UrlEncode(hashedCodeVerifier);
effectiveChallengeMethod = ChallengeMethodSha256;
}
catch
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, there's this one case on .NET 4.61, but I'll be more explicit about the exception: https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.sha256.create

request.CodeChallengeMethod = effectiveChallengeMethod;
}

private static string Base64UrlEncode(byte[] data)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually we do, and it did ring a bell when I wrote it.

/// The scopes which indicate the Google API access your application is requesting.
/// </param>
/// <param name="user">The user to authorize.</param>
/// <param name="disablePkce">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's fine, I'll swap it.

My intention was mostly to make it explicit that not using PKCE should be the exemption.

@@ -86,8 +103,8 @@ public class GoogleAuthorizationCodeRequestUrl : AuthorizationCodeRequestUrl
/// The name of this parameter is used only for the constructor and will not end up in the resultant query
/// string.
/// </remarks>
[Google.Apis.Util.RequestParameterAttribute("user_defined_query_params",
Google.Apis.Util.RequestParameterType.UserDefinedQueries)]
[RequestParameter("user_defined_query_params",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Collaborator

@jskeet jskeet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

/// <param name="code">Authorization code received from the authorization server.</param>
/// <param name="codeVerifier">
/// The PKCE code verifier to send along the exchange request.
/// You obtained the code verifier when calling <see cref="CreateAuthorizationCodeRequest(string, out string)"/>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, how about:

The PKCE code verifier to include in the exchange request. When called by the authentication library, this will be the same value specified by the <code>codeVerifier</code> out parameter in an earlier call to CreateAuthorizationCodeRequest.

using RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create();
randomNumberGenerator.GetBytes(data);

// The code verifier alphabet does not allow +, / or =, so we Base64URL encode.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's more that base64-URL-encoding is simply how the code verifier is specified in the RFC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, true, it just doesn't say so anywhere in our docs, just what the alphabet is, so I unconsciously followed along. Changing.

effectiveChallenge = TokenEncodingHelpers.UrlSafeBase64Encode(hashedCodeVerifier);
effectiveChallengeMethod = ChallengeMethodSha256;
}
catch (TargetInvocationException)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly add a comment here as you did in the GitHub conversation to explain why this might happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, done.

Copy link
Contributor Author

@amanda-tarafa amanda-tarafa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments addressed, thanks!

I'll merge on green.

/// <param name="code">Authorization code received from the authorization server.</param>
/// <param name="codeVerifier">
/// The PKCE code verifier to send along the exchange request.
/// You obtained the code verifier when calling <see cref="CreateAuthorizationCodeRequest(string, out string)"/>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, better, done.

using RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create();
randomNumberGenerator.GetBytes(data);

// The code verifier alphabet does not allow +, / or =, so we Base64URL encode.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, true, it just doesn't say so anywhere in our docs, just what the alphabet is, so I unconsciously followed along. Changing.

effectiveChallenge = TokenEncodingHelpers.UrlSafeBase64Encode(hashedCodeVerifier);
effectiveChallengeMethod = ChallengeMethodSha256;
}
catch (TargetInvocationException)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, done.

@amanda-tarafa amanda-tarafa merged commit abf24a9 into googleapis:main Jun 20, 2023
@amanda-tarafa amanda-tarafa deleted the pkce branch June 20, 2023 11:12
@clundin25
Copy link

@amanda-tarafa this is great, thanks!

amanda-tarafa added a commit to amanda-tarafa/google-api-dotnet-client that referenced this pull request Jun 26, 2023
Updates support version: 1.61.0 - beta02 -> 1.61.0

Features

- Improvements for  date/time parsing.
  - googleapis#2441
  - googleapis#2432
  - googleapis#2429
- googleapis#2435 PKCE support.
- googleapis#2394 Which improves impersonation support.
- googleapis#2349 and googleapis#2379 Which improve error reported when ADC is not configured.
amanda-tarafa added a commit that referenced this pull request Jun 26, 2023
Updates support version: 1.61.0 - beta02 -> 1.61.0

Features

- Improvements for  date/time parsing.
  - #2441
  - #2432
  - #2429
- #2435 PKCE support.
- #2394 Which improves impersonation support.
- #2349 and #2379 Which improve error reported when ADC is not configured.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants