Skip to content
Permalink
Browse files
feat: add PKCE support to AuthorizationCodeFlow (#470)
* Initial test code for a PKCE enabled Authorization Code Flow

* WIP: work on README.md

* Script to initialize keycloak by adding client via REST API.

* Improve keycloak init script and some code cleanup. Still WIP.

* WIP: work on README.md

* Working PKCE AuthorizationCodeFlow. Some cleanup of test classes.

* Add scopes back to the AuthorizationCodeRequestUrl creation.

* Simplify code by moving PKCE entirely into the AuthorizationCodeFlow class. Add documentation.

* Remove wildcard imports as that seems to be the way to do things here.

* Add @SInCE annotation in JavaDoc to the PKCE parameters of the autorization url class.

* Add PKCE unit test, documentation and minor cleanup of dependencies for code sample.

* Add PKCE unit test, documentation and minor cleanup of dependencies for code sample.

* Annotate PKCE with Beta annotation.

* Responding to code review comments

* Responding to more PR comments

* Improve Keycloak PKCE sample documentation

* Add license header with copyright to new files. Improve documentation.
  • Loading branch information
StFS committed Jun 29, 2020
1 parent 6447917 commit 13433cd7dd06267fc261f0b1d4764f8e3432c824
@@ -17,10 +17,14 @@
import com.google.api.client.auth.oauth2.Credential.AccessMethod;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpExecuteInterceptor;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.UrlEncodedContent;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.util.Base64;
import com.google.api.client.util.Beta;
import com.google.api.client.util.Data;
import com.google.api.client.util.Clock;
import com.google.api.client.util.Joiner;
import com.google.api.client.util.Lists;
@@ -29,8 +33,12 @@
import com.google.api.client.util.store.DataStoreFactory;

import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;

import static com.google.api.client.util.Strings.isNullOrEmpty;

@@ -85,6 +93,9 @@
/** Authorization server encoded URL. */
private final String authorizationServerEncodedUrl;

/** The Proof Key for Code Exchange (PKCE) or {@code null} if this flow should not use PKCE. */
private final PKCE pkce;

/** Credential persistence store or {@code null} for none. */
@Beta
@Deprecated
@@ -159,6 +170,7 @@ protected AuthorizationCodeFlow(Builder builder) {
clock = Preconditions.checkNotNull(builder.clock);
credentialCreatedListener = builder.credentialCreatedListener;
refreshListeners = Collections.unmodifiableCollection(builder.refreshListeners);
pkce = builder.pkce;
}

/**
@@ -182,8 +194,13 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro
* </pre>
*/
public AuthorizationCodeRequestUrl newAuthorizationUrl() {
return new AuthorizationCodeRequestUrl(authorizationServerEncodedUrl, clientId).setScopes(
scopes);
AuthorizationCodeRequestUrl url = new AuthorizationCodeRequestUrl(authorizationServerEncodedUrl, clientId);
url.setScopes(scopes);
if (pkce != null) {
url.setCodeChallenge(pkce.getChallenge());
url.setCodeChallengeMethod(pkce.getChallengeMethod());
}
return url;
}

/**
@@ -206,9 +223,20 @@ static TokenResponse requestAccessToken(AuthorizationCodeFlow flow, String code)
* @param authorizationCode authorization code.
*/
public AuthorizationCodeTokenRequest newTokenRequest(String authorizationCode) {
HttpExecuteInterceptor pkceClientAuthenticationWrapper = new HttpExecuteInterceptor() {
@Override
public void intercept(HttpRequest request) throws IOException {
clientAuthentication.intercept(request);
if (pkce != null) {
Map<String, Object> data = Data.mapOf(UrlEncodedContent.getContent(request).getData());
data.put("code_verifier", pkce.getVerifier());
}
}
};

return new AuthorizationCodeTokenRequest(transport, jsonFactory,
new GenericUrl(tokenServerEncodedUrl), authorizationCode).setClientAuthentication(
clientAuthentication).setRequestInitializer(requestInitializer).setScopes(scopes);
pkceClientAuthenticationWrapper).setRequestInitializer(requestInitializer).setScopes(scopes);
}

