From bad58c35bbd859d0a4483254507140b3c29e3222 Mon Sep 17 00:00:00 2001
From: "release-please[bot]"
<55107282+release-please[bot]@users.noreply.github.com>
Date: Tue, 17 May 2022 20:00:17 +0000
Subject: [PATCH 01/10] chore(main): release 1.7.1-SNAPSHOT (#922)
:robot: I have created a release *beep* *boop*
---
### Updating meta-information for bleeding-edge SNAPSHOT release.
---
This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
---
appengine/pom.xml | 2 +-
bom/pom.xml | 2 +-
credentials/pom.xml | 2 +-
oauth2_http/pom.xml | 2 +-
pom.xml | 2 +-
versions.txt | 12 ++++++------
6 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/appengine/pom.xml b/appengine/pom.xml
index b23af89fc..f40cfdaa3 100644
--- a/appengine/pom.xml
+++ b/appengine/pom.xml
@@ -5,7 +5,7 @@
com.google.authgoogle-auth-library-parent
- 1.7.0
+ 1.7.1-SNAPSHOT../pom.xml
diff --git a/bom/pom.xml b/bom/pom.xml
index f9f23bfcb..951ac42ee 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -3,7 +3,7 @@
4.0.0com.google.authgoogle-auth-library-bom
- 1.7.0
+ 1.7.1-SNAPSHOTpomGoogle Auth Library for Java BOM
diff --git a/credentials/pom.xml b/credentials/pom.xml
index 5fed3789b..82bc206a8 100644
--- a/credentials/pom.xml
+++ b/credentials/pom.xml
@@ -4,7 +4,7 @@
com.google.authgoogle-auth-library-parent
- 1.7.0
+ 1.7.1-SNAPSHOT../pom.xml
diff --git a/oauth2_http/pom.xml b/oauth2_http/pom.xml
index 156c3c5d2..656b86a5f 100644
--- a/oauth2_http/pom.xml
+++ b/oauth2_http/pom.xml
@@ -5,7 +5,7 @@
com.google.authgoogle-auth-library-parent
- 1.7.0
+ 1.7.1-SNAPSHOT../pom.xml
diff --git a/pom.xml b/pom.xml
index 190f87cb4..bd67e1e6c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0com.google.authgoogle-auth-library-parent
- 1.7.0
+ 1.7.1-SNAPSHOTpomGoogle Auth Library for JavaClient libraries providing authentication and
diff --git a/versions.txt b/versions.txt
index 3b95a5022..05ee34faa 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,9 +1,9 @@
# Format:
# module:released-version:current-version
-google-auth-library:1.7.0:1.7.0
-google-auth-library-bom:1.7.0:1.7.0
-google-auth-library-parent:1.7.0:1.7.0
-google-auth-library-appengine:1.7.0:1.7.0
-google-auth-library-credentials:1.7.0:1.7.0
-google-auth-library-oauth2-http:1.7.0:1.7.0
+google-auth-library:1.7.0:1.7.1-SNAPSHOT
+google-auth-library-bom:1.7.0:1.7.1-SNAPSHOT
+google-auth-library-parent:1.7.0:1.7.1-SNAPSHOT
+google-auth-library-appengine:1.7.0:1.7.1-SNAPSHOT
+google-auth-library-credentials:1.7.0:1.7.1-SNAPSHOT
+google-auth-library-oauth2-http:1.7.0:1.7.1-SNAPSHOT
From bbb51ce7a9265cb991739cd90e1ccf65675d05dc Mon Sep 17 00:00:00 2001
From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com>
Date: Thu, 19 May 2022 20:54:15 +0000
Subject: [PATCH 02/10] feat: add build scripts for native image testing in
Java 17 (#1440) (#923)
Source-Link: https://github.com/googleapis/synthtool/commit/505ce5a7edb58bf6d9d4de10b4bb4e81000ae324
Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-java:latest@sha256:2567a120ce90fadb6201999b87d649d9f67459de28815ad239bce9ebfaa18a74
---
.github/.OwlBot.lock.yaml | 4 +--
.kokoro/build.sh | 5 ++++
.kokoro/presubmit/graalvm-native-17.cfg | 33 +++++++++++++++++++++++++
3 files changed, 40 insertions(+), 2 deletions(-)
create mode 100644 .kokoro/presubmit/graalvm-native-17.cfg
diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml
index f60d77493..a79f06271 100644
--- a/.github/.OwlBot.lock.yaml
+++ b/.github/.OwlBot.lock.yaml
@@ -13,5 +13,5 @@
# limitations under the License.
docker:
image: gcr.io/cloud-devrel-public-resources/owlbot-java:latest
- digest: sha256:fc52b202aa298a50a12c64efd04fea3884d867947effe2fa85382a246c09e813
-# created: 2022-04-06T16:30:03.627422514Z
+ digest: sha256:2567a120ce90fadb6201999b87d649d9f67459de28815ad239bce9ebfaa18a74
+# created: 2022-05-19T15:12:45.278246753Z
diff --git a/.kokoro/build.sh b/.kokoro/build.sh
index 317bf8686..c483ec7bf 100755
--- a/.kokoro/build.sh
+++ b/.kokoro/build.sh
@@ -74,6 +74,11 @@ graalvm)
mvn -B ${INTEGRATION_TEST_ARGS} -ntp -Pnative -Penable-integration-tests test
RETURN_CODE=$?
;;
+graalvm17)
+ # Run Unit and Integration Tests with Native Image
+ mvn -B ${INTEGRATION_TEST_ARGS} -ntp -Pnative -Penable-integration-tests test
+ RETURN_CODE=$?
+ ;;
samples)
SAMPLES_DIR=samples
# only run ITs in snapshot/ on presubmit PRs. run ITs in all 3 samples/ subdirectories otherwise.
diff --git a/.kokoro/presubmit/graalvm-native-17.cfg b/.kokoro/presubmit/graalvm-native-17.cfg
new file mode 100644
index 000000000..a3f7fb9d4
--- /dev/null
+++ b/.kokoro/presubmit/graalvm-native-17.cfg
@@ -0,0 +1,33 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+# Configure the docker image for kokoro-trampoline.
+env_vars: {
+ key: "TRAMPOLINE_IMAGE"
+ value: "gcr.io/cloud-devrel-kokoro-resources/graalvm17"
+}
+
+env_vars: {
+ key: "JOB_TYPE"
+ value: "graalvm17"
+}
+
+# TODO: remove this after we've migrated all tests and scripts
+env_vars: {
+ key: "GCLOUD_PROJECT"
+ value: "gcloud-devel"
+}
+
+env_vars: {
+ key: "GOOGLE_CLOUD_PROJECT"
+ value: "gcloud-devel"
+}
+
+env_vars: {
+ key: "GOOGLE_APPLICATION_CREDENTIALS"
+ value: "secret_manager/java-it-service-account"
+}
+
+env_vars: {
+ key: "SECRET_MANAGER_KEYS"
+ value: "java-it-service-account"
+}
\ No newline at end of file
From c357d00432bf264af5d31fa701230adbb73c387f Mon Sep 17 00:00:00 2001
From: WhiteSource Renovate
Date: Fri, 10 Jun 2022 18:35:58 +0200
Subject: [PATCH 03/10] chore(deps): update dependency
org.apache.maven.plugins:maven-failsafe-plugin to v3.0.0-m7 (#925)
---
oauth2_http/pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/oauth2_http/pom.xml b/oauth2_http/pom.xml
index 656b86a5f..327128666 100644
--- a/oauth2_http/pom.xml
+++ b/oauth2_http/pom.xml
@@ -61,7 +61,7 @@
org.apache.maven.pluginsmaven-failsafe-plugin
- 3.0.0-M6
+ 3.0.0-M71200sponge_log
From ca1ada105c5bd7fb2e0d33d5e261b42d8395dd99 Mon Sep 17 00:00:00 2001
From: WhiteSource Renovate
Date: Fri, 10 Jun 2022 18:38:16 +0200
Subject: [PATCH 04/10] chore(deps): update dependency
com.google.http-client:google-http-client-bom to v1.42.0 (#928)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[](https://renovatebot.com)
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.http-client:google-http-client-bom](https://togithub.com/googleapis/google-http-java-client) | `1.41.8` -> `1.42.0` | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) |
---
### Release Notes
googleapis/google-http-java-client
### [`v1.42.0`](https://togithub.com/googleapis/google-http-java-client/blob/HEAD/CHANGELOG.md#1420-httpsgithubcomgoogleapisgoogle-http-java-clientcomparev1417v1420-2022-06-09)
[Compare Source](https://togithub.com/googleapis/google-http-java-client/compare/v1.41.8...v1.42.0)
##### Features
- add build scripts for native image testing in Java 17 ([#1440](https://togithub.com/googleapis/google-http-java-client/issues/1440)) ([#1666](https://togithub.com/googleapis/google-http-java-client/issues/1666)) ([05d4019](https://togithub.com/googleapis/google-http-java-client/commit/05d40193d40097e5a793154a0951f2577fc80f04))
- next release from main branch is 1.42.0 ([#1633](https://togithub.com/googleapis/google-http-java-client/issues/1633)) ([9acb1ab](https://togithub.com/googleapis/google-http-java-client/commit/9acb1abaa97392174dd35c5e0e68346f8f653b5b))
##### Dependencies
- update dependency com.fasterxml.jackson.core:jackson-core to v2.13.3 ([#1665](https://togithub.com/googleapis/google-http-java-client/issues/1665)) ([e4f0959](https://togithub.com/googleapis/google-http-java-client/commit/e4f095997050047d9a6cc20f034f5ef744aefd44))
- update dependency com.google.errorprone:error_prone_annotations to v2.13.0 ([#1630](https://togithub.com/googleapis/google-http-java-client/issues/1630)) ([bf777b3](https://togithub.com/googleapis/google-http-java-client/commit/bf777b364c8aafec09c486dc965587eae90549df))
- update dependency com.google.errorprone:error_prone_annotations to v2.13.1 ([#1632](https://togithub.com/googleapis/google-http-java-client/issues/1632)) ([9e46cd8](https://togithub.com/googleapis/google-http-java-client/commit/9e46cd85ed1c14161f6473f926802bf281edc4ad))
- update dependency com.google.errorprone:error_prone_annotations to v2.14.0 ([#1667](https://togithub.com/googleapis/google-http-java-client/issues/1667)) ([3516e18](https://togithub.com/googleapis/google-http-java-client/commit/3516e185b811d1935eebce31ba65da4813f7e998))
- update dependency com.google.protobuf:protobuf-java to v3.20.1 ([#1639](https://togithub.com/googleapis/google-http-java-client/issues/1639)) ([90a99e2](https://togithub.com/googleapis/google-http-java-client/commit/90a99e27b053f5dc6078d6d8cd9bfe150237e2b4))
- update dependency com.google.protobuf:protobuf-java to v3.21.0 ([#1668](https://togithub.com/googleapis/google-http-java-client/issues/1668)) ([babbe94](https://togithub.com/googleapis/google-http-java-client/commit/babbe94104710db7b4b428756d7db6c069674ff1))
- update dependency com.google.protobuf:protobuf-java to v3.21.1 ([#1669](https://togithub.com/googleapis/google-http-java-client/issues/1669)) ([30ec091](https://togithub.com/googleapis/google-http-java-client/commit/30ec091faea7b5ec9f130cb3fdee396e9923a4b9))
- update dependency org.apache.felix:maven-bundle-plugin to v5.1.6 ([#1643](https://togithub.com/googleapis/google-http-java-client/issues/1643)) ([8547f5f](https://togithub.com/googleapis/google-http-java-client/commit/8547f5fff9b27782162b0b6f0db7445c02918a45))
- update project.appengine.version to v2.0.5 ([#1662](https://togithub.com/googleapis/google-http-java-client/issues/1662)) ([2c82c0d](https://togithub.com/googleapis/google-http-java-client/commit/2c82c0d4da1162cbc6950cdd6b2f4472b884db13))
- update project.opencensus.version to v0.31.1 ([#1644](https://togithub.com/googleapis/google-http-java-client/issues/1644)) ([3c65a07](https://togithub.com/googleapis/google-http-java-client/commit/3c65a07c14d2bf7aa6cce25122df85670955d459))
##### [1.41.7](https://togithub.com/googleapis/google-http-java-client/compare/v1.41.6...v1.41.7) (2022-04-11)
##### Dependencies
- revert dependency com.google.protobuf:protobuf-java to v3.19.4 ([#1626](https://togithub.com/googleapis/google-http-java-client/issues/1626)) ([076433f](https://togithub.com/googleapis/google-http-java-client/commit/076433f3c233a757f31d5fa39bb6cedbb43b8361))
##### [1.41.6](https://togithub.com/googleapis/google-http-java-client/compare/v1.41.5...v1.41.6) (2022-04-06)
##### Bug Fixes
- `Content-Encoding: gzip` along with `Transfer-Encoding: chunked` sometimes terminates early ([#1608](https://togithub.com/googleapis/google-http-java-client/issues/1608)) ([941da8b](https://togithub.com/googleapis/google-http-java-client/commit/941da8badf64068d11a53ac57a4ba35b2ad13490))
##### Dependencies
- update dependency com.google.errorprone:error_prone_annotations to v2.12.1 ([#1622](https://togithub.com/googleapis/google-http-java-client/issues/1622)) ([4e1101d](https://togithub.com/googleapis/google-http-java-client/commit/4e1101d7674cb5715b88a00750cdd5286a9ae077))
- update dependency com.google.protobuf:protobuf-java to v3.20.0 ([#1621](https://togithub.com/googleapis/google-http-java-client/issues/1621)) ([640dc40](https://togithub.com/googleapis/google-http-java-client/commit/640dc4080249b65e5cabb7e1ae6cd9cd5b11bd8e))
##### [1.41.5](https://togithub.com/googleapis/google-http-java-client/compare/v1.41.4...v1.41.5) (2022-03-21)
##### Documentation
- **deps:** libraries-bom 24.4.0 release ([#1596](https://togithub.com/googleapis/google-http-java-client/issues/1596)) ([327fe12](https://togithub.com/googleapis/google-http-java-client/commit/327fe12a122ebb4022a2da55694217233a2badaf))
##### Dependencies
- update actions/checkout action to v3 ([#1593](https://togithub.com/googleapis/google-http-java-client/issues/1593)) ([92002c0](https://togithub.com/googleapis/google-http-java-client/commit/92002c07d60b738657383e2484f56abc1cde6920))
- update dependency com.fasterxml.jackson.core:jackson-core to v2.13.2 ([#1598](https://togithub.com/googleapis/google-http-java-client/issues/1598)) ([41ac833](https://togithub.com/googleapis/google-http-java-client/commit/41ac833249e18cbbd304f825b12202e51bebec85))
- update project.appengine.version to v2 (major) ([#1597](https://togithub.com/googleapis/google-http-java-client/issues/1597)) ([c06cf95](https://togithub.com/googleapis/google-http-java-client/commit/c06cf95f9b1be77e2229c3b2f78ece0789eaec15))
##### [1.41.4](https://togithub.com/googleapis/google-http-java-client/compare/v1.41.3...v1.41.4) (2022-02-11)
##### Dependencies
- update dependency com.google.code.gson:gson to v2.9.0 ([#1582](https://togithub.com/googleapis/google-http-java-client/issues/1582)) ([8772778](https://togithub.com/googleapis/google-http-java-client/commit/877277821dad65545518b06123e6e7b9801147a1))
##### [1.41.3](https://togithub.com/googleapis/google-http-java-client/compare/v1.41.2...v1.41.3) (2022-02-09)
##### Dependencies
- update dependency com.google.protobuf:protobuf-java to v3.19.4 ([#1568](https://togithub.com/googleapis/google-http-java-client/issues/1568)) ([416e5d7](https://togithub.com/googleapis/google-http-java-client/commit/416e5d7146ad145e3d5140110144b5119c6126df))
- update dependency com.puppycrawl.tools:checkstyle to v9.3 ([#1569](https://togithub.com/googleapis/google-http-java-client/issues/1569)) ([9c7ade8](https://togithub.com/googleapis/google-http-java-client/commit/9c7ade85eceb2dc348e1f9aa0637d0509d634160))
- update project.opencensus.version to v0.31.0 ([#1563](https://togithub.com/googleapis/google-http-java-client/issues/1563)) ([0f9d2b7](https://togithub.com/googleapis/google-http-java-client/commit/0f9d2b77ae23ea143b5b8caaa21af6548ca92345))
##### [1.41.2](https://togithub.com/googleapis/google-http-java-client/compare/v1.41.1...v1.41.2) (2022-01-27)
##### Dependencies
- **java:** update actions/github-script action to v5 ([#1339](https://togithub.com/googleapis/google-http-java-client/issues/1339)) ([#1561](https://togithub.com/googleapis/google-http-java-client/issues/1561)) ([c5dbec1](https://togithub.com/googleapis/google-http-java-client/commit/c5dbec1bbfb5f26f952cb8d80f607327594ab7a8))
- update dependency com.google.errorprone:error_prone_annotations to v2.11.0 ([#1560](https://togithub.com/googleapis/google-http-java-client/issues/1560)) ([d9609b0](https://togithub.com/googleapis/google-http-java-client/commit/d9609b00089952d816deffa178640bfcae1f2c3a))
##### [1.41.1](https://togithub.com/googleapis/google-http-java-client/compare/v1.41.0...v1.41.1) (2022-01-21)
##### Dependencies
- update dependency com.fasterxml.jackson.core:jackson-core to v2.13.1 ([#1527](https://togithub.com/googleapis/google-http-java-client/issues/1527)) ([7750398](https://togithub.com/googleapis/google-http-java-client/commit/7750398d6f4d6e447bfe078092f5cb146f747e50))
- update dependency com.google.protobuf:protobuf-java to v3.19.3 ([#1549](https://togithub.com/googleapis/google-http-java-client/issues/1549)) ([50c0765](https://togithub.com/googleapis/google-http-java-client/commit/50c0765f1eadbf7aef2dccf5f78ab62e2533c6f6))
- update dependency com.puppycrawl.tools:checkstyle to v9.2.1 ([#1532](https://togithub.com/googleapis/google-http-java-client/issues/1532)) ([e13eebd](https://togithub.com/googleapis/google-http-java-client/commit/e13eebd288afbde3aa7bdc0229c2d0db90ebbd4c))
- update dependency kr.motd.maven:os-maven-plugin to v1.7.0 ([#1547](https://togithub.com/googleapis/google-http-java-client/issues/1547)) ([8df0dbe](https://togithub.com/googleapis/google-http-java-client/commit/8df0dbe53521e918985e8f4882392cd2e0a0a1c3))
- update dependency org.apache.felix:maven-bundle-plugin to v5 ([#1548](https://togithub.com/googleapis/google-http-java-client/issues/1548)) ([ac10b6c](https://togithub.com/googleapis/google-http-java-client/commit/ac10b6c9fbe4986b8bf130d9f83ae77e84d74e5f))
- update project.appengine.version to v1.9.94 ([#1557](https://togithub.com/googleapis/google-http-java-client/issues/1557)) ([05c78f4](https://togithub.com/googleapis/google-http-java-client/commit/05c78f4bee92cc501aa084ad970ed6ac9c0e0444))
- update project.opencensus.version to v0.30.0 ([#1526](https://togithub.com/googleapis/google-http-java-client/issues/1526)) ([318e54a](https://togithub.com/googleapis/google-http-java-client/commit/318e54ae9be6bfeb4f5af0af0cb954031d95d1f9))
---
### Configuration
π **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).
π¦ **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
β» **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
π **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] If you want to rebase/retry this PR, click this checkbox.
---
This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-java).
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index bd67e1e6c..c454f971f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -59,7 +59,7 @@
UTF-8
- 1.41.8
+ 1.42.05.8.231.0.1-android2.0.5
From f2697569d60f740d4f0b9f888d0c05b464291786 Mon Sep 17 00:00:00 2001
From: WhiteSource Renovate
Date: Fri, 10 Jun 2022 18:42:17 +0200
Subject: [PATCH 05/10] chore(deps): update dependency
org.apache.maven.plugins:maven-surefire-plugin to v3.0.0-m7 (#926)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[](https://renovatebot.com)
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [org.apache.maven.plugins:maven-surefire-plugin](https://maven.apache.org/surefire/) | `3.0.0-M6` -> `3.0.0-M7` | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) |
---
### Configuration
π **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).
π¦ **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
β» **Rebasing**: Renovate will not automatically rebase this PR, because other commits have been found.
π **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] If you want to rebase/retry this PR, click this checkbox. β **Warning**: custom changes will be lost.
---
This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-java).
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index c454f971f..f68dfb56c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -194,7 +194,7 @@
org.apache.maven.pluginsmaven-surefire-plugin
- 3.0.0-M6
+ 3.0.0-M7sponge_log
From c3e8d169704943735c6b3df7bd0187f04fdd9aa5 Mon Sep 17 00:00:00 2001
From: Leo <39062083+lsirac@users.noreply.github.com>
Date: Fri, 24 Jun 2022 15:36:01 -0700
Subject: [PATCH 06/10] feat: Adds Pluggable Auth support (WIF) (#908)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: Adds Pluggable Auth support to ADC (#895)
* chore(deps): update dependency com.google.http-client:google-http-client-bom to v1.41.5 (#896)
[](https://renovatebot.com)
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.http-client:google-http-client-bom](https://togithub.com/googleapis/google-http-java-client) | `1.41.4` -> `1.41.5` | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) |
---
### Release Notes
googleapis/google-http-java-client
### [`v1.41.5`](https://togithub.com/googleapis/google-http-java-client/blob/HEAD/CHANGELOG.md#1415-httpsgithubcomgoogleapisgoogle-http-java-clientcomparev1414v1415-2022-03-21)
[Compare Source](https://togithub.com/googleapis/google-http-java-client/compare/v1.41.4...v1.41.5)
---
### Configuration
π **Schedule**: At any time (no schedule defined).
π¦ **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
β» **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
π **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] If you want to rebase/retry this PR, click this checkbox.
---
This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/google-auth-library-java).
* feat: Add ability to provide PrivateKey as Pkcs8 encoded string #883 (#889)
* feat: Add ability to provide PrivateKey as Pkcs8 encoded string #883
This change adds a new method `setPrivateKeyString` in `ServiceAccountCredentials.Builder` to accept Pkcs8 encoded string representation of private keys.
Co-authored-by: Timur Sadykov
* chore: fix downstream check (#898)
* fix: update branding in ExternalAccountCredentials (#893)
These changes align the Javadoc comments with the branding that Google uses externally:
+ STS -> Security Token Service
+ GCP -> Google Cloud
+ Remove references to a Google-internal token type
Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:
- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/google-auth-library-java/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
- [ ] Ensure the tests and linter pass: Tests are failing, but I don't think that was caused by the changes in this PR
- [ ] Code coverage does not decrease (if any source code was changed): n/a
- [ ] Appropriate docs were updated (if necessary): n/a
* feat: Adds the ExecutableHandler interface for Pluggable Auth
* feat: Adds a Pluggable Auth specific exception
* feat: Adds new PluggableAuthCredentials class that plug into ADC
* feat: Adds unit tests for PluggableAuthCredentials and ExternalAccountCredentials
* Add units tests for GoogleCredentials
* fix: update javadoc/comments
* fix: A concrete ExecutableOptions implementation is not needed
* review: javadoc changes + constants
Co-authored-by: WhiteSource Renovate
Co-authored-by: Navina Ramesh
Co-authored-by: Timur Sadykov
Co-authored-by: Neenu Shaji
Co-authored-by: Jeff Williams
* feat: finalizes PluggableAuth implementation (#906)
* Adds ExecutableResponse class
* Adds unit tests for ExecutableResponse
* Adds 3rd party executable handler
* Adds unit tests for PluggableAuthHandler
* Fix build issues
* don't fail on javadoc errors
* feat: Improve Pluggable Auth error handling (#912)
* feat: improves pluggable auth error handling
* cleanup
* fix: consume input stream immediately for Pluggable Auth (#915)
* feat: improves pluggable auth error handling
* cleanup
* fix: consume input stream immediately so that the spawned process will not hang if the STDOUT buffer is filled.
* fix: fix merge
* fix: review comments
* fix: refactor to keep ImpersonatedCredentials final (#917)
* fix: adds more documentation for InternalProcessBuilder and moves it to the bottom of the file
* fix: keep ImpersonatedCredentials final
* fix: make sure executor is shutdown
Co-authored-by: WhiteSource Renovate
Co-authored-by: Navina Ramesh
Co-authored-by: Timur Sadykov
Co-authored-by: Neenu Shaji
Co-authored-by: Jeff Williams
Co-authored-by: Emily Ball
---
.../google/auth/oauth2/ExecutableHandler.java | 67 ++
.../auth/oauth2/ExecutableResponse.java | 206 +++++
.../oauth2/ExternalAccountCredentials.java | 56 +-
.../auth/oauth2/PluggableAuthCredentials.java | 327 +++++++
.../auth/oauth2/PluggableAuthException.java | 48 ++
.../auth/oauth2/PluggableAuthHandler.java | 300 +++++++
.../auth/oauth2/ExecutableResponseTest.java | 302 +++++++
.../ExternalAccountCredentialsTest.java | 109 +++
.../auth/oauth2/GoogleCredentialsTest.java | 23 +
...ckExternalAccountCredentialsTransport.java | 5 +-
.../oauth2/PluggableAuthCredentialsTest.java | 444 ++++++++++
.../oauth2/PluggableAuthExceptionTest.java | 71 ++
.../auth/oauth2/PluggableAuthHandlerTest.java | 813 ++++++++++++++++++
oauth2_http/pom.xml | 12 +
pom.xml | 3 +
15 files changed, 2779 insertions(+), 7 deletions(-)
create mode 100644 oauth2_http/java/com/google/auth/oauth2/ExecutableHandler.java
create mode 100644 oauth2_http/java/com/google/auth/oauth2/ExecutableResponse.java
create mode 100644 oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java
create mode 100644 oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java
create mode 100644 oauth2_http/java/com/google/auth/oauth2/PluggableAuthHandler.java
create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/ExecutableResponseTest.java
create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java
create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthExceptionTest.java
create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthHandlerTest.java
diff --git a/oauth2_http/java/com/google/auth/oauth2/ExecutableHandler.java b/oauth2_http/java/com/google/auth/oauth2/ExecutableHandler.java
new file mode 100644
index 000000000..a052f2a5b
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/ExecutableHandler.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * 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 LLC 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.io.IOException;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/** An interface for 3rd party executable handling. */
+interface ExecutableHandler {
+
+ /** An interface for required fields needed to call 3rd party executables. */
+ interface ExecutableOptions {
+
+ /** An absolute path to the command used to retrieve 3rd party tokens. */
+ String getExecutableCommand();
+
+ /** A set of process-local environment variable mappings to be set for the script to execute. */
+ Map getEnvironmentMap();
+
+ /** A timeout for waiting for the executable to finish, in milliseconds. */
+ int getExecutableTimeoutMs();
+
+ /**
+ * An output file path which points to the 3rd party credentials generated by the executable.
+ */
+ @Nullable
+ String getOutputFilePath();
+ }
+
+ /**
+ * Handles executing the 3rd party script and parsing the token from the response.
+ *
+ * @param options A set executable options for handling the executable.
+ * @return A 3rd party token.
+ */
+ String retrieveTokenFromExecutable(ExecutableOptions options) throws IOException;
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/ExecutableResponse.java b/oauth2_http/java/com/google/auth/oauth2/ExecutableResponse.java
new file mode 100644
index 000000000..5559b5442
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/ExecutableResponse.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * 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 LLC 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 com.google.api.client.json.GenericJson;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.time.Instant;
+import javax.annotation.Nullable;
+
+/**
+ * Encapsulates response values for the 3rd party executable response (e.g. OIDC, SAML, error
+ * responses).
+ */
+class ExecutableResponse {
+
+ private static final String SAML_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:saml2";
+
+ private final int version;
+ private final boolean success;
+
+ @Nullable private Long expirationTime;
+ @Nullable private String tokenType;
+ @Nullable private String subjectToken;
+ @Nullable private String errorCode;
+ @Nullable private String errorMessage;
+
+ ExecutableResponse(GenericJson json) throws IOException {
+ if (!json.containsKey("version")) {
+ throw new PluggableAuthException(
+ "INVALID_EXECUTABLE_RESPONSE", "The executable response is missing the `version` field.");
+ }
+
+ if (!json.containsKey("success")) {
+ throw new PluggableAuthException(
+ "INVALID_EXECUTABLE_RESPONSE", "The executable response is missing the `success` field.");
+ }
+
+ this.version = parseIntField(json.get("version"));
+ this.success = (boolean) json.get("success");
+
+ if (success) {
+ if (!json.containsKey("token_type")) {
+ throw new PluggableAuthException(
+ "INVALID_EXECUTABLE_RESPONSE",
+ "The executable response is missing the `token_type` field.");
+ }
+
+ if (!json.containsKey("expiration_time")) {
+ throw new PluggableAuthException(
+ "INVALID_EXECUTABLE_RESPONSE",
+ "The executable response is missing the `expiration_time` field.");
+ }
+
+ this.tokenType = (String) json.get("token_type");
+ this.expirationTime = parseLongField(json.get("expiration_time"));
+
+ if (SAML_SUBJECT_TOKEN_TYPE.equals(tokenType)) {
+ this.subjectToken = (String) json.get("saml_response");
+ } else {
+ this.subjectToken = (String) json.get("id_token");
+ }
+ if (subjectToken == null || subjectToken.isEmpty()) {
+ throw new PluggableAuthException(
+ "INVALID_EXECUTABLE_RESPONSE",
+ "The executable response does not contain a valid token.");
+ }
+ } else {
+ // Error response must contain both an error code and message.
+ this.errorCode = (String) json.get("code");
+ this.errorMessage = (String) json.get("message");
+ if (errorCode == null
+ || errorCode.isEmpty()
+ || errorMessage == null
+ || errorMessage.isEmpty()) {
+ throw new PluggableAuthException(
+ "INVALID_EXECUTABLE_RESPONSE",
+ "The executable response must contain `error` and `message` fields when unsuccessful.");
+ }
+ }
+ }
+
+ /**
+ * Returns the version of the executable output. Only version `1` is currently supported. This is
+ * useful for future changes to the expected output format.
+ *
+ * @return The version of the JSON output.
+ */
+ int getVersion() {
+ return this.version;
+ }
+
+ /**
+ * Returns the status of the response.
+ *
+ *
When this is true, the response will contain the 3rd party token for a sign in / refresh
+ * operation. When this is false, the response should contain an additional error code and
+ * message.
+ *
+ * @return Whether the `success` field in the executable response is true.
+ */
+ boolean isSuccessful() {
+ return this.success;
+ }
+
+ /** Returns true if the subject token is expired or not present, false otherwise. */
+ boolean isExpired() {
+ return this.expirationTime == null || this.expirationTime <= Instant.now().getEpochSecond();
+ }
+
+ /** Returns whether the execution was successful and returned an unexpired token. */
+ boolean isValid() {
+ return isSuccessful() && !isExpired();
+ }
+
+ /** Returns the subject token expiration time in seconds (Unix epoch time). */
+ @Nullable
+ Long getExpirationTime() {
+ return this.expirationTime;
+ }
+
+ /**
+ * Returns the 3rd party subject token type.
+ *
+ *
Possible valid values:
+ *
+ *
+ *
urn:ietf:params:oauth:token-type:id_token
+ *
urn:ietf:params:oauth:token-type:jwt
+ *
urn:ietf:params:oauth:token-type:saml2
+ *
+ *
+ * @return The 3rd party subject token type for success responses, null otherwise.
+ */
+ @Nullable
+ String getTokenType() {
+ return this.tokenType;
+ }
+
+ /** Returns the subject token if the execution was successful, null otherwise. */
+ @Nullable
+ String getSubjectToken() {
+ return this.subjectToken;
+ }
+
+ /** Returns the error code if the execution was unsuccessful, null otherwise. */
+ @Nullable
+ String getErrorCode() {
+ return this.errorCode;
+ }
+
+ /** Returns the error message if the execution was unsuccessful, null otherwise. */
+ @Nullable
+ String getErrorMessage() {
+ return this.errorMessage;
+ }
+
+ private static int parseIntField(Object field) {
+ if (field instanceof String) {
+ return Integer.parseInt((String) field);
+ }
+ if (field instanceof BigDecimal) {
+ return ((BigDecimal) field).intValue();
+ }
+ return (int) field;
+ }
+
+ private static long parseLongField(Object field) {
+ if (field instanceof String) {
+ return Long.parseLong((String) field);
+ }
+ if (field instanceof BigDecimal) {
+ return ((BigDecimal) field).longValue();
+ }
+ return (long) field;
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
index 379e2a1cf..85af46335 100644
--- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
@@ -39,6 +39,7 @@
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.AwsCredentials.AwsCredentialSource;
import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource;
+import com.google.auth.oauth2.PluggableAuthCredentials.PluggableAuthCredentialSource;
import com.google.common.base.MoreObjects;
import java.io.IOException;
import java.io.InputStream;
@@ -76,6 +77,7 @@ abstract static class CredentialSource {
"https://www.googleapis.com/auth/cloud-platform";
static final String EXTERNAL_ACCOUNT_FILE_TYPE = "external_account";
+ static final String EXECUTABLE_SOURCE_KEY = "executable";
private final String transportFactoryClassName;
private final String audience;
@@ -99,6 +101,10 @@ abstract static class CredentialSource {
@Nullable protected final ImpersonatedCredentials impersonatedCredentials;
+ // Internal override for impersonated credentials. This is done to keep
+ // impersonatedCredentials final.
+ @Nullable private ImpersonatedCredentials impersonatedCredentialsOverride;
+
private EnvironmentProvider environmentProvider;
/**
@@ -194,7 +200,7 @@ protected ExternalAccountCredentials(
validateServiceAccountImpersonationInfoUrl(serviceAccountImpersonationUrl);
}
- this.impersonatedCredentials = initializeImpersonatedCredentials();
+ this.impersonatedCredentials = buildImpersonatedCredentials();
}
/**
@@ -236,10 +242,10 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder)
validateServiceAccountImpersonationInfoUrl(serviceAccountImpersonationUrl);
}
- this.impersonatedCredentials = initializeImpersonatedCredentials();
+ this.impersonatedCredentials = buildImpersonatedCredentials();
}
- private ImpersonatedCredentials initializeImpersonatedCredentials() {
+ ImpersonatedCredentials buildImpersonatedCredentials() {
if (serviceAccountImpersonationUrl == null) {
return null;
}
@@ -250,6 +256,11 @@ private ImpersonatedCredentials initializeImpersonatedCredentials() {
AwsCredentials.newBuilder((AwsCredentials) this)
.setServiceAccountImpersonationUrl(null)
.build();
+ } else if (this instanceof PluggableAuthCredentials) {
+ sourceCredentials =
+ PluggableAuthCredentials.newBuilder((PluggableAuthCredentials) this)
+ .setServiceAccountImpersonationUrl(null)
+ .build();
} else {
sourceCredentials =
IdentityPoolCredentials.newBuilder((IdentityPoolCredentials) this)
@@ -269,6 +280,10 @@ private ImpersonatedCredentials initializeImpersonatedCredentials() {
.build();
}
+ void overrideImpersonatedCredentials(ImpersonatedCredentials credentials) {
+ this.impersonatedCredentialsOverride = credentials;
+ }
+
@Override
public void getRequestMetadata(
URI uri, Executor executor, final RequestMetadataCallback callback) {
@@ -374,8 +389,20 @@ static ExternalAccountCredentials fromJson(
.setClientId(clientId)
.setClientSecret(clientSecret)
.build();
+ } else if (isPluggableAuthCredential(credentialSourceMap)) {
+ return PluggableAuthCredentials.newBuilder()
+ .setHttpTransportFactory(transportFactory)
+ .setAudience(audience)
+ .setSubjectTokenType(subjectTokenType)
+ .setTokenUrl(tokenUrl)
+ .setTokenInfoUrl(tokenInfoUrl)
+ .setCredentialSource(new PluggableAuthCredentialSource(credentialSourceMap))
+ .setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl)
+ .setQuotaProjectId(quotaProjectId)
+ .setClientId(clientId)
+ .setClientSecret(clientSecret)
+ .build();
}
-
return IdentityPoolCredentials.newBuilder()
.setHttpTransportFactory(transportFactory)
.setAudience(audience)
@@ -391,6 +418,11 @@ static ExternalAccountCredentials fromJson(
.build();
}
+ private static boolean isPluggableAuthCredential(Map credentialSource) {
+ // Pluggable Auth is enabled via a nested executable field in the credential source.
+ return credentialSource.containsKey(EXECUTABLE_SOURCE_KEY);
+ }
+
private static boolean isAwsCredential(Map credentialSource) {
return credentialSource.containsKey("environment_id")
&& ((String) credentialSource.get("environment_id")).startsWith("aws");
@@ -406,7 +438,10 @@ private static boolean isAwsCredential(Map credentialSource) {
protected AccessToken exchangeExternalCredentialForAccessToken(
StsTokenExchangeRequest stsTokenExchangeRequest) throws IOException {
// Handle service account impersonation if necessary.
- if (impersonatedCredentials != null) {
+ // Internal override takes priority.
+ if (impersonatedCredentialsOverride != null) {
+ return impersonatedCredentialsOverride.refreshAccessToken();
+ } else if (impersonatedCredentials != null) {
return impersonatedCredentials.refreshAccessToken();
}
@@ -468,6 +503,15 @@ public String getServiceAccountImpersonationUrl() {
return serviceAccountImpersonationUrl;
}
+ /** The service account email to be impersonated, if available. */
+ @Nullable
+ public String getServiceAccountEmail() {
+ if (serviceAccountImpersonationUrl == null || serviceAccountImpersonationUrl.isEmpty()) {
+ return null;
+ }
+ return ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl);
+ }
+
@Override
@Nullable
public String getQuotaProjectId() {
@@ -499,7 +543,7 @@ EnvironmentProvider getEnvironmentProvider() {
}
/**
- * Returns whether or not the current configuration is for Workforce Pools (which enable 3p user
+ * Returns whether the current configuration is for Workforce Pools (which enable 3p user
* identities, rather than workloads).
*/
public boolean isWorkforcePoolConfiguration() {
diff --git a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java
new file mode 100644
index 000000000..e3506c080
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * 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 LLC 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 com.google.auth.oauth2.ExecutableHandler.ExecutableOptions;
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * PluggableAuthCredentials enables the exchange of workload identity pool external credentials for
+ * Google access tokens by retrieving 3rd party tokens through a user supplied executable. These
+ * scripts/executables are completely independent of the Google Cloud Auth libraries. These
+ * credentials plug into ADC and will call the specified executable to retrieve the 3rd party token
+ * to be exchanged for a Google access token.
+ *
+ *
To use these credentials, the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment variable
+ * must be set to '1'. This is for security reasons.
+ *
+ *
Both OIDC and SAML are supported. The executable must adhere to a specific response format
+ * defined below.
+ *
+ *
The executable should print out the 3rd party token to STDOUT in JSON format. This is not
+ * required when an output_file is specified in the credential source, with the expectation being
+ * that the output file will contain the JSON response instead.
+ *
+ *
+ */
+ static class PluggableAuthCredentialSource extends CredentialSource {
+
+ // The default timeout for waiting for the executable to finish (30 seconds).
+ private static final int DEFAULT_EXECUTABLE_TIMEOUT_MS = 30 * 1000;
+ // The minimum timeout for waiting for the executable to finish (5 seconds).
+ private static final int MINIMUM_EXECUTABLE_TIMEOUT_MS = 5 * 1000;
+ // The maximum timeout for waiting for the executable to finish (120 seconds).
+ private static final int MAXIMUM_EXECUTABLE_TIMEOUT_MS = 120 * 1000;
+
+ private static final String COMMAND_KEY = "command";
+ private static final String TIMEOUT_MILLIS_KEY = "timeout_millis";
+ private static final String OUTPUT_FILE_KEY = "output_file";
+
+ // Required. The command used to retrieve the 3rd party token.
+ private final String executableCommand;
+
+ // Optional. Set to the default timeout when not provided.
+ private final int executableTimeoutMs;
+
+ // Optional. Provided when the 3rd party executable caches the response at the specified
+ // location.
+ @Nullable private final String outputFilePath;
+
+ PluggableAuthCredentialSource(Map credentialSourceMap) {
+ super(credentialSourceMap);
+
+ if (!credentialSourceMap.containsKey(EXECUTABLE_SOURCE_KEY)) {
+ throw new IllegalArgumentException(
+ "Invalid credential source for PluggableAuth credentials.");
+ }
+
+ Map executable =
+ (Map) credentialSourceMap.get(EXECUTABLE_SOURCE_KEY);
+
+ // Command is the only required field.
+ if (!executable.containsKey(COMMAND_KEY)) {
+ throw new IllegalArgumentException(
+ "The PluggableAuthCredentialSource is missing the required 'command' field.");
+ }
+
+ // Parse the executable timeout.
+ if (executable.containsKey(TIMEOUT_MILLIS_KEY)) {
+ Object timeout = executable.get(TIMEOUT_MILLIS_KEY);
+ if (timeout instanceof BigDecimal) {
+ executableTimeoutMs = ((BigDecimal) timeout).intValue();
+ } else if (executable.get(TIMEOUT_MILLIS_KEY) instanceof Integer) {
+ executableTimeoutMs = (int) timeout;
+ } else {
+ executableTimeoutMs = Integer.parseInt((String) timeout);
+ }
+ } else {
+ executableTimeoutMs = DEFAULT_EXECUTABLE_TIMEOUT_MS;
+ }
+
+ // Provided timeout must be between 5s and 120s.
+ if (executableTimeoutMs < MINIMUM_EXECUTABLE_TIMEOUT_MS
+ || executableTimeoutMs > MAXIMUM_EXECUTABLE_TIMEOUT_MS) {
+ throw new IllegalArgumentException(
+ String.format(
+ "The executable timeout must be between %s and %s milliseconds.",
+ MINIMUM_EXECUTABLE_TIMEOUT_MS, MAXIMUM_EXECUTABLE_TIMEOUT_MS));
+ }
+
+ executableCommand = (String) executable.get(COMMAND_KEY);
+ outputFilePath = (String) executable.get(OUTPUT_FILE_KEY);
+ }
+
+ String getCommand() {
+ return executableCommand;
+ }
+
+ int getTimeoutMs() {
+ return executableTimeoutMs;
+ }
+
+ @Nullable
+ String getOutputFilePath() {
+ return outputFilePath;
+ }
+ }
+
+ private final PluggableAuthCredentialSource config;
+
+ private final ExecutableHandler handler;
+
+ /** Internal constructor. See {@link Builder}. */
+ PluggableAuthCredentials(Builder builder) {
+ super(builder);
+ this.config = (PluggableAuthCredentialSource) builder.credentialSource;
+
+ if (builder.handler != null) {
+ handler = builder.handler;
+ } else {
+ handler = new PluggableAuthHandler(getEnvironmentProvider());
+ }
+
+ // Re-initialize impersonated credentials as the handler hasn't been set yet when
+ // this is called in the base class.
+ overrideImpersonatedCredentials(buildImpersonatedCredentials());
+ }
+
+ @Override
+ public AccessToken refreshAccessToken() throws IOException {
+ String credential = retrieveSubjectToken();
+ StsTokenExchangeRequest.Builder stsTokenExchangeRequest =
+ StsTokenExchangeRequest.newBuilder(credential, getSubjectTokenType())
+ .setAudience(getAudience());
+
+ Collection scopes = getScopes();
+ if (scopes != null && !scopes.isEmpty()) {
+ stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes));
+ }
+ return exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest.build());
+ }
+
+ /**
+ * Returns the 3rd party subject token by calling the executable specified in the credential
+ * source.
+ *
+ * @throws IOException if an error occurs with the executable execution.
+ */
+ @Override
+ public String retrieveSubjectToken() throws IOException {
+ String executableCommand = config.getCommand();
+ String outputFilePath = config.getOutputFilePath();
+ int executableTimeoutMs = config.getTimeoutMs();
+
+ Map envMap = new HashMap<>();
+ envMap.put("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE", getAudience());
+ envMap.put("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE", getSubjectTokenType());
+ // Always set to 0 for Workload Identity Federation.
+ envMap.put("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE", "0");
+ if (getServiceAccountEmail() != null) {
+ envMap.put("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL", getServiceAccountEmail());
+ }
+ if (outputFilePath != null && !outputFilePath.isEmpty()) {
+ envMap.put("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE", outputFilePath);
+ }
+
+ ExecutableOptions options =
+ new ExecutableOptions() {
+ @Override
+ public String getExecutableCommand() {
+ return executableCommand;
+ }
+
+ @Override
+ public Map getEnvironmentMap() {
+ return envMap;
+ }
+
+ @Override
+ public int getExecutableTimeoutMs() {
+ return executableTimeoutMs;
+ }
+
+ @Nullable
+ @Override
+ public String getOutputFilePath() {
+ return outputFilePath;
+ }
+ };
+
+ // Delegate handling of the executable to the handler.
+ return this.handler.retrieveTokenFromExecutable(options);
+ }
+
+ /** Clones the PluggableAuthCredentials with the specified scopes. */
+ @Override
+ public PluggableAuthCredentials createScoped(Collection newScopes) {
+ return new PluggableAuthCredentials(
+ (PluggableAuthCredentials.Builder) newBuilder(this).setScopes(newScopes));
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public static Builder newBuilder(PluggableAuthCredentials pluggableAuthCredentials) {
+ return new Builder(pluggableAuthCredentials);
+ }
+
+ @VisibleForTesting
+ @Nullable
+ ExecutableHandler getExecutableHandler() {
+ return this.handler;
+ }
+
+ public static class Builder extends ExternalAccountCredentials.Builder {
+
+ private ExecutableHandler handler;
+
+ Builder() {}
+
+ Builder(PluggableAuthCredentials credentials) {
+ super(credentials);
+ this.handler = credentials.handler;
+ }
+
+ public Builder setExecutableHandler(ExecutableHandler handler) {
+ this.handler = handler;
+ return this;
+ }
+
+ @Override
+ public PluggableAuthCredentials build() {
+ return new PluggableAuthCredentials(this);
+ }
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java
new file mode 100644
index 000000000..894b324a9
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthException.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * 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 LLC 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 static com.google.common.base.Preconditions.checkNotNull;
+
+/** Encapsulates the error response's for 3rd party executables defined by the executable spec. */
+class PluggableAuthException extends OAuthException {
+
+ PluggableAuthException(String errorCode, String errorDescription) {
+ super(errorCode, checkNotNull(errorDescription), /* errorUri=*/ null);
+ }
+
+ /** The message with format: Error code {errorCode}: {errorDescription}. */
+ @Override
+ public String getMessage() {
+ return "Error code " + getErrorCode() + ": " + getErrorDescription();
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthHandler.java b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthHandler.java
new file mode 100644
index 000000000..24b0978cd
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthHandler.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * 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 LLC 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 com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonParser;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+
+/**
+ * Internal handler for retrieving 3rd party tokens from user defined scripts/executables for
+ * workload identity federation.
+ *
+ *
See {@link PluggableAuthCredentials}.
+ */
+final class PluggableAuthHandler implements ExecutableHandler {
+
+ // The maximum supported version for the executable response.
+ // The executable response always includes a version number that is used
+ // to detect compatibility with the response and library verions.
+ private static final int EXECUTABLE_SUPPORTED_MAX_VERSION = 1;
+
+ // The GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES dictates if this feature is enabled.
+ // The GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment variable must be set to '1' for
+ // security reasons.
+ private static final String GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES =
+ "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES";
+
+ // The exit status of the 3P script that represents a successful execution.
+ private static final int EXIT_CODE_SUCCESS = 0;
+
+ private final EnvironmentProvider environmentProvider;
+ private InternalProcessBuilder internalProcessBuilder;
+
+ PluggableAuthHandler(EnvironmentProvider environmentProvider) {
+ this.environmentProvider = environmentProvider;
+ }
+
+ @VisibleForTesting
+ PluggableAuthHandler(
+ EnvironmentProvider environmentProvider, InternalProcessBuilder internalProcessBuilder) {
+ this.environmentProvider = environmentProvider;
+ this.internalProcessBuilder = internalProcessBuilder;
+ }
+
+ @Override
+ public String retrieveTokenFromExecutable(ExecutableOptions options) throws IOException {
+ // Validate that executables are allowed to run. To use Pluggable Auth,
+ // The GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment variable must be set to 1
+ // for security reasons.
+ if (!"1".equals(this.environmentProvider.getEnv(GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES))) {
+ throw new PluggableAuthException(
+ "PLUGGABLE_AUTH_DISABLED",
+ "Pluggable Auth executables need "
+ + "to be explicitly allowed to run by setting the "
+ + "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment variable to 1.");
+ }
+
+ // Users can specify an output file path in the Pluggable Auth ADC configuration.
+ // This is the file's absolute path. Their executable will handle writing the 3P credentials to
+ // this file.
+ // If specified, we will first check if we have valid unexpired credentials stored in this
+ // location to avoid running the executable until they are expired.
+ ExecutableResponse executableResponse = getCachedExecutableResponse(options);
+
+ // If the output_file does not contain a valid response, call the executable.
+ if (executableResponse == null) {
+ executableResponse = getExecutableResponse(options);
+ }
+
+ // The executable response includes a version. Validate that the version is compatible
+ // with the library.
+ if (executableResponse.getVersion() > EXECUTABLE_SUPPORTED_MAX_VERSION) {
+ throw new PluggableAuthException(
+ "UNSUPPORTED_VERSION",
+ "The version of the executable response is not supported. "
+ + String.format(
+ "The maximum version currently supported is %s.",
+ EXECUTABLE_SUPPORTED_MAX_VERSION));
+ }
+
+ if (!executableResponse.isSuccessful()) {
+ throw new PluggableAuthException(
+ executableResponse.getErrorCode(), executableResponse.getErrorMessage());
+ }
+
+ if (executableResponse.isExpired()) {
+ throw new PluggableAuthException("INVALID_RESPONSE", "The executable response is expired.");
+ }
+
+ // Subject token is valid and can be returned.
+ return executableResponse.getSubjectToken();
+ }
+
+ @Nullable
+ ExecutableResponse getCachedExecutableResponse(ExecutableOptions options)
+ throws PluggableAuthException {
+ ExecutableResponse executableResponse = null;
+ if (options.getOutputFilePath() != null && !options.getOutputFilePath().isEmpty()) {
+ // Try reading cached response from output_file.
+ try {
+ File outputFile = new File(options.getOutputFilePath());
+ // Check if the output file is valid and not empty.
+ if (outputFile.isFile() && outputFile.length() > 0) {
+ InputStream inputStream = new FileInputStream(options.getOutputFilePath());
+ BufferedReader reader =
+ new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+ JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(reader);
+ ExecutableResponse cachedResponse =
+ new ExecutableResponse(parser.parseAndClose(GenericJson.class));
+ // If the cached response is successful and unexpired, we can use it.
+ // Response version will be validated below.
+ if (cachedResponse.isValid()) {
+ executableResponse = cachedResponse;
+ }
+ }
+ } catch (Exception e) {
+ throw new PluggableAuthException(
+ "INVALID_OUTPUT_FILE",
+ "The output_file specified contains an invalid or malformed response." + e);
+ }
+ }
+ return executableResponse;
+ }
+
+ ExecutableResponse getExecutableResponse(ExecutableOptions options) throws IOException {
+ List components = Splitter.on(" ").splitToList(options.getExecutableCommand());
+
+ // Create the process.
+ InternalProcessBuilder processBuilder = getProcessBuilder(components);
+
+ // Inject environment variables.
+ Map envMap = processBuilder.environment();
+ envMap.putAll(options.getEnvironmentMap());
+
+ // Redirect error stream.
+ processBuilder.redirectErrorStream(true);
+
+ // Start the process.
+ Process process = processBuilder.start();
+
+ ExecutableResponse execResp;
+ String executableOutput = "";
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ try {
+ // Consume the input stream while waiting for the program to finish so that
+ // the process won't hang if the STDOUT buffer is filled.
+ Future future =
+ executor.submit(
+ () -> {
+ BufferedReader reader =
+ new BufferedReader(
+ new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
+
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line).append(System.lineSeparator());
+ }
+ return sb.toString().trim();
+ });
+
+ boolean success = process.waitFor(options.getExecutableTimeoutMs(), TimeUnit.MILLISECONDS);
+ if (!success) {
+ // Process has not terminated within the specified timeout.
+ throw new PluggableAuthException(
+ "TIMEOUT_EXCEEDED", "The executable failed to finish within the timeout specified.");
+ }
+ int exitCode = process.exitValue();
+ if (exitCode != EXIT_CODE_SUCCESS) {
+ throw new PluggableAuthException(
+ "EXIT_CODE", String.format("The executable failed with exit code %s.", exitCode));
+ }
+
+ executableOutput = future.get();
+ executor.shutdownNow();
+
+ JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(executableOutput);
+ execResp = new ExecutableResponse(parser.parseAndClose(GenericJson.class));
+ } catch (IOException e) {
+ // Destroy the process.
+ process.destroy();
+
+ // Shutdown executor if needed.
+ if (!executor.isShutdown()) {
+ executor.shutdownNow();
+ }
+
+ if (e instanceof PluggableAuthException) {
+ throw e;
+ }
+ // An error may have occurred in the executable and should be surfaced.
+ throw new PluggableAuthException(
+ "INVALID_RESPONSE",
+ String.format("The executable returned an invalid response: %s.", executableOutput));
+ } catch (InterruptedException | ExecutionException e) {
+ // Destroy the process.
+ process.destroy();
+
+ throw new PluggableAuthException(
+ "INTERRUPTED", String.format("The execution was interrupted: %s.", e));
+ }
+
+ process.destroy();
+ return execResp;
+ }
+
+ InternalProcessBuilder getProcessBuilder(List commandComponents) {
+ if (internalProcessBuilder != null) {
+ return internalProcessBuilder;
+ }
+ return new DefaultProcessBuilder(new ProcessBuilder(commandComponents));
+ }
+
+ /**
+ * An interface for creating and managing a process.
+ *
+ *
ProcessBuilder is final and does not implement any interface. This class allows concrete
+ * implementations to be specified to test these changes.
+ */
+ abstract static class InternalProcessBuilder {
+
+ abstract Map environment();
+
+ abstract InternalProcessBuilder redirectErrorStream(boolean redirectErrorStream);
+
+ abstract Process start() throws IOException;
+ }
+
+ /**
+ * A default implementation for {@link InternalProcessBuilder} that wraps {@link ProcessBuilder}.
+ */
+ static final class DefaultProcessBuilder extends InternalProcessBuilder {
+ ProcessBuilder processBuilder;
+
+ DefaultProcessBuilder(ProcessBuilder processBuilder) {
+ this.processBuilder = processBuilder;
+ }
+
+ @Override
+ Map environment() {
+ return this.processBuilder.environment();
+ }
+
+ @Override
+ InternalProcessBuilder redirectErrorStream(boolean redirectErrorStream) {
+ this.processBuilder.redirectErrorStream(redirectErrorStream);
+ return this;
+ }
+
+ @Override
+ Process start() throws IOException {
+ return this.processBuilder.start();
+ }
+ }
+}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExecutableResponseTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExecutableResponseTest.java
new file mode 100644
index 000000000..b6f85684a
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/oauth2/ExecutableResponseTest.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * 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 LLC 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.api.client.json.GenericJson;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.time.Instant;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link ExecutableResponse}. */
+class ExecutableResponseTest {
+
+ private static final String TOKEN_TYPE_OIDC = "urn:ietf:params:oauth:token-type:id_token";
+ private static final String TOKEN_TYPE_SAML = "urn:ietf:params:oauth:token-type:saml2";
+ private static final String ID_TOKEN = "header.payload.signature";
+ private static final String SAML_RESPONSE = "samlResponse";
+
+ private static final int EXECUTABLE_SUPPORTED_MAX_VERSION = 1;
+ private static final int EXPIRATION_DURATION = 3600;
+
+ @Test
+ void constructor_successOidcResponse() throws IOException {
+ ExecutableResponse response = new ExecutableResponse(buildOidcResponse());
+
+ assertTrue(response.isSuccessful());
+ assertTrue(response.isValid());
+ assertEquals(1, response.getVersion());
+ assertEquals(TOKEN_TYPE_OIDC, response.getTokenType());
+ assertEquals(ID_TOKEN, response.getSubjectToken());
+ assertEquals(
+ Instant.now().getEpochSecond() + EXPIRATION_DURATION, response.getExpirationTime());
+ assertEquals(1, response.getVersion());
+ }
+
+ @Test
+ void constructor_successSamlResponse() throws IOException {
+ ExecutableResponse response = new ExecutableResponse(buildSamlResponse());
+
+ assertTrue(response.isSuccessful());
+ assertTrue(response.isValid());
+ assertEquals(EXECUTABLE_SUPPORTED_MAX_VERSION, response.getVersion());
+ assertEquals(TOKEN_TYPE_SAML, response.getTokenType());
+ assertEquals(SAML_RESPONSE, response.getSubjectToken());
+ assertEquals(
+ Instant.now().getEpochSecond() + EXPIRATION_DURATION, response.getExpirationTime());
+ }
+
+ @Test
+ void constructor_validErrorResponse() throws IOException {
+ ExecutableResponse response = new ExecutableResponse(buildErrorResponse());
+
+ assertFalse(response.isSuccessful());
+ assertFalse(response.isValid());
+ assertTrue(response.isExpired());
+ assertNull(response.getSubjectToken());
+ assertNull(response.getTokenType());
+ assertNull(response.getExpirationTime());
+ assertEquals(1, response.getVersion());
+ assertEquals("401", response.getErrorCode());
+ assertEquals("Caller not authorized.", response.getErrorMessage());
+ }
+
+ @Test
+ void constructor_errorResponseMissingCode_throws() {
+ GenericJson jsonResponse = buildErrorResponse();
+
+ Object[] values = new Object[] {null, ""};
+ for (Object value : values) {
+ jsonResponse.put("code", value);
+
+ PluggableAuthException exception =
+ assertThrows(
+ PluggableAuthException.class,
+ () -> new ExecutableResponse(jsonResponse),
+ "Exception should be thrown.");
+
+ assertEquals(
+ "Error code INVALID_EXECUTABLE_RESPONSE: The executable response must contain "
+ + "`error` and `message` fields when unsuccessful.",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ void constructor_errorResponseMissingMessage_throws() {
+ GenericJson jsonResponse = buildErrorResponse();
+
+ Object[] values = new Object[] {null, ""};
+ for (Object value : values) {
+ jsonResponse.put("message", value);
+
+ PluggableAuthException exception =
+ assertThrows(
+ PluggableAuthException.class,
+ () -> new ExecutableResponse(jsonResponse),
+ "Exception should be thrown.");
+
+ assertEquals(
+ "Error code INVALID_EXECUTABLE_RESPONSE: The executable response must contain "
+ + "`error` and `message` fields when unsuccessful.",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ void constructor_successResponseMissingVersionField_throws() {
+ GenericJson jsonResponse = buildOidcResponse();
+ jsonResponse.remove("version");
+
+ PluggableAuthException exception =
+ assertThrows(
+ PluggableAuthException.class,
+ () -> new ExecutableResponse(jsonResponse),
+ "Exception should be thrown.");
+
+ assertEquals(
+ "Error code INVALID_EXECUTABLE_RESPONSE: The executable response is missing the "
+ + "`version` field.",
+ exception.getMessage());
+ }
+
+ @Test
+ void constructor_successResponseMissingSuccessField_throws() {
+ GenericJson jsonResponse = buildOidcResponse();
+ jsonResponse.remove("success");
+
+ PluggableAuthException exception =
+ assertThrows(
+ PluggableAuthException.class,
+ () -> new ExecutableResponse(jsonResponse),
+ "Exception should be thrown.");
+
+ assertEquals(
+ "Error code INVALID_EXECUTABLE_RESPONSE: The executable response is missing the "
+ + "`success` field.",
+ exception.getMessage());
+ }
+
+ @Test
+ void constructor_successResponseMissingTokenTypeField_throws() {
+ GenericJson jsonResponse = buildOidcResponse();
+ jsonResponse.remove("token_type");
+
+ PluggableAuthException exception =
+ assertThrows(
+ PluggableAuthException.class,
+ () -> new ExecutableResponse(jsonResponse),
+ "Exception should be thrown.");
+
+ assertEquals(
+ "Error code INVALID_EXECUTABLE_RESPONSE: The executable response is missing the "
+ + "`token_type` field.",
+ exception.getMessage());
+ }
+
+ @Test
+ void constructor_successResponseMissingExpirationTimeField_throws() {
+ GenericJson jsonResponse = buildOidcResponse();
+ jsonResponse.remove("expiration_time");
+
+ PluggableAuthException exception =
+ assertThrows(
+ PluggableAuthException.class,
+ () -> new ExecutableResponse(jsonResponse),
+ "Exception should be thrown.");
+
+ assertEquals(
+ "Error code INVALID_EXECUTABLE_RESPONSE: The executable response is missing the "
+ + "`expiration_time` field.",
+ exception.getMessage());
+ }
+
+ @Test
+ void constructor_samlResponseMissingSubjectToken_throws() {
+ GenericJson jsonResponse = buildSamlResponse();
+
+ Object[] values = new Object[] {null, ""};
+ for (Object value : values) {
+ jsonResponse.put("saml_response", value);
+
+ PluggableAuthException exception =
+ assertThrows(
+ PluggableAuthException.class,
+ () -> new ExecutableResponse(jsonResponse),
+ "Exception should be thrown.");
+
+ assertEquals(
+ "Error code INVALID_EXECUTABLE_RESPONSE: The executable response does not "
+ + "contain a valid token.",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ void constructor_oidcResponseMissingSubjectToken_throws() {
+ GenericJson jsonResponse = buildOidcResponse();
+
+ Object[] values = new Object[] {null, ""};
+ for (Object value : values) {
+ jsonResponse.put("id_token", value);
+
+ PluggableAuthException exception =
+ assertThrows(
+ PluggableAuthException.class,
+ () -> new ExecutableResponse(jsonResponse),
+ "Exception should be thrown.");
+
+ assertEquals(
+ "Error code INVALID_EXECUTABLE_RESPONSE: The executable response does not "
+ + "contain a valid token.",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ void isExpired() throws IOException {
+ GenericJson jsonResponse = buildOidcResponse();
+
+ BigDecimal[] values =
+ new BigDecimal[] {
+ BigDecimal.valueOf(Instant.now().getEpochSecond() - 1000),
+ BigDecimal.valueOf(Instant.now().getEpochSecond() + 1000)
+ };
+ boolean[] expectedResults = new boolean[] {true, false};
+
+ for (int i = 0; i < values.length; i++) {
+ jsonResponse.put("expiration_time", values[i]);
+
+ ExecutableResponse response = new ExecutableResponse(jsonResponse);
+
+ assertEquals(expectedResults[i], response.isExpired());
+ }
+ }
+
+ private static GenericJson buildOidcResponse() {
+ GenericJson json = new GenericJson();
+ json.setFactory(OAuth2Utils.JSON_FACTORY);
+ json.put("version", EXECUTABLE_SUPPORTED_MAX_VERSION);
+ json.put("success", true);
+ json.put("token_type", TOKEN_TYPE_OIDC);
+ json.put("id_token", ID_TOKEN);
+ json.put("expiration_time", Instant.now().getEpochSecond() + EXPIRATION_DURATION);
+ return json;
+ }
+
+ private static GenericJson buildSamlResponse() {
+ GenericJson json = new GenericJson();
+ json.setFactory(OAuth2Utils.JSON_FACTORY);
+ json.put("version", EXECUTABLE_SUPPORTED_MAX_VERSION);
+ json.put("success", true);
+ json.put("token_type", TOKEN_TYPE_SAML);
+ json.put("saml_response", "samlResponse");
+ json.put("expiration_time", Instant.now().getEpochSecond() + EXPIRATION_DURATION);
+ return json;
+ }
+
+ private static GenericJson buildErrorResponse() {
+ GenericJson json = new GenericJson();
+ json.setFactory(OAuth2Utils.JSON_FACTORY);
+ json.put("version", EXECUTABLE_SUPPORTED_MAX_VERSION);
+ json.put("success", false);
+ json.put("code", "401");
+ json.put("message", "Caller not authorized.");
+ return json;
+ }
+}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
index fb94bb93d..1b2b53a1c 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
@@ -43,9 +43,11 @@
import com.google.auth.TestUtils;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.ExternalAccountCredentialsTest.TestExternalAccountCredentials.TestCredentialSource;
+import com.google.auth.oauth2.PluggableAuthCredentials.PluggableAuthCredentialSource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
@@ -105,6 +107,16 @@ void fromStream_awsCredentials() throws IOException {
assertTrue(credential instanceof AwsCredentials);
}
+ @Test
+ void fromStream_pluggableAuthCredentials() throws IOException {
+ GenericJson json = buildJsonPluggableAuthCredential();
+
+ ExternalAccountCredentials credential =
+ ExternalAccountCredentials.fromStream(TestUtils.jsonToInputStream(json));
+
+ assertTrue(credential instanceof PluggableAuthCredentials);
+ }
+
@Test
void fromStream_invalidStream_throws() {
GenericJson json = buildJsonAwsCredential();
@@ -203,6 +215,53 @@ void fromJson_awsCredentials() {
assertNotNull(credential.getCredentialSource());
}
+ @Test
+ void fromJson_pluggableAuthCredentials() {
+ ExternalAccountCredentials credential =
+ ExternalAccountCredentials.fromJson(
+ buildJsonPluggableAuthCredential(), OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+
+ assertTrue(credential instanceof PluggableAuthCredentials);
+ assertEquals("audience", credential.getAudience());
+ assertEquals("subjectTokenType", credential.getSubjectTokenType());
+ assertEquals(STS_URL, credential.getTokenUrl());
+ assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
+ assertNotNull(credential.getCredentialSource());
+
+ PluggableAuthCredentialSource source =
+ (PluggableAuthCredentialSource) credential.getCredentialSource();
+ assertEquals("command", source.getCommand());
+ assertEquals(30000, source.getTimeoutMs()); // Default timeout is 30s.
+ assertNull(source.getOutputFilePath());
+ }
+
+ @Test
+ void fromJson_pluggableAuthCredentials_allExecutableOptionsSet() {
+ GenericJson json = buildJsonPluggableAuthCredential();
+ Map credentialSourceMap = (Map) json.get("credential_source");
+ // Add optional params to the executable config (timeout, output file path).
+ Map executableConfig =
+ (Map) credentialSourceMap.get("executable");
+ executableConfig.put("timeout_millis", 5000);
+ executableConfig.put("output_file", "path/to/output/file");
+
+ ExternalAccountCredentials credential =
+ ExternalAccountCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+
+ assertTrue(credential instanceof PluggableAuthCredentials);
+ assertEquals("audience", credential.getAudience());
+ assertEquals("subjectTokenType", credential.getSubjectTokenType());
+ assertEquals(STS_URL, credential.getTokenUrl());
+ assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
+ assertNotNull(credential.getCredentialSource());
+
+ PluggableAuthCredentialSource source =
+ (PluggableAuthCredentialSource) credential.getCredentialSource();
+ assertEquals("command", source.getCommand());
+ assertEquals("path/to/output/file", source.getOutputFilePath());
+ assertEquals(5000, source.getTimeoutMs());
+ }
+
@Test
void fromJson_nullJson_throws() {
assertThrows(
@@ -513,6 +572,38 @@ void exchangeExternalCredentialForAccessToken_withServiceAccountImpersonation()
transportFactory.transport.getServiceAccountAccessToken(), returnedToken.getTokenValue());
}
+ @Test
+ void exchangeExternalCredentialForAccessToken_withServiceAccountImpersonationOverride()
+ throws IOException {
+ transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime());
+
+ String serviceAccountEmail = "different@different.iam.gserviceaccount.com";
+ ExternalAccountCredentials credential =
+ ExternalAccountCredentials.fromStream(
+ IdentityPoolCredentialsTest.writeIdentityPoolCredentialsStream(
+ transportFactory.transport.getStsUrl(),
+ transportFactory.transport.getMetadataUrl(),
+ transportFactory.transport.getServiceAccountImpersonationUrl()),
+ transportFactory);
+
+ // Override impersonated credentials.
+ ExternalAccountCredentials sourceCredentials =
+ IdentityPoolCredentials.newBuilder((IdentityPoolCredentials) credential)
+ .setServiceAccountImpersonationUrl(null)
+ .build();
+ credential.overrideImpersonatedCredentials(
+ new ImpersonatedCredentials.Builder(sourceCredentials, serviceAccountEmail)
+ .setScopes(new ArrayList<>(sourceCredentials.getScopes()))
+ .setHttpTransportFactory(transportFactory)
+ .build());
+
+ credential.exchangeExternalCredentialForAccessToken(
+ StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build());
+
+ assertTrue(
+ transportFactory.transport.getRequests().get(2).getUrl().contains(serviceAccountEmail));
+ }
+
@Test
void exchangeExternalCredentialForAccessToken_throws() throws IOException {
ExternalAccountCredentials credential =
@@ -704,6 +795,24 @@ private GenericJson buildJsonAwsCredential() {
return json;
}
+ private GenericJson buildJsonPluggableAuthCredential() {
+ GenericJson json = new GenericJson();
+ json.put("audience", "audience");
+ json.put("subject_token_type", "subjectTokenType");
+ json.put("token_url", STS_URL);
+ json.put("token_info_url", "tokenInfoUrl");
+
+ Map> credentialSource = new HashMap<>();
+
+ Map executableConfig = new HashMap<>();
+ executableConfig.put("command", "command");
+
+ credentialSource.put("executable", executableConfig);
+ json.put("credential_source", credentialSource);
+
+ return json;
+ }
+
static class TestExternalAccountCredentials extends ExternalAccountCredentials {
static class TestCredentialSource extends IdentityPoolCredentials.IdentityPoolCredentialSource {
protected TestCredentialSource(Map credentialSourceMap) {
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
index 8294749b2..4505445ce 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
@@ -260,6 +260,29 @@ void fromStream_awsCredentials_providesToken() throws IOException {
TestUtils.assertContainsBearerToken(metadata, transportFactory.transport.getAccessToken());
}
+ @Test
+ void fromStream_pluggableAuthCredentials_providesToken() throws IOException {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ InputStream stream =
+ PluggableAuthCredentialsTest.writeCredentialsStream(transportFactory.transport.getStsUrl());
+
+ GoogleCredentials credentials = GoogleCredentials.fromStream(stream, transportFactory);
+
+ assertNotNull(credentials);
+
+ // Create copy with mock executable handler.
+ PluggableAuthCredentials copy =
+ PluggableAuthCredentials.newBuilder((PluggableAuthCredentials) credentials)
+ .setExecutableHandler(options -> "pluggableAuthToken")
+ .build();
+
+ copy = copy.createScoped(SCOPES);
+ Map> metadata = copy.getRequestMetadata(CALL_URI);
+ TestUtils.assertContainsBearerToken(metadata, transportFactory.transport.getAccessToken());
+ }
+
@Test
void fromStream_Impersonation_providesToken_WithQuotaProject() throws IOException {
MockTokenServerTransportFactory transportFactoryForSource =
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
index 74f4771ca..1199ac1f7 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/MockExternalAccountCredentialsTransport.java
@@ -84,6 +84,8 @@ public class MockExternalAccountCredentialsTransport extends MockHttpTransport {
static final String SERVICE_ACCOUNT_IMPERSONATION_URL =
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/testn@test.iam.gserviceaccount.com:generateAccessToken";
+ static final String IAM_ENDPOINT = "https://iamcredentials.googleapis.com";
+
private Queue responseSequence = new ArrayDeque<>();
private Queue responseErrorSequence = new ArrayDeque<>();
private Queue refreshTokenSequence = new ArrayDeque<>();
@@ -193,7 +195,8 @@ public LowLevelHttpResponse execute() throws IOException {
.setContentType(Json.MEDIA_TYPE)
.setContent(response.toPrettyString());
}
- if (SERVICE_ACCOUNT_IMPERSONATION_URL.equals(url)) {
+
+ if (url.contains(IAM_ENDPOINT)) {
GenericJson query =
OAuth2Utils.JSON_FACTORY
.createJsonParser(getContentAsString())
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java
new file mode 100644
index 000000000..01185bdb0
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * 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 LLC 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 static com.google.auth.oauth2.MockExternalAccountCredentialsTransport.SERVICE_ACCOUNT_IMPERSONATION_URL;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.json.GenericJson;
+import com.google.auth.TestUtils;
+import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.oauth2.ExecutableHandler.ExecutableOptions;
+import com.google.auth.oauth2.ExternalAccountCredentials.CredentialSource;
+import com.google.auth.oauth2.PluggableAuthCredentials.PluggableAuthCredentialSource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link PluggableAuthCredentials}. */
+class PluggableAuthCredentialsTest {
+ // The default timeout for waiting for the executable to finish (30 seconds).
+ private static final int DEFAULT_EXECUTABLE_TIMEOUT_MS = 30 * 1000;
+ // The minimum timeout for waiting for the executable to finish (5 seconds).
+ private static final int MINIMUM_EXECUTABLE_TIMEOUT_MS = 5 * 1000;
+ // The maximum timeout for waiting for the executable to finish (120 seconds).
+ private static final int MAXIMUM_EXECUTABLE_TIMEOUT_MS = 120 * 1000;
+ private static final String STS_URL = "https://sts.googleapis.com";
+
+ private static final PluggableAuthCredentials CREDENTIAL =
+ (PluggableAuthCredentials)
+ PluggableAuthCredentials.newBuilder()
+ .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
+ .setAudience(
+ "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider")
+ .setSubjectTokenType("subjectTokenType")
+ .setTokenUrl(STS_URL)
+ .setTokenInfoUrl("tokenInfoUrl")
+ .setCredentialSource(buildCredentialSource())
+ .build();
+
+ static class MockExternalAccountCredentialsTransportFactory implements HttpTransportFactory {
+
+ MockExternalAccountCredentialsTransport transport =
+ new MockExternalAccountCredentialsTransport();
+
+ @Override
+ public HttpTransport create() {
+ return transport;
+ }
+ }
+
+ @Test
+ void retrieveSubjectToken_shouldDelegateToHandler() throws IOException {
+ PluggableAuthCredentials credential =
+ PluggableAuthCredentials.newBuilder(CREDENTIAL)
+ .setExecutableHandler(options -> "pluggableAuthToken")
+ .build();
+ String subjectToken = credential.retrieveSubjectToken();
+ assertEquals(subjectToken, "pluggableAuthToken");
+ }
+
+ @Test
+ void retrieveSubjectToken_shouldPassAllOptionsToHandler() throws IOException {
+ String command = "/path/to/executable";
+ String timeout = "5000";
+ String outputFile = "/path/to/output/file";
+
+ final ExecutableOptions[] providedOptions = {null};
+ ExecutableHandler executableHandler =
+ options -> {
+ providedOptions[0] = options;
+ return "pluggableAuthToken";
+ };
+
+ PluggableAuthCredentials credential =
+ (PluggableAuthCredentials)
+ PluggableAuthCredentials.newBuilder(CREDENTIAL)
+ .setExecutableHandler(executableHandler)
+ .setCredentialSource(buildCredentialSource(command, timeout, outputFile))
+ .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
+ .build();
+
+ String subjectToken = credential.retrieveSubjectToken();
+
+ assertEquals(subjectToken, "pluggableAuthToken");
+
+ // Validate that the correct options were passed to the executable handler.
+ ExecutableOptions options = providedOptions[0];
+ assertEquals(options.getExecutableCommand(), command);
+ assertEquals(options.getExecutableTimeoutMs(), Integer.parseInt(timeout));
+ assertEquals(options.getOutputFilePath(), outputFile);
+
+ Map envMap = options.getEnvironmentMap();
+ assertEquals(envMap.size(), 5);
+ assertEquals(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"), credential.getAudience());
+ assertEquals(
+ envMap.get("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"), credential.getSubjectTokenType());
+ assertEquals(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"), "0");
+ assertEquals(
+ envMap.get("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"),
+ credential.getServiceAccountEmail());
+ assertEquals(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"), outputFile);
+ }
+
+ @Test
+ void retrieveSubjectToken_shouldPassMinimalOptionsToHandler() throws IOException {
+ String command = "/path/to/executable";
+
+ final ExecutableOptions[] providedOptions = {null};
+ ExecutableHandler executableHandler =
+ options -> {
+ providedOptions[0] = options;
+ return "pluggableAuthToken";
+ };
+
+ PluggableAuthCredentials credential =
+ (PluggableAuthCredentials)
+ PluggableAuthCredentials.newBuilder(CREDENTIAL)
+ .setExecutableHandler(executableHandler)
+ .setCredentialSource(
+ buildCredentialSource(command, /* timeoutMs= */ null, /* outputFile= */ null))
+ .build();
+
+ String subjectToken = credential.retrieveSubjectToken();
+
+ assertEquals(subjectToken, "pluggableAuthToken");
+
+ // Validate that the correct options were passed to the executable handler.
+ ExecutableOptions options = providedOptions[0];
+ assertEquals(options.getExecutableCommand(), command);
+ assertEquals(options.getExecutableTimeoutMs(), DEFAULT_EXECUTABLE_TIMEOUT_MS);
+ assertNull(options.getOutputFilePath());
+
+ Map envMap = options.getEnvironmentMap();
+ assertEquals(envMap.size(), 3);
+ assertEquals(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"), credential.getAudience());
+ assertEquals(
+ envMap.get("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"), credential.getSubjectTokenType());
+ assertEquals(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"), "0");
+ assertNull(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"));
+ assertNull(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"));
+ }
+
+ @Test
+ void refreshAccessToken_withoutServiceAccountImpersonation() throws IOException {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime());
+
+ PluggableAuthCredentials credential =
+ (PluggableAuthCredentials)
+ PluggableAuthCredentials.newBuilder(CREDENTIAL)
+ .setExecutableHandler(options -> "pluggableAuthToken")
+ .setTokenUrl(transportFactory.transport.getStsUrl())
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ AccessToken accessToken = credential.refreshAccessToken();
+
+ assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
+
+ // Validate that the correct subject token was passed to STS.
+ Map query =
+ TestUtils.parseQuery(transportFactory.transport.getRequests().get(0).getContentAsString());
+ assertEquals(query.get("subject_token"), "pluggableAuthToken");
+ }
+
+ @Test
+ void refreshAccessToken_withServiceAccountImpersonation() throws IOException {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime());
+
+ PluggableAuthCredentials credential =
+ (PluggableAuthCredentials)
+ PluggableAuthCredentials.newBuilder(CREDENTIAL)
+ .setExecutableHandler(options -> "pluggableAuthToken")
+ .setTokenUrl(transportFactory.transport.getStsUrl())
+ .setServiceAccountImpersonationUrl(
+ transportFactory.transport.getServiceAccountImpersonationUrl())
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ AccessToken accessToken = credential.refreshAccessToken();
+
+ assertEquals(
+ transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue());
+
+ // Validate that the correct subject token was passed to STS.
+ Map query =
+ TestUtils.parseQuery(transportFactory.transport.getRequests().get(0).getContentAsString());
+ assertEquals(query.get("subject_token"), "pluggableAuthToken");
+ }
+
+ @Test
+ void pluggableAuthCredentialSource_allFields() {
+ Map source = new HashMap<>();
+ Map executable = new HashMap<>();
+ source.put("executable", executable);
+ executable.put("command", "/path/to/executable");
+ executable.put("timeout_millis", "10000");
+ executable.put("output_file", "/path/to/output/file");
+
+ PluggableAuthCredentialSource credentialSource = new PluggableAuthCredentialSource(source);
+
+ assertEquals(credentialSource.getCommand(), "/path/to/executable");
+ assertEquals(credentialSource.getTimeoutMs(), 10000);
+ assertEquals(credentialSource.getOutputFilePath(), "/path/to/output/file");
+ }
+
+ @Test
+ void pluggableAuthCredentialSource_noTimeoutProvided_setToDefault() {
+ Map source = new HashMap<>();
+ Map executable = new HashMap<>();
+ source.put("executable", executable);
+ executable.put("command", "command");
+ PluggableAuthCredentialSource credentialSource = new PluggableAuthCredentialSource(source);
+
+ assertEquals(credentialSource.getCommand(), "command");
+ assertEquals(credentialSource.getTimeoutMs(), DEFAULT_EXECUTABLE_TIMEOUT_MS);
+ assertNull(credentialSource.getOutputFilePath());
+ }
+
+ @Test
+ void pluggableAuthCredentialSource_timeoutProvidedOutOfRange_throws() {
+ Map source = new HashMap<>();
+ Map executable = new HashMap<>();
+ source.put("executable", executable);
+
+ executable.put("command", "command");
+
+ int[] possibleOutOfRangeValues = new int[] {0, 4 * 1000, 121 * 1000};
+
+ for (int value : possibleOutOfRangeValues) {
+ executable.put("timeout_millis", value);
+
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ new PluggableAuthCredentialSource(source);
+ },
+ "Exception should be thrown.");
+ assertEquals(
+ String.format(
+ "The executable timeout must be between %s and %s milliseconds.",
+ MINIMUM_EXECUTABLE_TIMEOUT_MS, MAXIMUM_EXECUTABLE_TIMEOUT_MS),
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ void pluggableAuthCredentialSource_validTimeoutProvided() {
+ Map source = new HashMap<>();
+ Map executable = new HashMap<>();
+ source.put("executable", executable);
+
+ executable.put("command", "command");
+
+ Object[] possibleValues = new Object[] {"10000", 10000, BigDecimal.valueOf(10000L)};
+
+ for (Object value : possibleValues) {
+ executable.put("timeout_millis", value);
+ PluggableAuthCredentialSource credentialSource = new PluggableAuthCredentialSource(source);
+
+ assertEquals(credentialSource.getCommand(), "command");
+ assertEquals(credentialSource.getTimeoutMs(), 10000);
+ assertNull(credentialSource.getOutputFilePath());
+ }
+ }
+
+ @Test
+ void pluggableAuthCredentialSource_missingExecutableField_throws() {
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new PluggableAuthCredentialSource(new HashMap<>()),
+ "Exception should be thrown.");
+ assertEquals(
+ "Invalid credential source for PluggableAuth credentials.", exception.getMessage());
+ }
+
+ @Test
+ void pluggableAuthCredentialSource_missingExecutableCommandField_throws() {
+ Map source = new HashMap<>();
+ Map executable = new HashMap<>();
+ source.put("executable", executable);
+
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new PluggableAuthCredentialSource(source),
+ "Exception should be thrown.");
+ assertEquals(
+ "The PluggableAuthCredentialSource is missing the required 'command' field.",
+ exception.getMessage());
+ }
+
+ @Test
+ void builder_allFields() {
+ List scopes = Arrays.asList("scope1", "scope2");
+
+ CredentialSource source = buildCredentialSource();
+ ExecutableHandler handler = options -> "Token";
+
+ PluggableAuthCredentials credentials =
+ (PluggableAuthCredentials)
+ PluggableAuthCredentials.newBuilder()
+ .setExecutableHandler(handler)
+ .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
+ .setAudience("audience")
+ .setSubjectTokenType("subjectTokenType")
+ .setTokenUrl(STS_URL)
+ .setTokenInfoUrl("tokenInfoUrl")
+ .setCredentialSource(source)
+ .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
+ .setQuotaProjectId("quotaProjectId")
+ .setClientId("clientId")
+ .setClientSecret("clientSecret")
+ .setScopes(scopes)
+ .build();
+
+ assertEquals(credentials.getExecutableHandler(), handler);
+ assertEquals("audience", credentials.getAudience());
+ assertEquals("subjectTokenType", credentials.getSubjectTokenType());
+ assertEquals(credentials.getTokenUrl(), STS_URL);
+ assertEquals(credentials.getTokenInfoUrl(), "tokenInfoUrl");
+ assertEquals(
+ credentials.getServiceAccountImpersonationUrl(), SERVICE_ACCOUNT_IMPERSONATION_URL);
+ assertEquals(credentials.getCredentialSource(), source);
+ assertEquals(credentials.getQuotaProjectId(), "quotaProjectId");
+ assertEquals(credentials.getClientId(), "clientId");
+ assertEquals(credentials.getClientSecret(), "clientSecret");
+ assertEquals(credentials.getScopes(), scopes);
+ assertEquals(credentials.getEnvironmentProvider(), SystemEnvironmentProvider.getInstance());
+ }
+
+ @Test
+ void createdScoped_clonedCredentialWithAddedScopes() {
+ PluggableAuthCredentials credentials =
+ (PluggableAuthCredentials)
+ PluggableAuthCredentials.newBuilder(CREDENTIAL)
+ .setExecutableHandler(options -> "pluggableAuthToken")
+ .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
+ .setQuotaProjectId("quotaProjectId")
+ .setClientId("clientId")
+ .setClientSecret("clientSecret")
+ .build();
+
+ List newScopes = Arrays.asList("scope1", "scope2");
+
+ PluggableAuthCredentials newCredentials = credentials.createScoped(newScopes);
+
+ assertEquals(credentials.getAudience(), newCredentials.getAudience());
+ assertEquals(credentials.getSubjectTokenType(), newCredentials.getSubjectTokenType());
+ assertEquals(credentials.getTokenUrl(), newCredentials.getTokenUrl());
+ assertEquals(credentials.getTokenInfoUrl(), newCredentials.getTokenInfoUrl());
+ assertEquals(
+ credentials.getServiceAccountImpersonationUrl(),
+ newCredentials.getServiceAccountImpersonationUrl());
+ assertEquals(credentials.getCredentialSource(), newCredentials.getCredentialSource());
+ assertEquals(newScopes, newCredentials.getScopes());
+ assertEquals(credentials.getQuotaProjectId(), newCredentials.getQuotaProjectId());
+ assertEquals(credentials.getClientId(), newCredentials.getClientId());
+ assertEquals(credentials.getClientSecret(), newCredentials.getClientSecret());
+ assertEquals(credentials.getExecutableHandler(), newCredentials.getExecutableHandler());
+ }
+
+ private static CredentialSource buildCredentialSource() {
+ return buildCredentialSource("command", null, null);
+ }
+
+ private static CredentialSource buildCredentialSource(
+ String command, @Nullable String timeoutMs, @Nullable String outputFile) {
+ Map source = new HashMap<>();
+ Map executable = new HashMap<>();
+ source.put("executable", executable);
+ executable.put("command", command);
+ if (timeoutMs != null) {
+ executable.put("timeout_millis", timeoutMs);
+ }
+ if (outputFile != null) {
+ executable.put("output_file", outputFile);
+ }
+
+ return new PluggableAuthCredentialSource(source);
+ }
+
+ static InputStream writeCredentialsStream(String tokenUrl) throws IOException {
+ GenericJson json = new GenericJson();
+ json.put("audience", "audience");
+ json.put("subject_token_type", "subjectTokenType");
+ json.put("token_url", tokenUrl);
+ json.put("token_info_url", "tokenInfoUrl");
+ json.put("type", ExternalAccountCredentials.EXTERNAL_ACCOUNT_FILE_TYPE);
+
+ GenericJson credentialSource = new GenericJson();
+ GenericJson executable = new GenericJson();
+ executable.put("command", "/path/to/executable");
+ credentialSource.put("executable", executable);
+
+ json.put("credential_source", credentialSource);
+ return TestUtils.jsonToInputStream(json);
+ }
+}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthExceptionTest.java b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthExceptionTest.java
new file mode 100644
index 000000000..f924d4137
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthExceptionTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * 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 LLC 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link PluggableAuthException}. */
+class PluggableAuthExceptionTest {
+
+ private static final String MESSAGE_FORMAT = "Error code %s: %s";
+
+ @Test
+ void constructor() {
+ PluggableAuthException e = new PluggableAuthException("errorCode", "errorDescription");
+ assertEquals("errorCode", e.getErrorCode());
+ assertEquals("errorDescription", e.getErrorDescription());
+ }
+
+ @Test
+ void constructor_nullErrorCode_throws() {
+ assertThrows(
+ NullPointerException.class,
+ () -> new PluggableAuthException(/* errorCode= */ null, "errorDescription"));
+ }
+
+ @Test
+ void constructor_nullErrorDescription_throws() {
+ assertThrows(
+ NullPointerException.class,
+ () -> new PluggableAuthException("errorCode", /* errorDescription= */ null));
+ }
+
+ @Test
+ void getMessage() {
+ PluggableAuthException e = new PluggableAuthException("errorCode", "errorDescription");
+ String expectedMessage = String.format("Error code %s: %s", "errorCode", "errorDescription");
+ assertEquals(expectedMessage, e.getMessage());
+ }
+}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthHandlerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthHandlerTest.java
new file mode 100644
index 000000000..4e630d49c
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthHandlerTest.java
@@ -0,0 +1,813 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * 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 LLC 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.api.client.json.GenericJson;
+import com.google.auth.oauth2.ExecutableHandler.ExecutableOptions;
+import com.google.auth.oauth2.PluggableAuthHandler.InternalProcessBuilder;
+import com.google.common.collect.ImmutableMap;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+/** Tests for {@link PluggableAuthHandler}. */
+@RunWith(MockitoJUnitRunner.class)
+class PluggableAuthHandlerTest {
+ private static final String TOKEN_TYPE_OIDC = "urn:ietf:params:oauth:token-type:id_token";
+ private static final String TOKEN_TYPE_SAML = "urn:ietf:params:oauth:token-type:saml2";
+ private static final String ID_TOKEN = "header.payload.signature";
+ private static final String SAML_RESPONSE = "samlResponse";
+
+ private static final int EXECUTABLE_SUPPORTED_MAX_VERSION = 1;
+ private static final int EXPIRATION_DURATION = 3600;
+ private static final int EXIT_CODE_SUCCESS = 0;
+ private static final int EXIT_CODE_FAIL = 1;
+
+ private static final ExecutableOptions DEFAULT_OPTIONS =
+ new ExecutableOptions() {
+ @Override
+ public String getExecutableCommand() {
+ return "/path/to/executable";
+ }
+
+ @Override
+ public Map getEnvironmentMap() {
+ return ImmutableMap.of("optionKey1", "optionValue1", "optionValue2", "optionValue2");
+ }
+
+ @Override
+ public int getExecutableTimeoutMs() {
+ return 30000;
+ }
+
+ @Nullable
+ @Override
+ public String getOutputFilePath() {
+ return null;
+ }
+ };
+
+ @Test
+ void retrieveTokenFromExecutable_oidcResponse() throws IOException, InterruptedException {
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1");
+
+ Map currentEnv = new HashMap<>();
+ currentEnv.put("currentEnvKey1", "currentEnvValue1");
+ currentEnv.put("currentEnvKey2", "currentEnvValue2");
+
+ // Expected environment mappings.
+ HashMap expectedMap = new HashMap<>();
+ expectedMap.putAll(DEFAULT_OPTIONS.getEnvironmentMap());
+ expectedMap.putAll(currentEnv);
+
+ // Mock executable handling.
+ Process mockProcess = Mockito.mock(Process.class);
+ when(mockProcess.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(true);
+ when(mockProcess.exitValue()).thenReturn(EXIT_CODE_SUCCESS);
+
+ when(mockProcess.getInputStream())
+ .thenReturn(
+ new ByteArrayInputStream(
+ buildOidcResponse().toString().getBytes(StandardCharsets.UTF_8)));
+
+ InternalProcessBuilder processBuilder =
+ buildInternalProcessBuilder(
+ currentEnv, mockProcess, DEFAULT_OPTIONS.getExecutableCommand());
+
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder);
+
+ // Call retrieveTokenFromExecutable().
+ String token = handler.retrieveTokenFromExecutable(DEFAULT_OPTIONS);
+
+ verify(mockProcess, times(1)).destroy();
+ verify(mockProcess, times(1))
+ .waitFor(
+ eq(Long.valueOf(DEFAULT_OPTIONS.getExecutableTimeoutMs())), eq(TimeUnit.MILLISECONDS));
+ assertEquals(ID_TOKEN, token);
+
+ // Current env map should include the mappings from options.
+ assertEquals(4, currentEnv.size());
+ assertEquals(expectedMap, currentEnv);
+ }
+
+ @Test
+ void retrieveTokenFromExecutable_samlResponse() throws IOException, InterruptedException {
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1");
+
+ Map currentEnv = new HashMap<>();
+ currentEnv.put("currentEnvKey1", "currentEnvValue1");
+ currentEnv.put("currentEnvKey2", "currentEnvValue2");
+
+ // Expected environment mappings.
+ HashMap expectedMap = new HashMap<>();
+ expectedMap.putAll(DEFAULT_OPTIONS.getEnvironmentMap());
+ expectedMap.putAll(currentEnv);
+
+ // Mock executable handling.
+ Process mockProcess = Mockito.mock(Process.class);
+ when(mockProcess.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(true);
+ when(mockProcess.exitValue()).thenReturn(EXIT_CODE_SUCCESS);
+
+ // SAML response.
+ when(mockProcess.getInputStream())
+ .thenReturn(
+ new ByteArrayInputStream(
+ buildSamlResponse().toString().getBytes(StandardCharsets.UTF_8)));
+
+ InternalProcessBuilder processBuilder =
+ buildInternalProcessBuilder(
+ currentEnv, mockProcess, DEFAULT_OPTIONS.getExecutableCommand());
+
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder);
+
+ // Call retrieveTokenFromExecutable().
+ String token = handler.retrieveTokenFromExecutable(DEFAULT_OPTIONS);
+
+ verify(mockProcess, times(1)).destroy();
+ verify(mockProcess, times(1))
+ .waitFor(
+ eq(Long.valueOf(DEFAULT_OPTIONS.getExecutableTimeoutMs())), eq(TimeUnit.MILLISECONDS));
+ assertEquals(SAML_RESPONSE, token);
+
+ // Current env map should include the mappings from options.
+ assertEquals(4, currentEnv.size());
+ assertEquals(expectedMap, currentEnv);
+ }
+
+ @Test
+ void retrieveTokenFromExecutable_errorResponse_throws() throws InterruptedException {
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1");
+
+ // Mock executable handling.
+ Process mockProcess = Mockito.mock(Process.class);
+ when(mockProcess.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(true);
+ when(mockProcess.exitValue()).thenReturn(EXIT_CODE_SUCCESS);
+
+ // Error response.
+ when(mockProcess.getInputStream())
+ .thenReturn(
+ new ByteArrayInputStream(
+ buildErrorResponse().toString().getBytes(StandardCharsets.UTF_8)));
+
+ InternalProcessBuilder processBuilder =
+ buildInternalProcessBuilder(
+ new HashMap<>(), mockProcess, DEFAULT_OPTIONS.getExecutableCommand());
+
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder);
+
+ // Call retrieveTokenFromExecutable().
+ PluggableAuthException e =
+ assertThrows(
+ PluggableAuthException.class,
+ () -> handler.retrieveTokenFromExecutable(DEFAULT_OPTIONS));
+
+ assertEquals("401", e.getErrorCode());
+ assertEquals("Caller not authorized.", e.getErrorDescription());
+ }
+
+ @Test
+ void retrieveTokenFromExecutable_withOutputFile_usesCachedResponse()
+ throws IOException, InterruptedException {
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1");
+
+ // Build output_file.
+ File file = File.createTempFile("output_file", /* suffix= */ null, /* directory= */ null);
+ file.deleteOnExit();
+
+ OAuth2Utils.writeInputStreamToFile(
+ new ByteArrayInputStream(buildOidcResponse().toString().getBytes(StandardCharsets.UTF_8)),
+ file.getAbsolutePath());
+
+ // Options with output file specified.
+ ExecutableOptions options =
+ new ExecutableOptions() {
+ @Override
+ public String getExecutableCommand() {
+ return "/path/to/executable";
+ }
+
+ @Override
+ public Map getEnvironmentMap() {
+ return ImmutableMap.of();
+ }
+
+ @Override
+ public int getExecutableTimeoutMs() {
+ return 30000;
+ }
+
+ @Override
+ public String getOutputFilePath() {
+ return file.getAbsolutePath();
+ }
+ };
+
+ // Mock executable handling that does nothing since we are using the output file.
+ Process mockProcess = Mockito.mock(Process.class);
+ InternalProcessBuilder processBuilder =
+ buildInternalProcessBuilder(new HashMap<>(), mockProcess, options.getExecutableCommand());
+
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder);
+
+ // Call retrieveTokenFromExecutable().
+ String token = handler.retrieveTokenFromExecutable(options);
+
+ // Validate executable not invoked.
+ verify(mockProcess, times(0)).destroyForcibly();
+ verify(mockProcess, times(0))
+ .waitFor(eq(Long.valueOf(options.getExecutableTimeoutMs())), eq(TimeUnit.MILLISECONDS));
+
+ assertEquals(ID_TOKEN, token);
+ }
+
+ @Test
+ void retrieveTokenFromExecutable_withInvalidOutputFile_throws()
+ throws IOException, InterruptedException {
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1");
+
+ // Build output_file.
+ File file = File.createTempFile("output_file", /* suffix= */ null, /* directory= */ null);
+ file.deleteOnExit();
+
+ OAuth2Utils.writeInputStreamToFile(
+ new ByteArrayInputStream("Bad response.".getBytes(StandardCharsets.UTF_8)),
+ file.getAbsolutePath());
+
+ // Options with output file specified.
+ ExecutableOptions options =
+ new ExecutableOptions() {
+ @Override
+ public String getExecutableCommand() {
+ return "/path/to/executable";
+ }
+
+ @Override
+ public Map getEnvironmentMap() {
+ return ImmutableMap.of();
+ }
+
+ @Override
+ public int getExecutableTimeoutMs() {
+ return 30000;
+ }
+
+ @Override
+ public String getOutputFilePath() {
+ return file.getAbsolutePath();
+ }
+ };
+
+ // Mock executable handling that does nothing since we are using the output file.
+ Process mockProcess = Mockito.mock(Process.class);
+ InternalProcessBuilder processBuilder =
+ buildInternalProcessBuilder(new HashMap<>(), mockProcess, options.getExecutableCommand());
+
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder);
+
+ // Call retrieveTokenFromExecutable().
+ PluggableAuthException e =
+ assertThrows(
+ PluggableAuthException.class, () -> handler.retrieveTokenFromExecutable(options));
+
+ assertEquals("INVALID_OUTPUT_FILE", e.getErrorCode());
+ }
+
+ @Test
+ void retrieveTokenFromExecutable_expiredOutputFileResponse_callsExecutable()
+ throws IOException, InterruptedException {
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1");
+
+ // Build output_file.
+ File file = File.createTempFile("output_file", /* suffix= */ null, /* directory= */ null);
+ file.deleteOnExit();
+
+ // Create an expired response.
+ GenericJson json = buildOidcResponse();
+ json.put("expiration_time", Instant.now().getEpochSecond() - 1);
+
+ OAuth2Utils.writeInputStreamToFile(
+ new ByteArrayInputStream(json.toString().getBytes(StandardCharsets.UTF_8)),
+ file.getAbsolutePath());
+
+ // Options with output file specified.
+ ExecutableOptions options =
+ new ExecutableOptions() {
+ @Override
+ public String getExecutableCommand() {
+ return "/path/to/executable";
+ }
+
+ @Override
+ public Map getEnvironmentMap() {
+ return ImmutableMap.of();
+ }
+
+ @Override
+ public int getExecutableTimeoutMs() {
+ return 30000;
+ }
+
+ @Override
+ public String getOutputFilePath() {
+ return file.getAbsolutePath();
+ }
+ };
+
+ // Mock executable handling.
+ Process mockProcess = Mockito.mock(Process.class);
+ when(mockProcess.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(true);
+ when(mockProcess.exitValue()).thenReturn(EXIT_CODE_SUCCESS);
+ when(mockProcess.getInputStream())
+ .thenReturn(
+ new ByteArrayInputStream(
+ buildOidcResponse().toString().getBytes(StandardCharsets.UTF_8)));
+
+ InternalProcessBuilder processBuilder =
+ buildInternalProcessBuilder(new HashMap<>(), mockProcess, options.getExecutableCommand());
+
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder);
+
+ // Call retrieveTokenFromExecutable().
+ String token = handler.retrieveTokenFromExecutable(options);
+
+ // Validate that the executable was called.
+ verify(mockProcess, times(1)).destroy();
+ verify(mockProcess, times(1))
+ .waitFor(eq(Long.valueOf(options.getExecutableTimeoutMs())), eq(TimeUnit.MILLISECONDS));
+
+ assertEquals(ID_TOKEN, token);
+ }
+
+ @Test
+ void retrieveTokenFromExecutable_expiredResponse_throws() throws InterruptedException {
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1");
+
+ // Create expired response.
+ GenericJson json = buildOidcResponse();
+ json.put("expiration_time", Instant.now().getEpochSecond() - 1);
+
+ // Mock executable handling.
+ Process mockProcess = Mockito.mock(Process.class);
+ when(mockProcess.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(true);
+ when(mockProcess.exitValue()).thenReturn(EXIT_CODE_SUCCESS);
+ when(mockProcess.getInputStream())
+ .thenReturn(new ByteArrayInputStream(json.toString().getBytes(StandardCharsets.UTF_8)));
+
+ InternalProcessBuilder processBuilder =
+ buildInternalProcessBuilder(
+ new HashMap<>(), mockProcess, DEFAULT_OPTIONS.getExecutableCommand());
+
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder);
+
+ // Call retrieveTokenFromExecutable().
+ PluggableAuthException e =
+ assertThrows(
+ PluggableAuthException.class,
+ () -> handler.retrieveTokenFromExecutable(DEFAULT_OPTIONS));
+
+ assertEquals("INVALID_RESPONSE", e.getErrorCode());
+ assertEquals("The executable response is expired.", e.getErrorDescription());
+ }
+
+ @Test
+ void retrieveTokenFromExecutable_invalidVersion_throws() throws InterruptedException {
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1");
+
+ // Mock executable handling.
+ Process mockProcess = Mockito.mock(Process.class);
+ when(mockProcess.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(true);
+ when(mockProcess.exitValue()).thenReturn(EXIT_CODE_SUCCESS);
+
+ // SAML response.
+ GenericJson json = buildSamlResponse();
+ // Only version `1` is supported.
+ json.put("version", 2);
+ when(mockProcess.getInputStream())
+ .thenReturn(new ByteArrayInputStream(json.toString().getBytes(StandardCharsets.UTF_8)));
+
+ InternalProcessBuilder processBuilder =
+ buildInternalProcessBuilder(
+ new HashMap<>(), mockProcess, DEFAULT_OPTIONS.getExecutableCommand());
+
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder);
+
+ // Call retrieveTokenFromExecutable().
+ PluggableAuthException e =
+ assertThrows(
+ PluggableAuthException.class,
+ () -> handler.retrieveTokenFromExecutable(DEFAULT_OPTIONS));
+
+ assertEquals("UNSUPPORTED_VERSION", e.getErrorCode());
+ assertEquals(
+ "The version of the executable response is not supported. "
+ + String.format(
+ "The maximum version currently supported is %s.", EXECUTABLE_SUPPORTED_MAX_VERSION),
+ e.getErrorDescription());
+ }
+
+ @Test
+ void retrieveTokenFromExecutable_allowExecutablesDisabled_throws() {
+ // In order to use Pluggable Auth, GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES must be set to 1.
+ // If set to 0, a runtime exception should be thrown.
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "0");
+
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider);
+
+ PluggableAuthException e =
+ assertThrows(
+ PluggableAuthException.class,
+ () -> handler.retrieveTokenFromExecutable(DEFAULT_OPTIONS));
+
+ assertEquals("PLUGGABLE_AUTH_DISABLED", e.getErrorCode());
+ assertEquals(
+ "Pluggable Auth executables need to be explicitly allowed to run by "
+ + "setting the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment variable to 1.",
+ e.getErrorDescription());
+ }
+
+ @Test
+ void getExecutableResponse_oidcResponse() throws IOException, InterruptedException {
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1");
+
+ Map currentEnv = new HashMap<>();
+ currentEnv.put("currentEnvKey1", "currentEnvValue1");
+ currentEnv.put("currentEnvKey2", "currentEnvValue2");
+
+ // Expected environment mappings.
+ HashMap expectedMap = new HashMap<>();
+ expectedMap.putAll(DEFAULT_OPTIONS.getEnvironmentMap());
+ expectedMap.putAll(currentEnv);
+
+ // Mock executable handling.
+ Process mockProcess = Mockito.mock(Process.class);
+ when(mockProcess.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(true);
+ when(mockProcess.exitValue()).thenReturn(EXIT_CODE_SUCCESS);
+
+ // OIDC response.
+ when(mockProcess.getInputStream())
+ .thenReturn(
+ new ByteArrayInputStream(
+ buildOidcResponse().toString().getBytes(StandardCharsets.UTF_8)));
+
+ InternalProcessBuilder processBuilder =
+ buildInternalProcessBuilder(
+ currentEnv, mockProcess, DEFAULT_OPTIONS.getExecutableCommand());
+
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder);
+
+ ExecutableResponse response = handler.getExecutableResponse(DEFAULT_OPTIONS);
+
+ verify(mockProcess, times(1)).destroy();
+ verify(mockProcess, times(1))
+ .waitFor(
+ eq(Long.valueOf(DEFAULT_OPTIONS.getExecutableTimeoutMs())), eq(TimeUnit.MILLISECONDS));
+ assertEquals(EXECUTABLE_SUPPORTED_MAX_VERSION, response.getVersion());
+ assertTrue(response.isSuccessful());
+ assertEquals(TOKEN_TYPE_OIDC, response.getTokenType());
+ assertEquals(ID_TOKEN, response.getSubjectToken());
+ assertEquals(
+ Instant.now().getEpochSecond() + EXPIRATION_DURATION, response.getExpirationTime());
+ // Current env map should include the mappings from options.
+ assertEquals(4, currentEnv.size());
+ assertEquals(expectedMap, currentEnv);
+ }
+
+ @Test
+ void getExecutableResponse_samlResponse() throws IOException, InterruptedException {
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1");
+
+ Map currentEnv = new HashMap<>();
+ currentEnv.put("currentEnvKey1", "currentEnvValue1");
+ currentEnv.put("currentEnvKey2", "currentEnvValue2");
+
+ // Expected environment mappings.
+ HashMap expectedMap = new HashMap<>();
+ expectedMap.putAll(DEFAULT_OPTIONS.getEnvironmentMap());
+ expectedMap.putAll(currentEnv);
+
+ // Mock executable handling.
+ Process mockProcess = Mockito.mock(Process.class);
+ when(mockProcess.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(true);
+ when(mockProcess.exitValue()).thenReturn(EXIT_CODE_SUCCESS);
+
+ // SAML response.
+ when(mockProcess.getInputStream())
+ .thenReturn(
+ new ByteArrayInputStream(
+ buildSamlResponse().toString().getBytes(StandardCharsets.UTF_8)));
+
+ InternalProcessBuilder processBuilder =
+ buildInternalProcessBuilder(
+ currentEnv, mockProcess, DEFAULT_OPTIONS.getExecutableCommand());
+
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder);
+ ExecutableResponse response = handler.getExecutableResponse(DEFAULT_OPTIONS);
+
+ verify(mockProcess, times(1)).destroy();
+ verify(mockProcess, times(1))
+ .waitFor(
+ eq(Long.valueOf(DEFAULT_OPTIONS.getExecutableTimeoutMs())), eq(TimeUnit.MILLISECONDS));
+ assertEquals(EXECUTABLE_SUPPORTED_MAX_VERSION, response.getVersion());
+ assertTrue(response.isSuccessful());
+ assertEquals(TOKEN_TYPE_SAML, response.getTokenType());
+ assertEquals(SAML_RESPONSE, response.getSubjectToken());
+ assertEquals(
+ Instant.now().getEpochSecond() + EXPIRATION_DURATION, response.getExpirationTime());
+
+ // Current env map should include the mappings from options.
+ assertEquals(4, currentEnv.size());
+ assertEquals(expectedMap, currentEnv);
+
+ verify(mockProcess, times(1)).destroy();
+ }
+
+ @Test
+ void getExecutableResponse_errorResponse() throws IOException, InterruptedException {
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1");
+
+ Map currentEnv = new HashMap<>();
+ currentEnv.put("currentEnvKey1", "currentEnvValue1");
+ currentEnv.put("currentEnvKey2", "currentEnvValue2");
+
+ // Expected environment mappings.
+ HashMap expectedMap = new HashMap<>();
+ expectedMap.putAll(DEFAULT_OPTIONS.getEnvironmentMap());
+ expectedMap.putAll(currentEnv);
+
+ // Mock executable handling.
+ Process mockProcess = Mockito.mock(Process.class);
+
+ when(mockProcess.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(true);
+ when(mockProcess.exitValue()).thenReturn(EXIT_CODE_SUCCESS);
+
+ // Error response.
+ when(mockProcess.getInputStream())
+ .thenReturn(
+ new ByteArrayInputStream(
+ buildErrorResponse().toString().getBytes(StandardCharsets.UTF_8)));
+
+ InternalProcessBuilder processBuilder =
+ buildInternalProcessBuilder(
+ currentEnv, mockProcess, DEFAULT_OPTIONS.getExecutableCommand());
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder);
+
+ // Call getExecutableResponse().
+ ExecutableResponse response = handler.getExecutableResponse(DEFAULT_OPTIONS);
+
+ verify(mockProcess, times(1)).destroy();
+ verify(mockProcess, times(1))
+ .waitFor(
+ eq(Long.valueOf(DEFAULT_OPTIONS.getExecutableTimeoutMs())), eq(TimeUnit.MILLISECONDS));
+ assertEquals(EXECUTABLE_SUPPORTED_MAX_VERSION, response.getVersion());
+ assertFalse(response.isSuccessful());
+ assertEquals("401", response.getErrorCode());
+ assertEquals("Caller not authorized.", response.getErrorMessage());
+
+ // Current env map should include the mappings from options.
+ assertEquals(4, currentEnv.size());
+ assertEquals(expectedMap, currentEnv);
+ }
+
+ @Test
+ void getExecutableResponse_timeoutExceeded_throws() throws InterruptedException {
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1");
+
+ // Mock executable handling.
+ Process mockProcess = Mockito.mock(Process.class);
+ when(mockProcess.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(false);
+
+ InternalProcessBuilder processBuilder =
+ buildInternalProcessBuilder(
+ new HashMap<>(), mockProcess, DEFAULT_OPTIONS.getExecutableCommand());
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder);
+
+ // Call getExecutableResponse().
+ PluggableAuthException e =
+ assertThrows(
+ PluggableAuthException.class, () -> handler.getExecutableResponse(DEFAULT_OPTIONS));
+
+ assertEquals("TIMEOUT_EXCEEDED", e.getErrorCode());
+ assertEquals(
+ "The executable failed to finish within the timeout specified.", e.getErrorDescription());
+ verify(mockProcess, times(1))
+ .waitFor(
+ eq(Long.valueOf(DEFAULT_OPTIONS.getExecutableTimeoutMs())), eq(TimeUnit.MILLISECONDS));
+ verify(mockProcess, times(1)).destroy();
+ }
+
+ @Test
+ void getExecutableResponse_nonZeroExitCode_throws() throws InterruptedException {
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1");
+
+ // Mock executable handling.
+ Process mockProcess = Mockito.mock(Process.class);
+ when(mockProcess.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(true);
+ when(mockProcess.exitValue()).thenReturn(EXIT_CODE_FAIL);
+
+ InternalProcessBuilder processBuilder =
+ buildInternalProcessBuilder(
+ new HashMap<>(), mockProcess, DEFAULT_OPTIONS.getExecutableCommand());
+
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder);
+
+ // Call getExecutableResponse().
+ PluggableAuthException e =
+ assertThrows(
+ PluggableAuthException.class, () -> handler.getExecutableResponse(DEFAULT_OPTIONS));
+
+ assertEquals("EXIT_CODE", e.getErrorCode());
+ assertEquals(
+ String.format("The executable failed with exit code %s.", EXIT_CODE_FAIL),
+ e.getErrorDescription());
+
+ verify(mockProcess, times(1))
+ .waitFor(
+ eq(Long.valueOf(DEFAULT_OPTIONS.getExecutableTimeoutMs())), eq(TimeUnit.MILLISECONDS));
+ verify(mockProcess, times(1)).destroy();
+ }
+
+ @Test
+ void getExecutableResponse_processInterrupted_throws() throws InterruptedException {
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1");
+
+ // Mock executable handling.
+ Process mockProcess = Mockito.mock(Process.class);
+ when(mockProcess.waitFor(anyLong(), any(TimeUnit.class))).thenThrow(new InterruptedException());
+
+ InternalProcessBuilder processBuilder =
+ buildInternalProcessBuilder(
+ new HashMap<>(), mockProcess, DEFAULT_OPTIONS.getExecutableCommand());
+
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder);
+
+ // Call getExecutableResponse().
+ PluggableAuthException e =
+ assertThrows(
+ PluggableAuthException.class, () -> handler.getExecutableResponse(DEFAULT_OPTIONS));
+
+ assertEquals("INTERRUPTED", e.getErrorCode());
+ assertEquals(
+ String.format("The execution was interrupted: %s.", new InterruptedException()),
+ e.getErrorDescription());
+
+ verify(mockProcess, times(1))
+ .waitFor(
+ eq(Long.valueOf(DEFAULT_OPTIONS.getExecutableTimeoutMs())), eq(TimeUnit.MILLISECONDS));
+ verify(mockProcess, times(1)).destroy();
+ }
+
+ @Test
+ void getExecutableResponse_invalidResponse_throws() throws InterruptedException {
+ TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider();
+ environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1");
+
+ // Mock executable handling.
+ Process mockProcess = Mockito.mock(Process.class);
+ when(mockProcess.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(true);
+ when(mockProcess.exitValue()).thenReturn(EXIT_CODE_SUCCESS);
+
+ // Mock bad executable response.
+ String badResponse = "badResponse";
+ when(mockProcess.getInputStream())
+ .thenReturn(new ByteArrayInputStream(badResponse.getBytes(StandardCharsets.UTF_8)));
+
+ InternalProcessBuilder processBuilder =
+ buildInternalProcessBuilder(
+ new HashMap<>(), mockProcess, DEFAULT_OPTIONS.getExecutableCommand());
+
+ PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder);
+
+ // Call getExecutableResponse().
+ PluggableAuthException e =
+ assertThrows(
+ PluggableAuthException.class, () -> handler.getExecutableResponse(DEFAULT_OPTIONS));
+
+ assertEquals("INVALID_RESPONSE", e.getErrorCode());
+ assertEquals(
+ String.format("The executable returned an invalid response: %s.", badResponse),
+ e.getErrorDescription());
+
+ verify(mockProcess, times(1))
+ .waitFor(
+ eq(Long.valueOf(DEFAULT_OPTIONS.getExecutableTimeoutMs())), eq(TimeUnit.MILLISECONDS));
+ verify(mockProcess, times(1)).destroy();
+ }
+
+ private static GenericJson buildOidcResponse() {
+ GenericJson json = new GenericJson();
+ json.setFactory(OAuth2Utils.JSON_FACTORY);
+ json.put("version", EXECUTABLE_SUPPORTED_MAX_VERSION);
+ json.put("success", true);
+ json.put("token_type", TOKEN_TYPE_OIDC);
+ json.put("id_token", ID_TOKEN);
+ json.put("expiration_time", Instant.now().getEpochSecond() + EXPIRATION_DURATION);
+ return json;
+ }
+
+ private static GenericJson buildSamlResponse() {
+ GenericJson json = new GenericJson();
+ json.setFactory(OAuth2Utils.JSON_FACTORY);
+ json.put("version", EXECUTABLE_SUPPORTED_MAX_VERSION);
+ json.put("success", true);
+ json.put("token_type", TOKEN_TYPE_SAML);
+ json.put("saml_response", SAML_RESPONSE);
+ json.put("expiration_time", Instant.now().getEpochSecond() + EXPIRATION_DURATION);
+ return json;
+ }
+
+ private static GenericJson buildErrorResponse() {
+ GenericJson json = new GenericJson();
+ json.setFactory(OAuth2Utils.JSON_FACTORY);
+ json.put("version", EXECUTABLE_SUPPORTED_MAX_VERSION);
+ json.put("success", false);
+ json.put("code", "401");
+ json.put("message", "Caller not authorized.");
+ return json;
+ }
+
+ private static InternalProcessBuilder buildInternalProcessBuilder(
+ Map currentEnv, Process process, String command) {
+ return new InternalProcessBuilder() {
+
+ @Override
+ Map environment() {
+ return currentEnv;
+ }
+
+ @Override
+ InternalProcessBuilder redirectErrorStream(boolean redirectErrorStream) {
+ return this;
+ }
+
+ @Override
+ Process start() {
+ return process;
+ }
+ };
+ }
+}
diff --git a/oauth2_http/pom.xml b/oauth2_http/pom.xml
index 327128666..fea8123fc 100644
--- a/oauth2_http/pom.xml
+++ b/oauth2_http/pom.xml
@@ -134,5 +134,17 @@
1.3test
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ org.mockito
+ mockito-core
+ 2.23.4
+ test
+
diff --git a/pom.xml b/pom.xml
index f68dfb56c..6182ed492 100644
--- a/pom.xml
+++ b/pom.xml
@@ -169,6 +169,7 @@
3.4.07
+ false
@@ -329,6 +330,7 @@
+ falsenone7${project.build.directory}/javadoc
@@ -504,6 +506,7 @@
${sourceFileExclude}
+ false