CVE-2020-7692 (Java, published 2020-07-09T14:15Z)
Description: value: 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 v3 base score: 9.1)
Weakness: CWE-863 \u2014 Incorrect Authorization
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.
Repository: https://github.com/googleapis/google-oauth-java-client
Files changed:
google-oauth-client/src/main/java/com/google/api/client/auth/oauth2/AuthorizationCodeFlow.java
google-oauth-client/src/main/java/com/google/api/client/auth/oauth2/AuthorizationCodeRequestUrl.java
google-oauth-client/src/test/java/com/google/api/client/auth/oauth2/AuthorizationCodeFlowTest.java
pom.xml
samples/dailymotion-cmdline-sample/README.md
samples/keycloak-pkce-cmdline-sample/README.md
samples/keycloak-pkce-cmdline-sample/pom.xml
samples/keycloak-pkce-cmdline-sample/scripts/initialize-keycloak.sh
samples/keycloak-pkce-cmdline-sample/src/main/java/com/google/api/services/samples/keycloak/cmdline/PKCESample.java
Commit message: 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.

Vulnerable code:
    return new AuthorizationCodeRequestUrl(authorizationServerEncodedUrl, clientId).setScopes(
        scopes);
        clientAuthentication).setRequestInitializer(requestInitializer).setScopes(scopes);
**Prerequisites:** install [Java 6 or higher][install-java], [git][install-git], and

Fixed code:
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.UrlEncodedContent;
import com.google.api.client.util.Base64;
import com.google.api.client.util.Data;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Map;
  /** The Proof Key for Code Exchange (PKCE) or {@code null} if this flow should not use PKCE. */
  private final PKCE pkce;

    pkce = builder.pkce;
    AuthorizationCodeRequestUrl url = new  AuthorizationCodeRequestUrl(authorizationServerEncodedUrl, clientId);
    url.setScopes(scopes);
    if (pkce != null) {
      url.setCodeChallenge(pkce.getChallenge());
      url.setCodeChallengeMethod(pkce.getChallengeMethod());
    }
    return url;
    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());
        }
      }
    };

        pkceClientAuthenticationWrapper).setRequestInitializer(requestInitializer).setScopes(scopes);
  /**
   * 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;
    }
  }

    PKCE pkce;

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

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

  /**
   * 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;

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

import java.util.HashSet;
import java.util.Set;

  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);
  }
    <module>samples/keycloak-pkce-cmdline-sample</module>
**Prerequisites:** install [Java 7 or higher][install-java], [git][install-git], and
# 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/
<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>
#!/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}" 

 
/*
 * 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);
    }
}