/**
@@ -412,6 +440,61 @@ public final Clock getClock() {
void onCredentialCreated(Credential credential, TokenResponse tokenResponse) throws IOException;
}

/**
* An implementation of <a href="https://tools.ietf.org/html/rfc7636">Proof Key for Code Exchange</a>
* which, according to the <a href="https://tools.ietf.org/html/rfc8252#section-6">OAuth 2.0 for Native Apps RFC</a>,
* is mandatory for public native apps.
*/
private static class PKCE {
private final String verifier;
private String challenge;
private String challengeMethod;

public PKCE() {
verifier = generateVerifier();
generateChallenge(verifier);
}

private static String generateVerifier() {
SecureRandom sr = new SecureRandom();
byte[] code = new byte[32];
sr.nextBytes(code);
return Base64.encodeBase64URLSafeString(code);
}

/**
* Create the PKCE code verifier. It uses the S256 method but
* falls back to using the 'plain' method in the unlikely case
* that the SHA-256 MessageDigest algorithm implementation can't be
* loaded.
*/
private void generateChallenge(String verifier) {
try {
byte[] bytes = verifier.getBytes();
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(bytes, 0, bytes.length);
byte[] digest = md.digest();
challenge = Base64.encodeBase64URLSafeString(digest);
challengeMethod = "S256";
} catch (NoSuchAlgorithmException e) {
challenge = verifier;
challengeMethod = "plain";
}
}

public String getVerifier() {
return verifier;
}

public String getChallenge() {
return challenge;
}

public String getChallengeMethod() {
return challengeMethod;
}
}

/**
* Authorization code flow builder.
*
@@ -448,6 +531,8 @@ public final Clock getClock() {
/** Authorization server encoded URL. */
String authorizationServerEncodedUrl;

PKCE pkce;

/** Credential persistence store or {@code null} for none. */
@Deprecated
@Beta
@@ -784,6 +869,16 @@ public Builder setRequestInitializer(HttpRequestInitializer requestInitializer)
return this;
}

/**
* Enables Proof Key for Code Exchange (PKCE) for this Athorization Code Flow.
* @since 1.31
*/
@Beta
public Builder enablePKCE() {
this.pkce = new PKCE();
return this;
}

