CVE-2020-7692 (published 2020-07-09T14:15Z)

PKCE support is not implemented in accordance with the RFC for OAuth 2.0 for Native Apps. Without the use of PKCE, the authorization code returned by an authorization server is not enough to guarantee that the client that issued the initial authorization request is the one that will be authorized. An attacker is able to obtain the authorization code using a malicious app on the client-side and use it to gain authorization to the protected resource. This affects the package com.google.oauth-client:google-oauth-client before 1.31.0.

Severity: MEDIUM
CVSS v2 base score: 6.4 (AV:N/AC:L/Au:N/C:P/I:P/A:N)
CVSS v3 base score: 9.1, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)

Weakness information:
cwe_id: CWE-863
cwe_name: Incorrect Authorization
description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
extended_description: 
            Assuming a user with a given identity, authorization is the process of determining whether that user can access a given resource, based on the user's privileges and any permissions or other access-control specifications that apply to the resource.
            When access control checks are incorrectly applied, users are able to access data or perform actions that they should not be allowed to perform. This can lead to a wide range of problems, including information exposures, denial of service, and arbitrary code execution.
         
         

Fix information:
repo_url: https://github.com/googleapis/google-oauth-java-client
commit_details: repo_url: https://github.com/googleapis/google-oauth-java-client
author: Stefán Freyr Stefánsson
author_date: 2020-06-29 19:57:20+00:00
msg: 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.
repository: repo_url: https://github.com/googleapis/google-oauth-java-client
repo_name: googleapis/google-oauth-java-client
description: Google OAuth Client Library for Java
repo_language: Java
owner: googleapis
file_changes: filename: AuthorizationCodeFlow.java
change_type: ModificationType.MODIFY
diff: @@ -17,10 +17,14 @@ package com.google.api.client.auth.oauth2;
 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.DataStore;
 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 @@ 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
@@ -159,6 +170,7 @@ public class AuthorizationCodeFlow {
     clock = Preconditions.checkNotNull(builder.clock);
     credentialCreatedListener = builder.credentialCreatedListener;
     refreshListeners = Collections.unmodifiableCollection(builder.refreshListeners);
+    pkce = builder.pkce;
   }
 
   /**
@@ -182,8 +194,13 @@ public class AuthorizationCodeFlow {
    * </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 @@ public class AuthorizationCodeFlow {
    * @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 class AuthorizationCodeFlow {
     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 class AuthorizationCodeFlow {
     /** Authorization server encoded URL. */
     String authorizationServerEncodedUrl;
 
+    PKCE pkce;
+
     /** Credential persistence store or {@code null} for none. */
     @Deprecated
     @Beta
@@ -784,6 +869,16 @@ public class AuthorizationCodeFlow {
       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.
      *

programming_language: Java
filename: AuthorizationCodeRequestUrl.java
change_type: ModificationType.MODIFY
diff: @@ -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 @@ import java.util.Collections;
  */
 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 class AuthorizationCodeRequestUrl extends AuthorizationRequestUrl {
     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);

programming_language: Java
filename: AuthorizationCodeFlowTest.java
change_type: ModificationType.MODIFY
diff: @@ -23,6 +23,8 @@ import java.io.IOException;
 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 class AuthorizationCodeFlowTest extends AuthenticationTestBase {
       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);
+  }
 }

programming_language: Java
filename: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -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>

programming_language: HTML
filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -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:

programming_language: Markdown
filename: README.md
change_type: ModificationType.ADD
diff: @@ -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/
\ No newline at end of file

programming_language: Markdown
filename: pom.xml
change_type: ModificationType.ADD
diff: @@ -0,0 +1,104 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>com.google.oauth-client</groupId>
+    <artifactId>google-oauth-client-parent</artifactId>
+    <version>1.30.7-SNAPSHOT</version><!-- {x-version-update:google-oauth-client:current} -->
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+  <artifactId>keycloak-pkce-cmdline-sample</artifactId>
+  <name>Example for obtaining OAuth2 tokens with PKCE verification using the Authorization Code Flow against Keycloak.</name>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>exec-maven-plugin</artifactId>
+        <version>1.6.0</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>java</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <mainClass>com.google.api.services.samples.keycloak.cmdline.PKCESample</mainClass>
+          <systemProperties>
+            <systemProperty>
+              <key>java.util.logging.config.file</key>
+              <value>logging.properties</value>
+            </systemProperty>
+          </systemProperties>
+        </configuration>
+      </plugin>
+      <plugin>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+        <version>2.6</version>
+        <configuration>
+          <configLocation>../checkstyle.xml</configLocation>
+          <consoleOutput>true</consoleOutput>
+          <failOnViolation>false</failOnViolation>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>check</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>findbugs-maven-plugin</artifactId>
+        <version>3.0.5</version>
+        <configuration>
+          <excludeFilterFile>../../findbugs-exclude.xml</excludeFilterFile>
+          <failOnError>false</failOnError>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>check</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-deploy-plugin</artifactId>
+        <version>2.8.2</version>
+        <configuration>
+          <skip>true</skip>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.sonatype.plugins</groupId>
+        <artifactId>nexus-staging-maven-plugin</artifactId>
+        <version>1.6.8</version>
+        <configuration>
+          <skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo>
+        </configuration>
+      </plugin>
+    </plugins>
+    <finalName>${project.artifactId}-${project.version}</finalName>
+  </build>
+  <dependencies>
+    <dependency>
+      <groupId>com.google.oauth-client</groupId>
+      <artifactId>google-oauth-client</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.oauth-client</groupId>
+      <artifactId>google-oauth-client-jetty</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.http-client</groupId>
+      <artifactId>google-http-client-jackson2</artifactId>
+    </dependency>
+  </dependencies>
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+</project>

