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: add PKCE support to AuthorizationCodeFlow #470

Merged
merged 19 commits into from Jun 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
61a616e
Initial test code for a PKCE enabled Authorization Code Flow
StFS May 26, 2020
4bf2925
WIP: work on README.md
StFS May 27, 2020
778797b
Script to initialize keycloak by adding client via REST API.
StFS May 27, 2020
73fa760
Improve keycloak init script and some code cleanup. Still WIP.
StFS May 27, 2020
b0f0a9f
Merge branch 'pkce-support' of github.com:StFS/google-oauth-java-clie…
StFS May 27, 2020
8b5e316
WIP: work on README.md
StFS May 28, 2020
3bb51aa
Working PKCE AuthorizationCodeFlow. Some cleanup of test classes.
StFS May 28, 2020
b582b87
Add scopes back to the AuthorizationCodeRequestUrl creation.
StFS May 28, 2020
7238b21
Simplify code by moving PKCE entirely into the AuthorizationCodeFlow …
StFS May 29, 2020
c5def75
Remove wildcard imports as that seems to be the way to do things here.
StFS May 29, 2020
b089e33
Add @since annotation in JavaDoc to the PKCE parameters of the autori…
StFS May 29, 2020
066cadc
Add PKCE unit test, documentation and minor cleanup of dependencies f…
StFS May 29, 2020
7272beb
Add PKCE unit test, documentation and minor cleanup of dependencies f…
StFS May 29, 2020
bdb43ac
Merge branch 'pkce-support' of github.com:StFS/google-oauth-java-clie…
StFS May 29, 2020
f2efde6
Annotate PKCE with Beta annotation.
StFS May 29, 2020
1f0e9b8
Responding to code review comments
StFS Jun 8, 2020
d3e7b47
Responding to more PR comments
StFS Jun 8, 2020
1ee31a0
Improve Keycloak PKCE sample documentation
StFS Jun 8, 2020
0196cfa
Add license header with copyright to new files. Improve documentation.
StFS Jun 19, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -85,6 +93,9 @@ public class AuthorizationCodeFlow {
/** 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
Expand Down Expand Up @@ -159,6 +170,7 @@ protected AuthorizationCodeFlow(Builder builder) {
clock = Preconditions.checkNotNull(builder.clock);
credentialCreatedListener = builder.credentialCreatedListener;
refreshListeners = Collections.unmodifiableCollection(builder.refreshListeners);
pkce = builder.pkce;
}

/**
Expand All @@ -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;
}

/**
Expand All @@ -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());
StFS marked this conversation as resolved.
Show resolved Hide resolved
}
}
};

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

/**
Expand Down Expand Up @@ -412,6 +440,61 @@ public interface CredentialCreatedListener {
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.
*
Expand Down Expand Up @@ -448,6 +531,8 @@ public static class Builder {
/** Authorization server encoded URL. */
String authorizationServerEncodedUrl;

PKCE pkce;

/** Credential persistence store or {@code null} for none. */
@Deprecated
@Beta
Expand Down Expand Up @@ -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.
*
Expand Down
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
Expand Up @@ -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}.
Expand Down Expand Up @@ -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);
}
}
1 change: 1 addition & 0 deletions pom.xml
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion samples/dailymotion-cmdline-sample/README.md
Expand Up @@ -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:
Expand Down
42 changes: 42 additions & 0 deletions samples/keycloak-pkce-cmdline-sample/README.md
@@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure what keycloak is, or if we can include this part in a google project

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ahh... that had not occurred to me. Keycloak is a JBoss (Red Hat) project, it's an "open source identity and access management solution" (https://www.keycloak.org/) and it's released under the Apache 2.0 license.
So, as such there shouldn't be a problem using it, especially since it's only being used for the sample demonstration and not as an actual component. But maybe there is some other more political/legal reason for it not being possible. I could remove the sample but I would like to keep it as it's pretty good and clear for the purpose of testing and demonstration.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not a license issue, but we would like the docs for this product to be self-contained.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

gotcha. However, the daily motion example isn't exactly self-contained is is? It actually depends on the user registering an account with dailymotion.com.

But if using keycloak is not acceptable, can you suggest an alternative or should I remove the sample completely?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@elharo any thoughts on this?


```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/