/**
* Sets the collection of scopes.
*
@@ -14,6 +14,8 @@

package com.google.api.client.auth.oauth2;

import com.google.api.client.util.Key;

import java.util.Collection;
import java.util.Collections;

@@ -52,6 +54,20 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro
*/
public class AuthorizationCodeRequestUrl extends AuthorizationRequestUrl {

/**
* The PKCE <a href="https://tools.ietf.org/html/rfc7636#section-4.3">Code Challenge</a>.
* @since 1.31
*/
@Key("code_challenge")
String codeChallenge;

/**
* The PKCE <a href="https://tools.ietf.org/html/rfc7636#section-4.3">Code Challenge Method</a>.
* @since 1.31
*/
@Key("code_challenge_method")
String codeChallengeMethod;

/**
* @param authorizationServerEncodedUrl authorization server encoded URL
* @param clientId client identifier
@@ -60,6 +76,44 @@ public AuthorizationCodeRequestUrl(String authorizationServerEncodedUrl, String
super(authorizationServerEncodedUrl, clientId, Collections.singleton("code"));
}

/**
* Get the code challenge (<a href="https://tools.ietf.org/html/rfc7636#section-4.3">details</a>).
*
* @since 1.31
*/
public String getCodeChallenge() {
return codeChallenge;
}

/**
* Get the code challenge method (<a href="https://tools.ietf.org/html/rfc7636#section-4.3">details</a>).
*
* @since 1.31
*/
public String getCodeChallengeMethod() {
return codeChallengeMethod;
}

/**
* Set the code challenge (<a href="https://tools.ietf.org/html/rfc7636#section-4.3">details</a>).
* @param codeChallenge the code challenge.
*
* @since 1.31
*/
public void setCodeChallenge(String codeChallenge) {
this.codeChallenge = codeChallenge;
}

/**
* Set the code challenge method (<a href="https://tools.ietf.org/html/rfc7636#section-4.3">details</a>).
* @param codeChallengeMethod the code challenge method.
*
* @since 1.31
*/
public void setCodeChallengeMethod(String codeChallengeMethod) {
this.codeChallengeMethod = codeChallengeMethod;
}

@Override
public AuthorizationCodeRequestUrl setResponseTypes(Collection<String> responseTypes) {
return (AuthorizationCodeRequestUrl) super.setResponseTypes(responseTypes);
@@ -23,6 +23,8 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
* Tests {@link AuthorizationCodeFlow}.
@@ -123,4 +125,24 @@ public void subsetTestNewAuthorizationUrl(Collection<String> scopes) {
assertEquals(Joiner.on(' ').join(scopes), url.getScopes());
}
}

public void testPKCE() {
AuthorizationCodeFlow flow =
new AuthorizationCodeFlow.Builder(BearerToken.queryParameterAccessMethod(),
new AccessTokenTransport(),
new JacksonFactory(),
TOKEN_SERVER_URL,
new BasicAuthentication(CLIENT_ID, CLIENT_SECRET),
CLIENT_ID,
"https://example.com")
.enablePKCE()
.build();

AuthorizationCodeRequestUrl url = flow.newAuthorizationUrl();
assertNotNull(url.getCodeChallenge());
assertNotNull(url.getCodeChallengeMethod());
Set<String> methods = new HashSet<>(Arrays.asList("plain", "s256"));
assertTrue(methods.contains(url.getCodeChallengeMethod().toLowerCase()));
assertTrue(url.getCodeChallenge().length() > 0);
}
}
@@ -65,6 +65,7 @@
<module>google-oauth-client-java6</module>
<module>google-oauth-client-jetty</module>
<module>samples/dailymotion-cmdline-sample</module>
<module>samples/keycloak-pkce-cmdline-sample</module>

<!-- For deployment reasons, a deployable artifact must be the last one. -->
<module>google-oauth-client-assembly</module>
@@ -6,7 +6,7 @@

## Command-Line Instructions

**Prerequisites:** install [Java 6 or higher][install-java], [git][install-git], and
**Prerequisites:** install [Java 7 or higher][install-java], [git][install-git], and
[Maven][install-maven]. You may need to set your `JAVA_HOME`.

1. Check out the sample code:
@@ -0,0 +1,42 @@
# Instructions for the Keycloak OAuth2 with PKCE Command-Line Sample

## Browse Online

[Browse Source][browse-source], or main file [PKCESample.java][main-source].

## Command-Line Instructions

**Prerequisites:** install [Java 7 or higher][install-java], [git][install-git], and
[Maven][install-maven]. You may need to set your `JAVA_HOME`.
You'll also need [Docker][install-docker].

1. Check out the sample code:

```bash
git clone https://github.com/google/google-oauth-java-client.git
cd google-oauth-java-client
```

2. Run keycloak in a docker container:

```
docker run -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:10.0.1
```

3. Run the sample:

```bash
mvn install
mvn exec:java -pl samples/keycloak-pkce-cmdline-sample
```

This will open up the Keycloak login page where you can log in with the username/password specified
when running the Keycloak docker container above (`admin / admin`). Once you log in, the application
will print out a message that it successfully obtained an access token.

[browse-source]: https://github.com/google/google-oauth-java-client/tree/dev/samples/keycloak-pkce-cmdline-sample
[main-source]: https://github.com/google/google-oauth-java-client/blob/dev/samples/keycloak-pkce-cmdline-sample/src/main/java/com/google/api/services/samples/keycloak/cmdline/PKCESample.java
[install-java]: https://java.com/
[install-git]: https://git-scm.com
[install-maven]: https://maven.apache.org
[install-docker]: https://docs.docker.com/get-docker/

0 comments on commit 13433cd

Please sign in to comment.