programming_language: CoffeeScript
filename: initialize-keycloak.sh
change_type: ModificationType.ADD
diff: @@ -0,0 +1,44 @@
+#!/bin/sh
+# Copyright 2020 Google LLC
+#
+# 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.
+
+
+#Start keycloak server before running this script:
+# docker run -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:10.0.1
+
+# The following script will create a new public client in the running keycloak server
+# in which PKCE is required for obtaining an authorization token via the authorization
+# code flow. Once this script has been run, the PKCESample.java sample application can
+# be run.
+
+KEYCLOAK_BASE_URL="http://localhost:8080/auth"
+KEYCLOAK_REALM="master"
+KEYCLOAK_URL="${KEYCLOAK_BASE_URL}/realms/${KEYCLOAK_REALM}"
+
+KEYCLOAK_CLIENT_ID="admin"
+KEYCLOAK_CLIENT_SECRET="admin"
+
+export TKN=$(curl -s -X POST "${KEYCLOAK_URL}/protocol/openid-connect/token" \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "username=${KEYCLOAK_CLIENT_ID}" \
+ -d "password=${KEYCLOAK_CLIENT_SECRET}" \
+ -d 'grant_type=password' \
+ -d 'client_id=admin-cli' | jq -r '.access_token')
+
+curl -s -X POST "${KEYCLOAK_URL}/clients-registrations/default" \
+ -d '{ "clientId": "pkce-test-client", "publicClient": true, "redirectUris": ["http://127.0.0.1*"], "attributes": {"pkce.code.challenge.method": "S256"} }' \
+ -H "Content-Type:application/json" \
+ -H "Authorization: bearer ${TKN}" 
+
+ 
\ No newline at end of file

programming_language: Shell
filename: PKCESample.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2020 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.
+ */
+
+package com.google.api.services.samples.keycloak.cmdline;
+
+import com.google.api.client.auth.oauth2.AuthorizationCodeFlow;
+import com.google.api.client.auth.oauth2.BearerToken;
+import com.google.api.client.auth.oauth2.ClientParametersAuthentication;
+import com.google.api.client.auth.oauth2.Credential;
+import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
+import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.client.util.store.DataStoreFactory;
+import com.google.api.client.util.store.MemoryDataStoreFactory;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * A sample application that demonstrates how the Google OAuth2 library can be used to authenticate
+ * against a locally running Keycloak server with a registered public client where using
+ * <a href="https://tools.ietf.org/html/rfc7636">PKCE</a> is required.
+ *
+ * Please note that before running this sample application, a local Keycloak server must be running
+ * and a PKCE enabled client must have been defined. Please see
+ * <code>samples/keycloak-pkce-cmdline-sample/scripts/initialize-keycloak.sh</code> for further
+ * information.
+ *
+ * @author Stefan Freyr Stefansson
+ */
+public class PKCESample {
+    /**
+     * Global instance of the {@link DataStoreFactory}. The best practice is to make it a single
+     * globally shared instance across your application.
+     */
+    private static DataStoreFactory DATA_STORE_FACTORY;
+
+    /** OAuth 2 scope. */
+    private static final String SCOPE = "email";
+
+    /** Global instance of the HTTP transport. */
+    private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
+
+    /** Global instance of the JSON factory. */
+    static final JsonFactory JSON_FACTORY = new JacksonFactory();
+
+    private static final String TOKEN_SERVER_URL = "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/token";
+    private static final String AUTHORIZATION_SERVER_URL = "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/auth";
+
+    /** Authorizes the installed application to access user's protected data. */
+    private static Credential authorize() throws Exception {
+        // set up authorization code flow
+        String clientId = "pkce-test-client";
+        AuthorizationCodeFlow flow = new AuthorizationCodeFlow.Builder(
+                BearerToken.authorizationHeaderAccessMethod(),
+                HTTP_TRANSPORT,
+                JSON_FACTORY,
+                new GenericUrl(TOKEN_SERVER_URL),
+                new ClientParametersAuthentication(clientId, null),
+                clientId,
+                AUTHORIZATION_SERVER_URL)
+                .setScopes(Arrays.asList(SCOPE))
+                .enablePKCE()
+                .setDataStoreFactory(DATA_STORE_FACTORY).build();
+        // authorize
+        LocalServerReceiver receiver = new LocalServerReceiver.Builder().setHost("127.0.0.1").build();
+        return new AuthorizationCodeInstalledApp(flow, receiver).authorize("user");
+    }
+
+    public static void main(String[] args) {
+        try {
+            DATA_STORE_FACTORY = new MemoryDataStoreFactory();
+            final Credential credential = authorize();
+            System.out.println("Successfully obtained credential from Keycloak running on localhost.");
+            final String accessToken = credential.getAccessToken();
+            System.out.println("Retrieved an access token of length " + accessToken.length());
+            return;
+        } catch (IOException e) {
+            System.err.println(e.getMessage());
+        } catch (Throwable t) {
+            t.printStackTrace();
+        }
+        System.exit(1);
+    }
+}

programming_language: Java