diff --git a/oauth2_http/java/com/google/auth/oauth2/DefaultPKCEProvider.java b/oauth2_http/java/com/google/auth/oauth2/DefaultPKCEProvider.java new file mode 100644 index 000000000..c114383ff --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/DefaultPKCEProvider.java @@ -0,0 +1,103 @@ +/* + * Copyright 2023, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.auth.oauth2; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; + +public class DefaultPKCEProvider implements PKCEProvider { + private String codeVerifier; + private CodeChallenge codeChallenge; + private static final int MAX_CODE_VERIFIER_LENGTH = 127; + + private class CodeChallenge { + private String codeChallenge; + private String codeChallengeMethod; + + CodeChallenge(String codeVerifier) { + try { + byte[] bytes = codeVerifier.getBytes(); + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(bytes); + + byte[] digest = md.digest(); + + this.codeChallenge = Base64.getUrlEncoder().encodeToString(digest); + this.codeChallengeMethod = "S256"; + } catch (NoSuchAlgorithmException e) { + this.codeChallenge = codeVerifier; + this.codeChallengeMethod = "plain"; + } + } + + public String getCodeChallenge() { + return codeChallenge; + } + + public String getCodeChallengeMethod() { + return codeChallengeMethod; + } + } + + private String createCodeVerifier() { + SecureRandom sr = new SecureRandom(); + byte[] code = new byte[MAX_CODE_VERIFIER_LENGTH]; + sr.nextBytes(code); + return Base64.getUrlEncoder().encodeToString(code); + } + + private CodeChallenge createCodeChallenge(String codeVerifier) { + return new DefaultPKCEProvider.CodeChallenge(codeVerifier); + } + + public DefaultPKCEProvider() { + this.codeVerifier = createCodeVerifier(); + this.codeChallenge = createCodeChallenge(this.codeVerifier); + } + + @Override + public String getCodeVerifier() { + return codeVerifier; + } + + @Override + public String getCodeChallenge() { + return codeChallenge.getCodeChallenge(); + } + + @Override + public String getCodeChallengeMethod() { + return codeChallenge.getCodeChallengeMethod(); + } +} diff --git a/oauth2_http/java/com/google/auth/oauth2/PKCEProvider.java b/oauth2_http/java/com/google/auth/oauth2/PKCEProvider.java new file mode 100644 index 000000000..4800dd1cd --- /dev/null +++ b/oauth2_http/java/com/google/auth/oauth2/PKCEProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.auth.oauth2; + +public interface PKCEProvider { + /** + * Get the code_challenge parameter used in PKCE. + * + * @return The code_challenge String. + */ + String getCodeChallenge(); + + /** + * Get the code_challenge_method parameter used in PKCE. + * + *
Currently possible values are: S256,plain
+ *
+ * @return The code_challenge_method String.
+ */
+ String getCodeChallengeMethod();
+ /**
+ * Get the code_verifier parameter used in PKCE.
+ *
+ * @return The code_verifier String.
+ */
+ String getCodeVerifier();
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/UserAuthorizer.java b/oauth2_http/java/com/google/auth/oauth2/UserAuthorizer.java
index c152b8b44..29a8284d5 100644
--- a/oauth2_http/java/com/google/auth/oauth2/UserAuthorizer.java
+++ b/oauth2_http/java/com/google/auth/oauth2/UserAuthorizer.java
@@ -67,6 +67,7 @@ public class UserAuthorizer {
private final HttpTransportFactory transportFactory;
private final URI tokenServerUri;
private final URI userAuthUri;
+ private final PKCEProvider pkce;
/**
* Constructor with all parameters.
@@ -79,6 +80,7 @@ public class UserAuthorizer {
* tokens.
* @param tokenServerUri URI of the end point that provides tokens
* @param userAuthUri URI of the Web UI for user consent
+ * @param pkce PKCE implementation
*/
private UserAuthorizer(
ClientId clientId,
@@ -87,7 +89,8 @@ private UserAuthorizer(
URI callbackUri,
HttpTransportFactory transportFactory,
URI tokenServerUri,
- URI userAuthUri) {
+ URI userAuthUri,
+ PKCEProvider pkce) {
this.clientId = Preconditions.checkNotNull(clientId);
this.scopes = ImmutableList.copyOf(Preconditions.checkNotNull(scopes));
this.callbackUri = (callbackUri == null) ? DEFAULT_CALLBACK_URI : callbackUri;
@@ -96,6 +99,7 @@ private UserAuthorizer(
this.tokenServerUri = (tokenServerUri == null) ? OAuth2Utils.TOKEN_SERVER_URI : tokenServerUri;
this.userAuthUri = (userAuthUri == null) ? OAuth2Utils.USER_AUTH_URI : userAuthUri;
this.tokenStore = (tokenStore == null) ? new MemoryTokensStorage() : tokenStore;
+ this.pkce = pkce;
}
/**
@@ -181,6 +185,10 @@ public URL getAuthorizationUrl(String userId, String state, URI baseUri) {
url.put("login_hint", userId);
}
url.put("include_granted_scopes", true);
+ if (pkce != null) {
+ url.put("code_challenge", pkce.getCodeChallenge());
+ url.put("code_challenge_method", pkce.getCodeChallengeMethod());
+ }
return url.toURL();
}
@@ -248,6 +256,11 @@ public UserCredentials getCredentialsFromCode(String code, URI baseUri) throws I
tokenData.put("client_secret", clientId.getClientSecret());
tokenData.put("redirect_uri", resolvedCallbackUri);
tokenData.put("grant_type", "authorization_code");
+
+ if (pkce != null) {
+ tokenData.put("code_verifier", pkce.getCodeVerifier());
+ }
+
UrlEncodedContent tokenContent = new UrlEncodedContent(tokenData);
HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory();
HttpRequest tokenRequest =
@@ -430,6 +443,7 @@ public static class Builder {
private URI userAuthUri;
private Collection