Skip to content

Commit

Permalink
feat: Support PKCE
Browse files Browse the repository at this point in the history
  • Loading branch information
amanda-tarafa committed Jun 19, 2023
1 parent 5f6c410 commit d675d5e
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ limitations under the License.
using Google.Apis.Auth.OAuth2.Responses;
using Google.Apis.Auth.OAuth2.Requests;
using Google.Apis.Logging;
using Google.Apis.Util;
using Google.Apis.Util.Store;

namespace Google.Apis.Auth.OAuth2
{
Expand All @@ -35,15 +37,19 @@ public class AuthorizationCodeInstalledApp : IAuthorizationCodeInstalledApp
{
private static readonly ILogger Logger = ApplicationContext.Logger.ForType<AuthorizationCodeInstalledApp>();

private readonly IAuthorizationCodeFlow flow;
private readonly IPkceAuthorizationCodeFlow flow;
private readonly ICodeReceiver codeReceiver;

/// <summary>
/// Constructs a new authorization code installed application with the given flow and code receiver.
/// </summary>
public AuthorizationCodeInstalledApp(IAuthorizationCodeFlow flow, ICodeReceiver codeReceiver)
{
this.flow = flow;
this.flow = flow switch
{
IPkceAuthorizationCodeFlow pkceFlow => pkceFlow,
_ => new NoOpPckeAuthorizationFlow(flow)
};
this.codeReceiver = codeReceiver;
}

Expand Down Expand Up @@ -72,7 +78,7 @@ public async Task<UserCredential> AuthorizeAsync(string userId, CancellationToke
{
// Create an authorization code request.
var redirectUri = CodeReceiver.RedirectUri;
AuthorizationCodeRequestUrl codeRequest = Flow.CreateAuthorizationCodeRequest(redirectUri);
AuthorizationCodeRequestUrl codeRequest = flow.CreateAuthorizationCodeRequest(redirectUri, out string codeVerifier);

// Receive the code.
var response = await CodeReceiver.ReceiveCodeAsync(codeRequest, taskCancellationToken)
Expand All @@ -88,7 +94,7 @@ public async Task<UserCredential> AuthorizeAsync(string userId, CancellationToke
Logger.Debug("Received \"{0}\" code", response.Code);

// Get the token based on the code.
token = await Flow.ExchangeCodeForTokenAsync(userId, response.Code, redirectUri,
token = await flow.ExchangeCodeForTokenAsync(userId, response.Code, codeVerifier, redirectUri,
taskCancellationToken).ConfigureAwait(false);
}

Expand All @@ -109,5 +115,57 @@ public bool ShouldRequestAuthorizationCode(TokenResponse token)
}

#endregion

/// <summary>
/// Helper class to wrap non PKCE flows so that <see cref="AuthorizationCodeInstalledApp"/>
/// does not need to know whether its flow supports PKCE or not.
/// </summary>
private class NoOpPckeAuthorizationFlow : IPkceAuthorizationCodeFlow
{
private readonly IAuthorizationCodeFlow flow;

internal NoOpPckeAuthorizationFlow(IAuthorizationCodeFlow flow) => this.flow = flow;

public IAccessMethod AccessMethod => flow.AccessMethod;

public IClock Clock => flow.Clock;

public IDataStore DataStore => flow.DataStore;

public AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri, out string codeVerifier)
{
// Let's return an invalid codeVerifier just to make certain that it wouldn't be accepted server side
// if we were to have a bug an include it in the request. But this should never be included in a request.
codeVerifier = "invalid+*/";
return flow.CreateAuthorizationCodeRequest(redirectUri);
}

public AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri) =>
flow.CreateAuthorizationCodeRequest(redirectUri);

public Task DeleteTokenAsync(string userId, CancellationToken taskCancellationToken) =>
flow.DeleteTokenAsync(userId, taskCancellationToken);

public void Dispose() => flow.Dispose();

public Task<TokenResponse> ExchangeCodeForTokenAsync(string userId, string code, string codeVerifier, string redirectUri, CancellationToken taskCancellationToken) =>
// We ignore the codeVerifier parameter.
flow.ExchangeCodeForTokenAsync(userId, code, redirectUri, taskCancellationToken);

public Task<TokenResponse> ExchangeCodeForTokenAsync(string userId, string code, string redirectUri, CancellationToken taskCancellationToken) =>
flow.ExchangeCodeForTokenAsync(userId, code, redirectUri, taskCancellationToken);

public Task<TokenResponse> LoadTokenAsync(string userId, CancellationToken taskCancellationToken) =>
flow.LoadTokenAsync(userId, taskCancellationToken);

public Task<TokenResponse> RefreshTokenAsync(string userId, string refreshToken, CancellationToken taskCancellationToken) =>
flow.RefreshTokenAsync(userId, refreshToken, taskCancellationToken);

public Task RevokeTokenAsync(string userId, string token, CancellationToken taskCancellationToken) =>
flow.RevokeTokenAsync(userId, token, taskCancellationToken);

public bool ShouldForceTokenRetrieval() =>
flow.ShouldForceTokenRetrieval();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,28 @@ public virtual AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string
public async Task<TokenResponse> ExchangeCodeForTokenAsync(string userId, string code, string redirectUri,
CancellationToken taskCancellationToken)
{
var authorizationCodeTokenReq = new AuthorizationCodeTokenRequest
var authorizationCodeTokenReq = CreateAuthorizationCodeTokenRequest(userId, code, redirectUri);

return await ExchangeCodeForTokenAsync(userId, authorizationCodeTokenReq, taskCancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Creates a <see cref="AuthorizationCodeTokenRequest"/> for the given parameters.
/// </summary>
protected internal AuthorizationCodeTokenRequest CreateAuthorizationCodeTokenRequest(string userId, string code, string redirectUri) =>
new AuthorizationCodeTokenRequest
{
Scope = Scopes == null ? null : string.Join(" ", Scopes),
RedirectUri = redirectUri,
Code = code,
};

/// <summary>
/// Executes <paramref name="authorizationCodeTokenReq"/> and stores and returns the received token.
/// </summary>
protected internal async Task<TokenResponse> ExchangeCodeForTokenAsync(string userId, AuthorizationCodeTokenRequest authorizationCodeTokenReq,
CancellationToken taskCancellationToken)
{
var token = await FetchTokenAsync(userId, authorizationCodeTokenReq, taskCancellationToken)
.ConfigureAwait(false);
await StoreTokenAsync(userId, token, taskCancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
Copyright 2013 Google Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

using Google.Apis.Auth.OAuth2.Requests;
using Google.Apis.Auth.OAuth2.Responses;
using System.Threading;
using System.Threading.Tasks;

namespace Google.Apis.Auth.OAuth2.Flows
{
/// <summary>
/// Authorization flow that supports Proof Key for Code Exchange (PKCE)
/// as described in https://www.rfc-editor.org/rfc/rfc7636.
/// </summary>
/// <remarks>
/// If you are writing your own authorization flow to be used with <see cref="AuthorizationCodeInstalledApp"/>
/// make sure you implement this interface if you need to support PKCE.
/// See https://developers.google.com/identity/protocols/oauth2/native-app for how Google supports PKCE.
/// </remarks>
public interface IPkceAuthorizationCodeFlow : IAuthorizationCodeFlow
{
/// <summary>
/// Creates an authorization code request with the specified redirect URI.
/// </summary>
/// <param name="redirectUri">
/// The redirect URI for the authorization code request.
/// </param>
/// <param name="codeVerifier">
/// The code verifier associated to the code challenge that should be included
/// 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>
AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri, out string codeVerifier);

/// <summary>Asynchronously exchanges code with a token.</summary>
/// <param name="userId">User identifier.</param>
/// <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)"/>
/// for creating the authorization code request that yielded <paramref name="code"/>.
/// </param>
/// <param name="redirectUri">Redirect URI which is used in the token request.</param>
/// <param name="taskCancellationToken">Cancellation token to cancel operation.</param>
/// <returns>Token response which contains the access token.</returns>
Task<TokenResponse> ExchangeCodeForTokenAsync(string userId, string code, string codeVerifier, string redirectUri, CancellationToken taskCancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
Copyright 2013 Google Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

using Google.Apis.Auth.OAuth2.Requests;
using Google.Apis.Auth.OAuth2.Responses;
using System;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Google.Apis.Auth.OAuth2.Flows
{
/// <summary>
/// Google authorization flow implementation that supports PKCE as described in https://www.rfc-editor.org/rfc/rfc7636
/// and https://developers.google.com/identity/protocols/oauth2/native-app.
/// </summary>
public class PkceGoogleAuthorizationCodeFlow : GoogleAuthorizationCodeFlow, IPkceAuthorizationCodeFlow
{
private const int CodeVerifierMaxLengthChars = 128;
// We generate the random bytes and then encode them to Base64 whose characters are 6 bits long.
private const int CodeVerifierRandomNumberLengthBytes = CodeVerifierMaxLengthChars * 6 / 8;
private const string ChallengeMethodSha256 = "S256";
private const string ChallengeMethodPlain = "plain";

/// <summary>
/// Creates a new instance from the given initializer.
/// </summary>
public PkceGoogleAuthorizationCodeFlow(Initializer initializer) : base(initializer)
{
}

/// <inheritdoc/>
public AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri, out string codeVerifier)
{
var request = CreateAuthorizationCodeRequest(redirectUri) as GoogleAuthorizationCodeRequestUrl;
codeVerifier = GenerateCodeVerifier();
AddCodeChallenge(request, codeVerifier);
return request;
}

/// <inheritdoc/>
public async Task<TokenResponse> ExchangeCodeForTokenAsync(string userId, string code, string codeVerifier, string redirectUri, CancellationToken taskCancellationToken)
{
var request = CreateAuthorizationCodeTokenRequest(userId, code, redirectUri);
request.CodeVerifier = codeVerifier;

return await ExchangeCodeForTokenAsync(userId, request, taskCancellationToken).ConfigureAwait(false);
}

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

// The code verifier alphabet does not allow +, / or =, so we Base64URL encode.
// https://developers.google.com/identity/protocols/oauth2/native-app#step1-code-verifier
return Base64UrlEncode(data);
}

private static void AddCodeChallenge(GoogleAuthorizationCodeRequestUrl request, string codeVerifier)
{
// https://developers.google.com/identity/protocols/oauth2/native-app#step1-code-verifier
string effectiveChallenge;
string effectiveChallengeMethod;
try
{
byte[] codeVerifierAsciiBytes = Encoding.ASCII.GetBytes(codeVerifier);
byte[] hashedCodeVerifier;
using (var sha256 = SHA256.Create())
{
hashedCodeVerifier = sha256.ComputeHash(codeVerifierAsciiBytes);
}
effectiveChallenge = Base64UrlEncode(hashedCodeVerifier);
effectiveChallengeMethod = ChallengeMethodSha256;
}
catch
{
effectiveChallenge = codeVerifier;
effectiveChallengeMethod = ChallengeMethodPlain;
}

request.CodeChallenge = effectiveChallenge;
request.CodeChallengeMethod = effectiveChallengeMethod;
}

private static string Base64UrlEncode(byte[] data)
{
StringBuilder codeVerifierBuilder = new StringBuilder(Convert.ToBase64String(data));
// Remove = padding.
while (codeVerifierBuilder[codeVerifierBuilder.Length - 1] == '=')
{
codeVerifierBuilder.Length--;
}
codeVerifierBuilder.Replace('+', '-');
codeVerifierBuilder.Replace('/', '_');
return codeVerifierBuilder.ToString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,35 @@ public static async Task ReauthorizeAsync(UserCredential userCredential,
public static async Task<UserCredential> AuthorizeAsync(
GoogleAuthorizationCodeFlow.Initializer initializer, IEnumerable<string> scopes, string user,
CancellationToken taskCancellationToken, IDataStore dataStore = null,
ICodeReceiver codeReceiver = null) =>
await AuthorizeAsync(initializer, scopes, user, disablePkce: false, taskCancellationToken, dataStore, codeReceiver).ConfigureAwait(false);

/// <summary>
/// The core logic for asynchronously authorizing the specified user.
/// Requires user interaction; see <see cref="GoogleWebAuthorizationBroker"/> remarks for more details.
/// </summary>
/// <param name="initializer">The authorization code initializer.</param>
/// <param name="scopes">
/// The scopes which indicate the Google API access your application is requesting.
/// </param>
/// <param name="user">The user to authorize.</param>
/// <param name="disablePkce">
/// If true, PKCE is disabled, note that this is not recommended.
/// See https://developers.google.com/identity/protocols/oauth2/native-app for more information.
/// </param>
/// <param name="taskCancellationToken">Cancellation token to cancel an operation.</param>
/// <param name="dataStore">The data store, if not specified a file data store will be used.</param>
/// <param name="codeReceiver">The code receiver, if not specified a local server code receiver will be used.</param>
/// <returns>User credential.</returns>
public static async Task<UserCredential> AuthorizeAsync(
GoogleAuthorizationCodeFlow.Initializer initializer, IEnumerable<string> scopes, string user, bool disablePkce,
CancellationToken taskCancellationToken, IDataStore dataStore = null,
ICodeReceiver codeReceiver = null)
{
initializer.Scopes = scopes;
initializer.DataStore = dataStore ?? new FileDataStore(Folder);

var flow = new GoogleAuthorizationCodeFlow(initializer);
var flow = disablePkce ? new GoogleAuthorizationCodeFlow(initializer) : new PkceGoogleAuthorizationCodeFlow(initializer);
codeReceiver = codeReceiver ?? new LocalServerCodeReceiver();

// Create an authorization code installed app instance and authorize the user.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ You may obtain a copy of the License at
limitations under the License.
*/

using System;
using Google.Apis.Util;

namespace Google.Apis.Auth.OAuth2.Requests
{
Expand All @@ -25,15 +25,24 @@ namespace Google.Apis.Auth.OAuth2.Requests
public class AuthorizationCodeTokenRequest : TokenRequest
{
/// <summary>Gets or sets the authorization code received from the authorization server.</summary>
[Google.Apis.Util.RequestParameterAttribute("code")]
[RequestParameter("code")]
public string Code { get; set; }

/// <summary>
/// Gets or sets the redirect URI parameter matching the redirect URI parameter in the authorization request.
/// </summary>
[Google.Apis.Util.RequestParameterAttribute("redirect_uri")]
[RequestParameter("redirect_uri")]
public string RedirectUri { get; set; }


/// <summary>
/// Gets or sets the code verifier matching the code challenge in the authorization request.
/// See https://developers.google.com/identity/protocols/oauth2/native-app#exchange-authorization-code
/// for more information.
/// </summary>
[RequestParameter("code_verifier")]
public string CodeVerifier { get; set; }

/// <summary>
/// Constructs a new authorization code token request and sets grant_type to <c>authorization_code</c>.
/// </summary>
Expand Down
Loading

0 comments on commit d675d5e

Please sign in to comment.