diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 3ead58e4ca32..638874f99e04 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -8,3 +8,4 @@
# java-vertexai has maintainers
/java-vertexai/ @googleapis/vertexai-team @googleapis/cloud-sdk-java-team
/java-bigquerystorage/ @googleapis/bigquery-team @googleapis/cloud-sdk-java-team
+/google-auth-library-java/ @googleapis/cloud-sdk-auth-team @googleapis/cloud-sdk-java-team
diff --git a/.github/workflows/generated_files_sync.yaml b/.github/workflows/generated_files_sync.yaml
index e1d7998ec923..1abaadb27006 100644
--- a/.github/workflows/generated_files_sync.yaml
+++ b/.github/workflows/generated_files_sync.yaml
@@ -170,6 +170,7 @@ jobs:
# the rest : the same as above
invalid_files=$(find . -name '*.java' \
|grep --invert-match 'java/com/google' \
+ |grep --invert-match 'javatests/com/google' \
|grep --invert-match samples \
|grep --invert-match grafeas \
|grep --invert-match 'cloud-build.*v2' \
diff --git a/.github/workflows/google-auth-library-java-ci.yaml b/.github/workflows/google-auth-library-java-ci.yaml
new file mode 100644
index 000000000000..f2b4d199f30e
--- /dev/null
+++ b/.github/workflows/google-auth-library-java-ci.yaml
@@ -0,0 +1,157 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# GitHub action job to test core java library features on
+# downstream client libraries before they are released.
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+name: google-auth-library-java ci
+env:
+ BUILD_SUBDIR: google-auth-library-java
+jobs:
+ filter:
+ runs-on: ubuntu-latest
+ outputs:
+ library: ${{ steps.filter.outputs.library }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dorny/paths-filter@v3
+ id: filter
+ with:
+ filters: |
+ library:
+ - 'google-auth-library-java/**'
+ units:
+ needs: filter
+ if: ${{ needs.filter.outputs.library == 'true' }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ java: [11, 17, 21, 25]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: ${{matrix.java}}
+ - run: java -version
+ - run: .kokoro/build.sh
+ env:
+ JOB_TYPE: test
+ units-logging:
+ needs: filter
+ if: ${{ needs.filter.outputs.library == 'true' }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ java: [11, 17, 21, 25]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: ${{matrix.java}}
+ - run: java -version
+ - run: .kokoro/build.sh
+ env:
+ JOB_TYPE: test-logging
+ units-java8:
+ needs: filter
+ if: ${{ needs.filter.outputs.library == 'true' }}
+ # Building using Java 17 and run the tests with Java 8 runtime
+ name: "units (8)"
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ java-version: 8
+ distribution: temurin
+ - name: "Set jvm system property environment variable for surefire plugin (unit tests)"
+ # Maven surefire plugin (unit tests) allows us to specify JVM to run the tests.
+ # https://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html#jvm
+ run: echo "SUREFIRE_JVM_OPT=-Djvm=${JAVA_HOME}/bin/java -P !java17" >> $GITHUB_ENV
+ shell: bash
+ - uses: actions/setup-java@v4
+ with:
+ java-version: 17
+ distribution: temurin
+ - run: .kokoro/build.sh
+ env:
+ JOB_TYPE: test
+ windows:
+ needs: filter
+ if: ${{ needs.filter.outputs.library == 'true' }}
+ runs-on: windows-latest
+ steps:
+ - name: Support longpaths
+ run: git config --system core.longpaths true
+ - name: Support longpaths
+ run: git config --system core.longpaths true
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: 8
+ - run: java -version
+ - run: .kokoro/build.sh
+ env:
+ JOB_TYPE: test
+ dependencies:
+ needs: filter
+ if: ${{ needs.filter.outputs.library == 'true' }}
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ java: [17]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: ${{matrix.java}}
+ - run: java -version
+ - run: .kokoro/dependencies.sh
+ javadoc:
+ needs: filter
+ if: ${{ needs.filter.outputs.library == 'true' }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: 17
+ - run: java -version
+ - run: .kokoro/build.sh
+ env:
+ JOB_TYPE: javadoc
+ lint:
+ needs: filter
+ if: ${{ needs.filter.outputs.library == 'true' }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: 17
+ - run: java -version
+ - run: .kokoro/build.sh
+ env:
+ JOB_TYPE: lint
diff --git a/.github/workflows/google-auth-library-java-downstream.yaml b/.github/workflows/google-auth-library-java-downstream.yaml
new file mode 100644
index 000000000000..4b630568dcf4
--- /dev/null
+++ b/.github/workflows/google-auth-library-java-downstream.yaml
@@ -0,0 +1,160 @@
+on:
+ pull_request:
+ types: [ labeled ]
+ branches:
+ - main
+name: google-auth-library-java downstream
+env:
+ BUILD_SUBDIR: google-auth-library-java
+jobs:
+ filter:
+ runs-on: ubuntu-latest
+ outputs:
+ library: ${{ steps.filter.outputs.library }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dorny/paths-filter@v3
+ id: filter
+ with:
+ filters: |
+ library:
+ - 'google-auth-library-java/**'
+ dependencies:
+ needs: filter
+ if: ${{ needs.filter.outputs.library == 'true' }}
+ if: ${{ github.event.label.name == 'downstream-check:run' }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ java: [17]
+ repo:
+ # This list needs to be updated manually until an automated solution is in place.
+ - accessapproval
+ - accesscontextmanager
+ - aiplatform
+ - analytics-admin
+ - analytics-data
+ - api-gateway
+ - apigee-connect
+ - appengine-admin
+ - area120-tables
+ - artifact-registry
+ - asset
+ - assured-workloads
+ - automl
+ - bigquery
+ - bigqueryconnection
+ - bigquerydatatransfer
+ - bigquerymigration
+ - bigqueryreservation
+ - bigtable
+ - billing
+ - billingbudgets
+ - binary-authorization
+ - channel
+ - cloudbuild
+ - compute
+ - contact-center-insights
+ - container
+ - containeranalysis
+ - data-fusion
+ - datacatalog
+ - dataflow
+ - datalabeling
+ - dataproc
+ - dataproc-metastore
+ - datastore
+ - datastream
+ - debugger-client
+ - deploy
+ - dialogflow
+ - dialogflow-cx
+ - dlp
+ - dms
+ - dns
+ - document-ai
+ - domains
+ - errorreporting
+ - essential-contacts
+ - eventarc
+ - filestore
+ - firestore
+ - functions
+ - game-servers
+ - gke-connect-gateway
+ - gkehub
+ - gsuite-addons
+ - iam-admin
+ - iamcredentials
+ - iot
+ - kms
+ - language
+ - life-sciences
+ - logging
+ - logging-logback
+ - managed-identities
+ - mediatranslation
+ - memcache
+ - monitoring
+ - monitoring-dashboards
+ - network-management
+ - network-security
+ - networkconnectivity
+ - notebooks
+ - orchestration-airflow
+ - orgpolicy
+ - os-config
+ - os-login
+ - phishingprotection
+ - policy-troubleshooter
+ - private-catalog
+ - profiler
+ - pubsublite
+ - recaptchaenterprise
+ - recommendations-ai
+ - recommender
+ - redis
+ - resource-settings
+ - resourcemanager
+ - retail
+ - scheduler
+ - secretmanager
+ - security-private-ca
+ - securitycenter
+ - securitycenter-settings
+ - service-control
+ - service-management
+ - service-usage
+ - servicedirectory
+ - shell
+ - spanner
+ - spanner-jdbc
+ - speech
+ - storage
+ - storage-nio
+ - storage-transfer
+ - talent
+ - tasks
+ - texttospeech
+ - tpu
+ - trace
+ - translate
+ - video-intelligence
+ - video-transcoder
+ - vision
+ - vpcaccess
+ - webrisk
+ - websecurityscanner
+ - workflow-executions
+ - workflows
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: zulu
+ java-version: ${{matrix.java}}
+ - run: java -version
+ - run: sudo apt-get update -y
+ - run: sudo apt-get install libxml2-utils
+ - run: .kokoro/downstream-client-library-check.sh google-auth-library-bom ${{matrix.repo}}
diff --git a/.github/workflows/google-auth-library-java-sonar.yaml b/.github/workflows/google-auth-library-java-sonar.yaml
new file mode 100644
index 000000000000..ec8440a8ac08
--- /dev/null
+++ b/.github/workflows/google-auth-library-java-sonar.yaml
@@ -0,0 +1,64 @@
+name: google-auth-library-java SonarCloud
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: [opened, synchronize, reopened]
+env:
+ BUILD_SUBDIR: google-auth-library-java
+jobs:
+ filter:
+ runs-on: ubuntu-latest
+ outputs:
+ library: ${{ steps.filter.outputs.library }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dorny/paths-filter@v3
+ id: filter
+ with:
+ filters: |
+ library:
+ - 'google-auth-library-java/**'
+ build:
+ needs: filter
+ if: ${{ needs.filter.outputs.library == 'true' }}
+ name: Build
+ runs-on: ubuntu-22.04
+ # Sonar Token can't be passed to PRs from forks. Disable Sonar workflow unless PR is from a branch.
+ if: github.event.pull_request.head.repo.full_name == github.repository
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: 17
+ distribution: temurin
+ - name: Cache SonarCloud packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.sonar/cache
+ key: ${{ runner.os }}-sonar
+ restore-keys: ${{ runner.os }}-sonar
+ - name: Cache Maven packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.m2
+ key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+ restore-keys: ${{ runner.os }}-m2
+ - name: Build and analyze for full test coverage
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: |
+ mvn -B verify -Dcheckstyle.skip \
+ -Djacoco.skip=true \
+ -DenableFullTestCoverage \
+ -Dsonar.coverage.jacoco.xmlReportPaths=oauth2_http/target/site/jacoco/jacoco.xml \
+ org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \
+ -Dsonar.projectKey=googleapis_google-auth-library-java \
+ -Dsonar.organization=googleapis \
+ -Dsonar.host.url=https://sonarcloud.io
+
diff --git a/.github/workflows/hermetic_library_generation.yaml b/.github/workflows/hermetic_library_generation.yaml
index 9249538f2818..acd11afe7412 100644
--- a/.github/workflows/hermetic_library_generation.yaml
+++ b/.github/workflows/hermetic_library_generation.yaml
@@ -37,7 +37,7 @@ jobs:
with:
fetch-depth: 0
token: ${{ secrets.CLOUD_JAVA_BOT_GITHUB_TOKEN }}
- - uses: googleapis/sdk-platform-java/.github/scripts@v2.67.0
+ - uses: googleapis/sdk-platform-java/.github/scripts@68296949dac74c61eb803cad0eff39f8d912f0b8
if: env.SHOULD_RUN == 'true'
with:
base_ref: ${{ github.base_ref }}
diff --git a/.kokoro/common.sh b/.kokoro/common.sh
index ba6ca87a3fee..4f6fb1650c18 100644
--- a/.kokoro/common.sh
+++ b/.kokoro/common.sh
@@ -22,6 +22,7 @@ excluded_modules=(
'java-bigquerystorage'
'java-datastore'
'java-logging-logback'
+ 'google-auth-library-java'
)
function retry_with_backoff {
diff --git a/.kokoro/presubmit/google-auth-library-java-graalvm-native-presubmit.cfg b/.kokoro/presubmit/google-auth-library-java-graalvm-native-presubmit.cfg
new file mode 100644
index 000000000000..dacef5d75fa1
--- /dev/null
+++ b/.kokoro/presubmit/google-auth-library-java-graalvm-native-presubmit.cfg
@@ -0,0 +1,54 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+build_file: "google-auth-library-java/.kokoro/build.sh"
+
+env_vars: {
+ key: "JOB_TYPE"
+ value: "graalvm-single"
+}
+
+# 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"
+}
+
+env_vars: {
+ key: "GCS_BUCKET"
+ value: "byoid-it-bucket"
+}
+
+env_vars: {
+ key: "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES"
+ value: "1"
+}
+
+env_vars: {
+ key: "GOOGLE_CLOUD_QUOTA_PROJECT"
+ value: "gcloud-devel"
+}
+
+container_properties {
+ docker_image: "us-docker.pkg.dev/java-graalvm-ci-prod/graalvm-integration-testing/graalvm_a:1.17.0"
+}
+
+
+env_vars: {
+ key: "BUILD_SUBDIR"
+ value: "google-auth-library-java"
+}
diff --git a/.kokoro/presubmit/google-auth-library-java-integration.cfg b/.kokoro/presubmit/google-auth-library-java-integration.cfg
new file mode 100644
index 000000000000..c7910213bf91
--- /dev/null
+++ b/.kokoro/presubmit/google-auth-library-java-integration.cfg
@@ -0,0 +1,53 @@
+# 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/java8"
+}
+
+env_vars: {
+ key: "JOB_TYPE"
+ value: "integration-single"
+}
+
+# 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"
+}
+
+env_vars: {
+ key: "GCS_BUCKET"
+ value: "byoid-it-bucket"
+}
+
+env_vars: {
+ key: "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES"
+ value: "1"
+}
+
+env_vars: {
+ key: "GOOGLE_CLOUD_QUOTA_PROJECT"
+ value: "gcloud-devel"
+}
+
+env_vars: {
+ key: "BUILD_SUBDIR"
+ value: "google-auth-library-java"
+}
diff --git a/.kokoro/presubmit/logging-graalvm-native-presubmit.cfg b/.kokoro/presubmit/logging-graalvm-native-presubmit.cfg
index d16598bb817f..dacef5d75fa1 100644
--- a/.kokoro/presubmit/logging-graalvm-native-presubmit.cfg
+++ b/.kokoro/presubmit/logging-graalvm-native-presubmit.cfg
@@ -1,10 +1,6 @@
# Format: //devtools/kokoro/config/proto/build.proto
-# Configure the docker image for kokoro-trampoline.
-env_vars: {
- key: "TRAMPOLINE_IMAGE"
- value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.56.1"
-}
+build_file: "google-auth-library-java/.kokoro/build.sh"
env_vars: {
key: "JOB_TYPE"
@@ -14,25 +10,45 @@ env_vars: {
# TODO: remove this after we've migrated all tests and scripts
env_vars: {
key: "GCLOUD_PROJECT"
- value: "cloud-java-ci-test"
+ value: "gcloud-devel"
}
env_vars: {
key: "GOOGLE_CLOUD_PROJECT"
- value: "cloud-java-ci-test"
+ value: "gcloud-devel"
}
env_vars: {
key: "GOOGLE_APPLICATION_CREDENTIALS"
- value: "secret_manager/cloud-java-ci-it-service-account"
+ value: "secret_manager/java-it-service-account"
}
env_vars: {
key: "SECRET_MANAGER_KEYS"
- value: "cloud-java-ci-it-service-account, java-bigqueryconnection-samples-secrets"
+ value: "java-it-service-account"
+}
+
+env_vars: {
+ key: "GCS_BUCKET"
+ value: "byoid-it-bucket"
+}
+
+env_vars: {
+ key: "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES"
+ value: "1"
+}
+
+env_vars: {
+ key: "GOOGLE_CLOUD_QUOTA_PROJECT"
+ value: "gcloud-devel"
}
+container_properties {
+ docker_image: "us-docker.pkg.dev/java-graalvm-ci-prod/graalvm-integration-testing/graalvm_a:1.17.0"
+}
+
+
env_vars: {
key: "BUILD_SUBDIR"
- value: "java-logging"
+ value: "google-auth-library-java"
}
diff --git a/.kokoro/presubmit/logging-integration.cfg b/.kokoro/presubmit/logging-integration.cfg
index 30143885f2b4..c7910213bf91 100644
--- a/.kokoro/presubmit/logging-integration.cfg
+++ b/.kokoro/presubmit/logging-integration.cfg
@@ -3,7 +3,7 @@
# Configure the docker image for kokoro-trampoline.
env_vars: {
key: "TRAMPOLINE_IMAGE"
- value: "gcr.io/cloud-devrel-kokoro-resources/java11"
+ value: "gcr.io/cloud-devrel-kokoro-resources/java8"
}
env_vars: {
@@ -14,25 +14,40 @@ env_vars: {
# TODO: remove this after we've migrated all tests and scripts
env_vars: {
key: "GCLOUD_PROJECT"
- value: "cloud-java-ci-test"
+ value: "gcloud-devel"
}
env_vars: {
key: "GOOGLE_CLOUD_PROJECT"
- value: "cloud-java-ci-test"
+ value: "gcloud-devel"
}
env_vars: {
key: "GOOGLE_APPLICATION_CREDENTIALS"
- value: "secret_manager/cloud-java-ci-it-service-account"
+ value: "secret_manager/java-it-service-account"
}
env_vars: {
key: "SECRET_MANAGER_KEYS"
- value: "cloud-java-ci-it-service-account, java-bigqueryconnection-samples-secrets"
+ value: "java-it-service-account"
+}
+
+env_vars: {
+ key: "GCS_BUCKET"
+ value: "byoid-it-bucket"
+}
+
+env_vars: {
+ key: "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES"
+ value: "1"
+}
+
+env_vars: {
+ key: "GOOGLE_CLOUD_QUOTA_PROJECT"
+ value: "gcloud-devel"
}
env_vars: {
key: "BUILD_SUBDIR"
- value: "java-logging"
+ value: "google-auth-library-java"
}
diff --git a/generation/check_non_release_please_versions.sh b/generation/check_non_release_please_versions.sh
index 4bc05a534b7b..facc7434c40a 100755
--- a/generation/check_non_release_please_versions.sh
+++ b/generation/check_non_release_please_versions.sh
@@ -11,6 +11,7 @@ for pomFile in $(find . -mindepth 2 -name pom.xml | sort ); do
[[ "${pomFile}" =~ .*java-bigquerystorage.* ]] || \
[[ "${pomFile}" =~ .*java-datastore.* ]] || \
[[ "${pomFile}" =~ .*java-logging-logback.* ]] || \
+ [[ "${pomFile}" =~ .*google-auth-library-java.* ]] || \
[[ "${pomFile}" =~ .*.github*. ]]; then
continue
fi
diff --git a/google-auth-library-java/.release-please-manifest.json b/google-auth-library-java/.release-please-manifest.json
new file mode 100644
index 000000000000..b5fcdb93a43a
--- /dev/null
+++ b/google-auth-library-java/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+ ".": "1.43.0"
+}
\ No newline at end of file
diff --git a/google-auth-library-java/.repo-metadata.json b/google-auth-library-java/.repo-metadata.json
new file mode 100644
index 000000000000..1aa441105b27
--- /dev/null
+++ b/google-auth-library-java/.repo-metadata.json
@@ -0,0 +1,12 @@
+{
+ "api_shortname": "google-auth-library",
+ "name_pretty": "Google Auth Library",
+ "client_documentation": "https://googleapis.dev/java/google-auth-library/latest/",
+ "release_level": "stable",
+ "language": "java",
+ "repo": "googleapis/google-cloud-java",
+ "repo_short": "google-cloud-java",
+ "library_type": "AUTH",
+ "distribution_name": "com.google.auth:google-auth-library",
+ "codeowner_team": "@googleapis/cloud-sdk-auth-team"
+}
diff --git a/google-auth-library-java/CHANGELOG.md b/google-auth-library-java/CHANGELOG.md
new file mode 100644
index 000000000000..da23d922f69d
--- /dev/null
+++ b/google-auth-library-java/CHANGELOG.md
@@ -0,0 +1,1274 @@
+# Changelog
+
+## [1.43.0](https://github.com/googleapis/google-auth-library-java/compare/v1.42.1...v1.43.0) (2026-02-10)
+
+
+### Features
+
+* Next release from main branch is 1.43.0 ([#1887](https://github.com/googleapis/google-auth-library-java/issues/1887)) ([bec0ece](https://github.com/googleapis/google-auth-library-java/commit/bec0ecea8d1064a3467c4329a0d09f7d5705f84d))
+
+
+### Bug Fixes
+
+* Deserialization checks valid class types for HttpTransportFactory ([#1882](https://github.com/googleapis/google-auth-library-java/issues/1882)) ([76ff74e](https://github.com/googleapis/google-auth-library-java/commit/76ff74e4c810d54763ca34d4f483730c43c329a8))
+
+## [1.42.1](https://github.com/googleapis/google-auth-library-java/compare/v1.42.0...v1.42.1) (2026-01-23)
+
+
+### Bug Fixes
+
+* Mark GdchCredentialsTestUtil test util class as public ([#1877](https://github.com/googleapis/google-auth-library-java/issues/1877)) ([1868969](https://github.com/googleapis/google-auth-library-java/commit/1868969b2701fe0faa4511c36443666c3aaad534))
+
+## [1.42.0](https://github.com/googleapis/google-auth-library-java/compare/v1.41.0...v1.42.0) (2026-01-23)
+
+
+### Features
+
+* Update protobuf version to 4.33.2 ([#1875](https://github.com/googleapis/google-auth-library-java/issues/1875)) ([13ddbd1](https://github.com/googleapis/google-auth-library-java/commit/13ddbd1744fb908fb51e8866e5aac291f0e9bada))
+
+
+### Bug Fixes
+
+* Simplify call to directly retrieve the default service account from MDS ([#1844](https://github.com/googleapis/google-auth-library-java/issues/1844)) ([6efda0b](https://github.com/googleapis/google-auth-library-java/commit/6efda0bc2063b1d1b30de43785d08ec86da1791c))
+
+## [1.41.0](https://github.com/googleapis/google-auth-library-java/compare/v1.40.0...v1.41.0) (2025-12-03)
+
+
+### Features
+
+* Introduce per-credential specific load methods ([#1827](https://github.com/googleapis/google-auth-library-java/issues/1827)) ([39fdc64](https://github.com/googleapis/google-auth-library-java/commit/39fdc647c2e14d8006a758fa81dbaeff63fed74e))
+
+
+### Bug Fixes
+
+* Add configurable connect and read timeouts for IdentityPoolCredentials requests ([#1847](https://github.com/googleapis/google-auth-library-java/issues/1847)) ([d6cff60](https://github.com/googleapis/google-auth-library-java/commit/d6cff6048b506896fc960d99fbb4a00e03f23b62))
+* Do not retrieve the default SA principal when getting an access token ([#1839](https://github.com/googleapis/google-auth-library-java/issues/1839)) ([a65c22d](https://github.com/googleapis/google-auth-library-java/commit/a65c22da2c93bdf33dcd98ece47ee6668d1ed32c))
+
+## [1.40.0](https://github.com/googleapis/google-auth-library-java/compare/v1.39.1...v1.40.0) (2025-10-14)
+
+
+### Features
+
+* Add projectId getter to GoogleCredentials ([#1813](https://github.com/googleapis/google-auth-library-java/issues/1813)) ([c3d9ee0](https://github.com/googleapis/google-auth-library-java/commit/c3d9ee09df30a26586b0e834cfda7763fd7854f5))
+* Support user defined or json defined scopes for impersonated token ([#1815](https://github.com/googleapis/google-auth-library-java/issues/1815)) ([84fc566](https://github.com/googleapis/google-auth-library-java/commit/84fc566d92f03e648cd82a8860fb191520bb6c7e))
+
+
+### Dependencies
+
+* Bump guava to v33.5.0 ([#1825](https://github.com/googleapis/google-auth-library-java/issues/1825)) ([79f0a35](https://github.com/googleapis/google-auth-library-java/commit/79f0a353e12a8206584e9031989861ca6abaaf85))
+
+## [1.39.1](https://github.com/googleapis/google-auth-library-java/compare/v1.39.0...v1.39.1) (2025-09-17)
+
+
+### Documentation
+
+* Additional information for deprecated fromStream() methods. ([#1802](https://github.com/googleapis/google-auth-library-java/issues/1802)) ([a0d873d](https://github.com/googleapis/google-auth-library-java/commit/a0d873db6bf192aad710e17da6127195f253c7e7))
+
+## [1.39.0](https://github.com/googleapis/google-auth-library-java/compare/v1.38.0...v1.39.0) (2025-09-04)
+
+
+### Features
+
+* Add Credential Information to GoogleCredential classes ([#1791](https://github.com/googleapis/google-auth-library-java/issues/1791)) ([5511913](https://github.com/googleapis/google-auth-library-java/commit/551191340c97497db991ff65994cbb0086490d2a))
+
+
+### Bug Fixes
+
+* Indicate non-validated external credentials in generic methods ([e7d4380](https://github.com/googleapis/google-auth-library-java/commit/e7d4380ce94bbdd5a879591e0652945262e896aa))
+
+
+### Dependencies
+
+* Add `com.google.api:api-commons` dependency ([e7d4380](https://github.com/googleapis/google-auth-library-java/commit/e7d4380ce94bbdd5a879591e0652945262e896aa))
+* Update `com.google.errorprone:error_prone_annotations` dependency to 2.38.0 ([e7d4380](https://github.com/googleapis/google-auth-library-java/commit/e7d4380ce94bbdd5a879591e0652945262e896aa))
+
+## [1.38.0](https://github.com/googleapis/google-auth-library-java/compare/v1.37.1...v1.38.0) (2025-08-20)
+
+
+### Features
+
+* Next release from main branch is 1.38.0 ([#1786](https://github.com/googleapis/google-auth-library-java/issues/1786)) ([1669dc8](https://github.com/googleapis/google-auth-library-java/commit/1669dc8b7d23fd7085ea7085b2bb5507a6932920))
+
+
+### Bug Fixes
+
+* Override toBuilder() for ExternalAccountCredential and subclasses ([#1793](https://github.com/googleapis/google-auth-library-java/issues/1793)) ([a9c3de6](https://github.com/googleapis/google-auth-library-java/commit/a9c3de60a078fd93f1922b4dbcf7570af20150f0))
+
+
+### Documentation
+
+* Update README with X.509 feature details ([#1790](https://github.com/googleapis/google-auth-library-java/issues/1790)) ([7b51cb3](https://github.com/googleapis/google-auth-library-java/commit/7b51cb3042f278c60ca3d90555f7ebb93e6e8250))
+
+## [1.37.1](https://github.com/googleapis/google-auth-library-java/compare/v1.37.0...v1.37.1) (2025-06-05)
+
+
+### Bug Fixes
+
+* Correct typo in minExpirationTime variable name ([#1769](https://github.com/googleapis/google-auth-library-java/issues/1769)) ([91e6274](https://github.com/googleapis/google-auth-library-java/commit/91e6274828310e614802ad948ccfc240aebc0873))
+
+## [1.37.0](https://github.com/googleapis/google-auth-library-java/compare/v1.36.0...v1.37.0) (2025-06-04)
+
+
+### Features
+
+* **mtls:** Introduce DefaultMtlsProviderFactory and SecureConnectProvider ([#1730](https://github.com/googleapis/google-auth-library-java/issues/1730)) ([c9fd1b1](https://github.com/googleapis/google-auth-library-java/commit/c9fd1b1a477329ae336accd151a57795a0c83955))
+
+
+### Bug Fixes
+
+* Correct capitalization of GitHub ([#1761](https://github.com/googleapis/google-auth-library-java/issues/1761)) ([f79a2e4](https://github.com/googleapis/google-auth-library-java/commit/f79a2e406ef1128f9a222155cad2effc1e69a331))
+* Correct extra spaces in README heading ([#1760](https://github.com/googleapis/google-auth-library-java/issues/1760)) ([8d26666](https://github.com/googleapis/google-auth-library-java/commit/8d266669c1fcc007f35948052dc7cb7c53c1a639))
+* Correct misspelling of OAuth in comments ([#1762](https://github.com/googleapis/google-auth-library-java/issues/1762)) ([42b9602](https://github.com/googleapis/google-auth-library-java/commit/42b9602886b00b0090e519c79cfc96d9b876ffeb))
+* Correct typo in ServiceAccountJwtAccessCredentials.java comment ([#1765](https://github.com/googleapis/google-auth-library-java/issues/1765)) ([3058b06](https://github.com/googleapis/google-auth-library-java/commit/3058b069e474fb06d16926c9313ca1f931934a11))
+* Update Javadoc reference in ExternalAccountCredentials ([#1763](https://github.com/googleapis/google-auth-library-java/issues/1763)) ([5eb3659](https://github.com/googleapis/google-auth-library-java/commit/5eb3659c131969e674ea1bb4b84698202befbc9b))
+
+
+### Documentation
+
+* Duplicate "the" in Javadoc comments ([#1764](https://github.com/googleapis/google-auth-library-java/issues/1764)) ([5f7a084](https://github.com/googleapis/google-auth-library-java/commit/5f7a0841b32c5e03ca1bbf49a7e612725062311b))
+
+## [1.36.0](https://github.com/googleapis/google-auth-library-java/compare/v1.35.0...v1.36.0) (2025-05-28)
+
+
+### Features
+
+* Support ability to set universe domain in ServiceAccountJwtAccessCredentials ([#1754](https://github.com/googleapis/google-auth-library-java/issues/1754)) ([919ae32](https://github.com/googleapis/google-auth-library-java/commit/919ae320bf5a4f06fd39871bf055b8487ed55d71))
+
+
+### Bug Fixes
+
+* IdTokenCredentials should fetch license id claim when requested ([#1450](https://github.com/googleapis/google-auth-library-java/issues/1450)) ([c5648a5](https://github.com/googleapis/google-auth-library-java/commit/c5648a55f15a75b1d616fbdc37cd331811e66f3a))
+* Update approval_prompt=force to prompt=consent ([#1752](https://github.com/googleapis/google-auth-library-java/issues/1752)) ([4543d04](https://github.com/googleapis/google-auth-library-java/commit/4543d0423775f1e568249eb782b88dc0e6d64a59))
+
+## [1.35.0](https://github.com/googleapis/google-auth-library-java/compare/v1.34.0...v1.35.0) (2025-05-12)
+
+
+### Features
+
+* Add support for mTLS authentication via X.509 certificates ([#1736](https://github.com/googleapis/google-auth-library-java/issues/1736)) ([b347603](https://github.com/googleapis/google-auth-library-java/commit/b347603db4deabb4eb34ed14b96178c95d3e7b45))
+* Return X509 certificate chain as the subject token. ([#1746](https://github.com/googleapis/google-auth-library-java/issues/1746)) ([6d05be8](https://github.com/googleapis/google-auth-library-java/commit/6d05be8e5cecf62ca0952bc3ef23c527c9e0d01d))
+
+
+### Bug Fixes
+
+* Handle optional fields in ExternalAccountCredentials with null JSON value gracefully ([#1706](https://github.com/googleapis/google-auth-library-java/issues/1706)) ([f1f306d](https://github.com/googleapis/google-auth-library-java/commit/f1f306dffd874741663238283deed173ce02bea9))
+
+## [1.34.0](https://github.com/googleapis/google-auth-library-java/compare/v1.33.1...v1.34.0) (2025-04-29)
+
+
+### Features
+
+* Implement X509 certificate provider ([#1722](https://github.com/googleapis/google-auth-library-java/issues/1722)) ([4340684](https://github.com/googleapis/google-auth-library-java/commit/4340684fe29c9e9bffa90e88d0b1746f19b623ab))
+* Next release from main branch is 1.34.0 ([#1698](https://github.com/googleapis/google-auth-library-java/issues/1698)) ([fe43815](https://github.com/googleapis/google-auth-library-java/commit/fe4381513db1340190c4309a53c6265718682dde))
+* Next release from main branch is 1.34.0 ([#1702](https://github.com/googleapis/google-auth-library-java/issues/1702)) ([4507cf9](https://github.com/googleapis/google-auth-library-java/commit/4507cf9e17e7ff40cf142056d3929c87f5742dd1))
+
+
+### Bug Fixes
+
+* Do not add padding in Client-Side CAB tokens. ([#1728](https://github.com/googleapis/google-auth-library-java/issues/1728)) ([8a75ccd](https://github.com/googleapis/google-auth-library-java/commit/8a75ccd1c09191abd8ebf463bc41810a38e185f5))
+
+## [1.33.1](https://github.com/googleapis/google-auth-library-java/compare/v1.33.0...v1.33.1) (2025-02-25)
+
+
+### Dependencies
+
+* Update dependency com.google.cloud:google-cloud-shared-config to v1.14.4 ([53a2abc](https://github.com/googleapis/google-auth-library-java/commit/53a2abc5b19e25079113ebff501aebc18efca309))
+
+## [1.33.0](https://github.com/googleapis/google-auth-library-java/compare/v1.32.1...v1.33.0) (2025-02-24)
+
+
+### Features
+
+* Add client logging with slf4j ([#1586](https://github.com/googleapis/google-auth-library-java/issues/1586)) ([24761d6](https://github.com/googleapis/google-auth-library-java/commit/24761d6cc3590c4b08c56c8c992b740e235b31c5))
+
+
+### Dependencies
+
+* Update dependency com.google.http-client:google-http-client-bom to v1.46.1 ([96a5ad8](https://github.com/googleapis/google-auth-library-java/commit/96a5ad88a7b187e1a0d472dca06ff39d74804d61))
+
+## [1.32.1](https://github.com/googleapis/google-auth-library-java/compare/v1.32.0...v1.32.1) (2025-02-07)
+
+
+### Bug Fixes
+
+* Add cab-token-generator module to Auth BOM ([#1662](https://github.com/googleapis/google-auth-library-java/issues/1662)) ([e409b02](https://github.com/googleapis/google-auth-library-java/commit/e409b02b124619ffd6af95890c6ce340b204554a))
+* Remove unnecessary nexus-staging-maven-plugin activation ([#1665](https://github.com/googleapis/google-auth-library-java/issues/1665)) ([d138023](https://github.com/googleapis/google-auth-library-java/commit/d138023aae55abb7b36d2bef6b21cd00a2ec4511))
+
+
+### Dependencies
+
+* Update dependency com.google.http-client:google-http-client-bom to v1.46.0 ([e53c441](https://github.com/googleapis/google-auth-library-java/commit/e53c4415f472594f56c53e92d302f745b96c4fba))
+
+
+### Documentation
+
+* Update README with client-side CAB instructions ([#1607](https://github.com/googleapis/google-auth-library-java/issues/1607)) ([#1666](https://github.com/googleapis/google-auth-library-java/issues/1666)) ([2996297](https://github.com/googleapis/google-auth-library-java/commit/2996297f54823c43a2bb7c96a634013a79be6fd4))
+
+## [1.32.0](https://github.com/googleapis/google-auth-library-java/compare/v1.31.0...v1.32.0) (2025-02-04)
+
+
+### Features
+
+* Introduce Client-Side Credential Access Boundary (CAB) functionality ([#1629](https://github.com/googleapis/google-auth-library-java/issues/1629)) ([f481123](https://github.com/googleapis/google-auth-library-java/commit/f4811236018502595987eea8ce5f3fa1c7fdbfaf))
+
+
+### Bug Fixes
+
+* Handle 404 and non 200 Status Code from MDS Identity Token calls ([#1636](https://github.com/googleapis/google-auth-library-java/issues/1636)) ([152c851](https://github.com/googleapis/google-auth-library-java/commit/152c851bfb90196437f268a6975e66a89985444b))
+* Respect token_uri from json in UserCredentials creation. ([#1630](https://github.com/googleapis/google-auth-library-java/issues/1630)) ([f92cc4f](https://github.com/googleapis/google-auth-library-java/commit/f92cc4faf46ab6b0b2b5659fdbbd4c83c1c2f0fe))
+
+
+### Documentation
+
+* Re-organize the README + Add a section on migrating to GoogleCredentials ([#1644](https://github.com/googleapis/google-auth-library-java/issues/1644)) ([30b26b2](https://github.com/googleapis/google-auth-library-java/commit/30b26b280268530eb46fb85baa1ca808245e8d26))
+
+## [1.31.0](https://github.com/googleapis/google-auth-library-java/compare/v1.30.1...v1.31.0) (2025-01-22)
+
+
+### Features
+
+* ImpersonatedCredentials to support universe domain for idtoken and signblob ([#1566](https://github.com/googleapis/google-auth-library-java/issues/1566)) ([adc2ff3](https://github.com/googleapis/google-auth-library-java/commit/adc2ff3dcabb79e367d0d66b5b3fd8a51e35bc2b))
+* Support transport and binding-enforcement MDS parameters. ([#1558](https://github.com/googleapis/google-auth-library-java/issues/1558)) ([9828a8e](https://github.com/googleapis/google-auth-library-java/commit/9828a8eeb9f144f7c341df0c03282a8790356962))
+
+
+### Documentation
+
+* Promote use of bill of materials in quickstart documentation ([#1620](https://github.com/googleapis/google-auth-library-java/issues/1620)) ([fc20d9c](https://github.com/googleapis/google-auth-library-java/commit/fc20d9c9d33b7eada964cf41297f8a3e13c27fe1)), closes [#1552](https://github.com/googleapis/google-auth-library-java/issues/1552)
+
+## [1.30.1](https://github.com/googleapis/google-auth-library-java/compare/v1.30.0...v1.30.1) (2024-12-11)
+
+
+### Bug Fixes
+
+* JSON parsing of S2A addresses. ([#1589](https://github.com/googleapis/google-auth-library-java/issues/1589)) ([9d5ebfe](https://github.com/googleapis/google-auth-library-java/commit/9d5ebfe8870a11d27af3a7c7f3fd9930ab207162))
+
+## [1.30.0](https://github.com/googleapis/google-auth-library-java/compare/v1.29.0...v1.30.0) (2024-11-08)
+
+
+### Features
+
+* Support querying S2A Addresses from MDS ([#1400](https://github.com/googleapis/google-auth-library-java/issues/1400)) ([df06bd1](https://github.com/googleapis/google-auth-library-java/commit/df06bd1f94d03c4f8807c2adf42d25d29b731531))
+
+
+### Bug Fixes
+
+* Make it explicit that there is a network call to MDS to get SecureSessionAgentConfig ([#1573](https://github.com/googleapis/google-auth-library-java/issues/1573)) ([18020fe](https://github.com/googleapis/google-auth-library-java/commit/18020fedb855742ee27b6558f5de58d3818c6b48))
+
+## [1.29.0](https://github.com/googleapis/google-auth-library-java/compare/v1.28.0...v1.29.0) (2024-10-22)
+
+
+### Features
+
+* Service sccount to service account impersonation to support universe domain ([#1528](https://github.com/googleapis/google-auth-library-java/issues/1528)) ([c498ccf](https://github.com/googleapis/google-auth-library-java/commit/c498ccf67755c6ec619cb37962c2c86ae3ec9d4c))
+
+
+### Bug Fixes
+
+* Make some enum fields final ([#1526](https://github.com/googleapis/google-auth-library-java/issues/1526)) ([8920155](https://github.com/googleapis/google-auth-library-java/commit/89201558db913d9a71b3acccbab8eb0045ada6de))
+
+## [1.28.0](https://github.com/googleapis/google-auth-library-java/compare/v1.27.0...v1.28.0) (2024-10-02)
+
+
+### Features
+
+* Add metric headers ([#1503](https://github.com/googleapis/google-auth-library-java/issues/1503)) ([7f0c1d3](https://github.com/googleapis/google-auth-library-java/commit/7f0c1d31176f9e634fac3b2c6b06f880a51b5fa6))
+
+## [1.27.0](https://github.com/googleapis/google-auth-library-java/compare/v1.26.0...v1.27.0) (2024-09-20)
+
+
+### Features
+
+* Add api key credential as client library authorization type ([#1483](https://github.com/googleapis/google-auth-library-java/issues/1483)) ([6401e51](https://github.com/googleapis/google-auth-library-java/commit/6401e51c04fa6bd819e8dff98a62b7f079608a43))
+
+## [1.26.0](https://github.com/googleapis/google-auth-library-java/compare/v1.25.0...v1.26.0) (2024-09-18)
+
+
+### Features
+
+* Updates UserAuthorizer to support retrieving token response directly with different client auth types ([#1486](https://github.com/googleapis/google-auth-library-java/issues/1486)) ([1651006](https://github.com/googleapis/google-auth-library-java/commit/16510064e861868f649b6bc8fdc54b8a39890812))
+
+## [1.25.0](https://github.com/googleapis/google-auth-library-java/compare/v1.24.1...v1.25.0) (2024-09-03)
+
+
+### Features
+
+* Support retrieving ID Token from IAM endpoint for ServiceAccountCredentials ([#1433](https://github.com/googleapis/google-auth-library-java/issues/1433)) ([4fcf83e](https://github.com/googleapis/google-auth-library-java/commit/4fcf83e0f96de0e6323b85b9a47119a257b37e90))
+
+
+### Bug Fixes
+
+* ComputeEngineCredentials.createScoped should invalidate existing AccessToken ([#1428](https://github.com/googleapis/google-auth-library-java/issues/1428)) ([079a065](https://github.com/googleapis/google-auth-library-java/commit/079a06563114e359b74694b78aec687601a2f628))
+* Invalidate the SA's AccessToken when createScoped() is called ([#1489](https://github.com/googleapis/google-auth-library-java/issues/1489)) ([f26fee7](https://github.com/googleapis/google-auth-library-java/commit/f26fee78d69fce1aaa00dbd5548f3e0266ee6441))
+
+## [1.24.1](https://github.com/googleapis/google-auth-library-java/compare/v1.24.0...v1.24.1) (2024-08-13)
+
+
+### Bug Fixes
+
+* Retry sign blob call with exponential backoff ([#1452](https://github.com/googleapis/google-auth-library-java/issues/1452)) ([d42f30a](https://github.com/googleapis/google-auth-library-java/commit/d42f30acae7c7bd81afbecbfa83ebde5c6db931a))
+
+## [1.24.0](https://github.com/googleapis/google-auth-library-java/compare/v1.23.0...v1.24.0) (2024-07-09)
+
+
+### Features
+
+* [java] allow passing libraries_bom_version from env ([#1967](https://github.com/googleapis/google-auth-library-java/issues/1967)) ([#1407](https://github.com/googleapis/google-auth-library-java/issues/1407)) ([d92b421](https://github.com/googleapis/google-auth-library-java/commit/d92b421c8fa9c22dda47b49f5ebec7f6ac2658a9))
+* Next release from main branch is 1.21.0 ([#1372](https://github.com/googleapis/google-auth-library-java/issues/1372)) ([23c3cbe](https://github.com/googleapis/google-auth-library-java/commit/23c3cbe70fdce49a3075e15ba965739704a87ace))
+
+
+### Bug Fixes
+
+* Makes default token url universe aware ([#1383](https://github.com/googleapis/google-auth-library-java/issues/1383)) ([e3caf05](https://github.com/googleapis/google-auth-library-java/commit/e3caf05831011dc05d3a8b01ebf79305eda70183))
+* Remove Base64 padding in DefaultPKCEProvider ([#1375](https://github.com/googleapis/google-auth-library-java/issues/1375)) ([1405378](https://github.com/googleapis/google-auth-library-java/commit/1405378b05469841a3683bc914f47b92437abcfc))
+
+
+### Documentation
+
+* Add supplier sections to table of contents ([#1371](https://github.com/googleapis/google-auth-library-java/issues/1371)) ([9e11763](https://github.com/googleapis/google-auth-library-java/commit/9e11763e79127b3691533488482575adef6f73d2))
+* Adds docs for supplier based external account credentials ([#1362](https://github.com/googleapis/google-auth-library-java/issues/1362)) ([bd898c6](https://github.com/googleapis/google-auth-library-java/commit/bd898c64875a87414f84ca0787ba6c140e05921b))
+* Fix readme documentation for workload custom suppliers. ([#1382](https://github.com/googleapis/google-auth-library-java/issues/1382)) ([75bd749](https://github.com/googleapis/google-auth-library-java/commit/75bd749985e2d507dc48863408067950fcda3ef1))
+
+## [1.23.0](https://github.com/googleapis/google-auth-library-java/compare/v1.22.0...v1.23.0) (2024-02-05)
+
+
+### Features
+
+* Add context object to pass to supplier functions ([#1363](https://github.com/googleapis/google-auth-library-java/issues/1363)) ([1d9efc7](https://github.com/googleapis/google-auth-library-java/commit/1d9efc78aa6ab24fc2aab5f081240a815c394c95))
+* Adds support for user defined subject token suppliers in AWSCredentials and IdentityPoolCredentials ([#1336](https://github.com/googleapis/google-auth-library-java/issues/1336)) ([64ce8a1](https://github.com/googleapis/google-auth-library-java/commit/64ce8a1fbb82cb19e17ca0c6713c7c187078c28b))
+* Adds universe domain for DownscopedCredentials and ExternalAccountAuthorizedUserCredentials ([#1355](https://github.com/googleapis/google-auth-library-java/issues/1355)) ([17ef707](https://github.com/googleapis/google-auth-library-java/commit/17ef70748aae4820f10694ae99c82ed7ca89dbce))
+* Modify the refresh window to match go/async-token-refresh. Serverless tokens are cached until 4 minutes before expiration, so 4 minutes is the ideal refresh window. ([#1352](https://github.com/googleapis/google-auth-library-java/issues/1352)) ([a7a8d7a](https://github.com/googleapis/google-auth-library-java/commit/a7a8d7a4102b0b7c1b83791947ccb662f060eca7))
+
+
+### Bug Fixes
+
+* Add missing copyright header ([#1364](https://github.com/googleapis/google-auth-library-java/issues/1364)) ([a24e563](https://github.com/googleapis/google-auth-library-java/commit/a24e5631b8198d988a7b82deab5453e43917b0d2))
+* Issue [#1347](https://github.com/googleapis/google-auth-library-java/issues/1347): ExternalAccountCredentials serialization is broken ([#1358](https://github.com/googleapis/google-auth-library-java/issues/1358)) ([e3a2e9c](https://github.com/googleapis/google-auth-library-java/commit/e3a2e9cbdd767c4664d895f98f69d8b742d645f0))
+* Refactor compute and cloudshell credentials to pass quota project to base class ([#1284](https://github.com/googleapis/google-auth-library-java/issues/1284)) ([fb75239](https://github.com/googleapis/google-auth-library-java/commit/fb75239ead37b6677a392f38ea2ef2012b3f21e0))
+
+## [1.22.0](https://github.com/googleapis/google-auth-library-java/compare/v1.21.0...v1.22.0) (2024-01-09)
+
+
+### Features
+
+* Adds universe domain support for compute credentials ([#1346](https://github.com/googleapis/google-auth-library-java/issues/1346)) ([7e26861](https://github.com/googleapis/google-auth-library-java/commit/7e268611d2c2152e84702b1c67ca846902bbe2d5))
+
+
+### Bug Fixes
+
+* Handle error-prone warnings ([#1334](https://github.com/googleapis/google-auth-library-java/issues/1334)) ([927cad8](https://github.com/googleapis/google-auth-library-java/commit/927cad835567cd6619ca51c97546831b0f13edec))
+
+## [1.21.0](https://github.com/googleapis/google-auth-library-java/compare/v1.20.0...v1.21.0) (2023-12-21)
+
+
+### Features
+
+* Add code sample and test for getting an access token from an impersonated SA ([#1289](https://github.com/googleapis/google-auth-library-java/issues/1289)) ([826ee40](https://github.com/googleapis/google-auth-library-java/commit/826ee4007d3e0600dfdf42383f56dbcf6cdd4cec))
+* Multi universe support, adding universe_domain field ([#1282](https://github.com/googleapis/google-auth-library-java/issues/1282)) ([7eb322e](https://github.com/googleapis/google-auth-library-java/commit/7eb322e3af6bce85774b2a1051242a4b62b53963))
+
+
+### Bug Fixes
+
+* Remove -Xlint:unchecked, suppress all existing violations, add @CanIgnoreReturnValue ([#1324](https://github.com/googleapis/google-auth-library-java/issues/1324)) ([04dfd40](https://github.com/googleapis/google-auth-library-java/commit/04dfd40c57b89c2d55327d5ea08036d749ebac02))
+
+
+### Documentation
+
+* Update README.md to link to Cloud authentication documentation rather than AIPs ([98fc7e1](https://github.com/googleapis/google-auth-library-java/commit/98fc7e1f2f551d59811de63eaef0df6bf8e21c2c))
+
+## [1.20.0](https://github.com/googleapis/google-auth-library-java/compare/v1.19.0...v1.20.0) (2023-09-19)
+
+
+### Features
+
+* Byoid metrics framework ([#1232](https://github.com/googleapis/google-auth-library-java/issues/1232)) ([38bdf60](https://github.com/googleapis/google-auth-library-java/commit/38bdf60189b44171f5d481fa934f4ece60553653))
+
+
+### Bug Fixes
+
+* Make derived classes of CredentialSource public ([#1236](https://github.com/googleapis/google-auth-library-java/issues/1236)) ([9bb9e0a](https://github.com/googleapis/google-auth-library-java/commit/9bb9e0a67c503415a69f35e390f6c64357fc7be1))
+
+
+### Documentation
+
+* Update library definitions in README to the latest version ([#1239](https://github.com/googleapis/google-auth-library-java/issues/1239)) ([0c5cff2](https://github.com/googleapis/google-auth-library-java/commit/0c5cff26fc66ad90d2dbccd374c6ead81f66d569))
+
+## [1.19.0](https://github.com/googleapis/google-auth-library-java/compare/v1.18.0...v1.19.0) (2023-06-27)
+
+
+### Features
+
+* Expose test-jar and mock classes in oauth2 ([12e8db6](https://github.com/googleapis/google-auth-library-java/commit/12e8db6025e0263b801d5385844924a4f5ff7b7e))
+
+## [1.18.0](https://github.com/googleapis/google-auth-library-java/compare/v1.17.1...v1.18.0) (2023-06-16)
+
+
+### Features
+
+* Introduce a way to pass additional parameters to auhtorization url ([#1134](https://github.com/googleapis/google-auth-library-java/issues/1134)) ([3a2c5d3](https://github.com/googleapis/google-auth-library-java/commit/3a2c5d3d1abf23bce0af7f958240b5f9ee9d1bf8))
+
+## [1.17.1](https://github.com/googleapis/google-auth-library-java/compare/v1.17.0...v1.17.1) (2023-05-25)
+
+
+### Dependencies
+
+* Update doclet version to v1.9.0 ([#1211](https://github.com/googleapis/google-auth-library-java/issues/1211)) ([8b6e28e](https://github.com/googleapis/google-auth-library-java/commit/8b6e28e00aa609edefceafbb4f2c1dbc10afd6f9))
+
+## [1.17.0](https://github.com/googleapis/google-auth-library-java/compare/v1.16.1...v1.17.0) (2023-05-20)
+
+
+### Features
+
+* Adds universe_domain to external account creds ([#1199](https://github.com/googleapis/google-auth-library-java/issues/1199)) ([608ee87](https://github.com/googleapis/google-auth-library-java/commit/608ee87c92b3e6c355541b50e39387b03deebdf8))
+* Expose method to manually obtain ADC from gcloud CLI well-known… ([#1188](https://github.com/googleapis/google-auth-library-java/issues/1188)) ([2fa9d52](https://github.com/googleapis/google-auth-library-java/commit/2fa9d5211569f802948ed2d1aaf13f7d37f8409c))
+* Updating readme for external account authorized user credentials ([#1200](https://github.com/googleapis/google-auth-library-java/issues/1200)) ([bf25574](https://github.com/googleapis/google-auth-library-java/commit/bf255749b7b403cc5f7538f6e901d9089f529fca))
+
+
+### Bug Fixes
+
+* Do not expose universe_domain yet ([#1206](https://github.com/googleapis/google-auth-library-java/issues/1206)) ([9cce49c](https://github.com/googleapis/google-auth-library-java/commit/9cce49cbba26892e573629b4d11a375eb6ec28fc))
+* Improve errors and warnings related to ADC ([#1172](https://github.com/googleapis/google-auth-library-java/issues/1172)) ([6d2251c](https://github.com/googleapis/google-auth-library-java/commit/6d2251cd8e87b018a65a9296bb5c10f487b304cb))
+* Marking 503 as retryable for Compute credentials ([#1205](https://github.com/googleapis/google-auth-library-java/issues/1205)) ([8ea9445](https://github.com/googleapis/google-auth-library-java/commit/8ea9445a3b738e74c6fc0b59f593b32ef0df5314))
+
+## [1.16.1](https://github.com/googleapis/google-auth-library-java/compare/v1.16.0...v1.16.1) (2023-04-07)
+
+
+### Bug Fixes
+
+* Make supporting classes of AwsCredentials serializable ([#1113](https://github.com/googleapis/google-auth-library-java/issues/1113)) ([82bf871](https://github.com/googleapis/google-auth-library-java/commit/82bf871125b8473677a499c979ab9a843972c930))
+* Remove AWS credential source validation. ([#1177](https://github.com/googleapis/google-auth-library-java/issues/1177)) ([77a99c9](https://github.com/googleapis/google-auth-library-java/commit/77a99c9cfab3c1ce2db50c92e89fc292efaeb3ab))
+
+## [1.16.0](https://github.com/googleapis/google-auth-library-java/compare/v1.15.0...v1.16.0) (2023-02-15)
+
+
+### Features
+
+* Add PKCE to 3LO exchange. ([#1146](https://github.com/googleapis/google-auth-library-java/issues/1146)) ([5bf606b](https://github.com/googleapis/google-auth-library-java/commit/5bf606bb8f6d863b44e87587eebf51eaeea4a0ae))
+
+
+### Bug Fixes
+
+* Create and reuse self signed jwt creds for better performance ([#1154](https://github.com/googleapis/google-auth-library-java/issues/1154)) ([eaaa8e8](https://github.com/googleapis/google-auth-library-java/commit/eaaa8e89cf69d1e0d581443121f315854d52c75f))
+* Java doc for DefaultPKCEProvider.java ([#1148](https://github.com/googleapis/google-auth-library-java/issues/1148)) ([154c127](https://github.com/googleapis/google-auth-library-java/commit/154c1279b3ec96cc34a3225e5e78800ccdda927c))
+* Removed url pattern validation for google urls in external account credential configurations ([#1150](https://github.com/googleapis/google-auth-library-java/issues/1150)) ([35495b1](https://github.com/googleapis/google-auth-library-java/commit/35495b1207ffe11712ee996d3e305449752fb87c))
+
+
+### Documentation
+
+* Clarified Maven artifact for HTTP-based clients ([#1136](https://github.com/googleapis/google-auth-library-java/issues/1136)) ([b49fc13](https://github.com/googleapis/google-auth-library-java/commit/b49fc13b10d0e326c7296e2aad7a50ea03e774f5))
+
+## [1.15.0](https://github.com/googleapis/google-auth-library-java/compare/v1.14.0...v1.15.0) (2023-01-25)
+
+
+### Features
+
+* Adds external account authorized user credentials ([#1129](https://github.com/googleapis/google-auth-library-java/issues/1129)) ([06bf21a](https://github.com/googleapis/google-auth-library-java/commit/06bf21a6ce9478a35907bd6681e53a0e86ffc83f))
+* Expose scopes granted by user ([#1107](https://github.com/googleapis/google-auth-library-java/issues/1107)) ([240c26b](https://github.com/googleapis/google-auth-library-java/commit/240c26bf11652e208469c2a6ea2fc2f383343c25))
+
+
+### Bug Fixes
+
+* AccessToken scopes clean serialization and default as empty list ([#1125](https://github.com/googleapis/google-auth-library-java/issues/1125)) ([f55d41f](https://github.com/googleapis/google-auth-library-java/commit/f55d41fa90750464a37a452ff03a8b811ae93425))
+* Enforce Locale.US for AwsRequestSignerTest ([#1111](https://github.com/googleapis/google-auth-library-java/issues/1111)) ([aeb1218](https://github.com/googleapis/google-auth-library-java/commit/aeb12182241e75cba664975a83bbcfa2449fb0f5))
+* Ensure both refreshMargin and expirationMargin are set when using OAuth2CredentialsWithRefresh ([#1131](https://github.com/googleapis/google-auth-library-java/issues/1131)) ([326e4a1](https://github.com/googleapis/google-auth-library-java/commit/326e4a15dae72b3806b7a640843d7abc669b19c6))
+
+## [1.14.0](https://github.com/googleapis/google-auth-library-java/compare/v1.13.0...v1.14.0) (2022-12-06)
+
+
+### Features
+
+* Add GDCH support ([#1087](https://github.com/googleapis/google-auth-library-java/issues/1087)) ([cfafb2d](https://github.com/googleapis/google-auth-library-java/commit/cfafb2d4c8d6ab3179e709ff09fc09e6dbc11a70))
+* Adding functional tests for Compute Engine ([#1105](https://github.com/googleapis/google-auth-library-java/issues/1105)) ([6f32ac3](https://github.com/googleapis/google-auth-library-java/commit/6f32ac3d4db91ff05fd7134ad6c788a16ffe44f4))
+* Introduce Environment Variable for Quota Project Id ([#1082](https://github.com/googleapis/google-auth-library-java/issues/1082)) ([040acef](https://github.com/googleapis/google-auth-library-java/commit/040acefec507f419f6e4ec4eab9645a6e3888a15))
+* Next release from main branch is 1.13.0 ([#1077](https://github.com/googleapis/google-auth-library-java/issues/1077)) ([d56eee0](https://github.com/googleapis/google-auth-library-java/commit/d56eee07911ba65a685ccba585e71061037ea756))
+
+
+### Bug Fixes
+
+* AwsCredentials should not call metadata server if security creds and region are retrievable through environment vars ([#1100](https://github.com/googleapis/google-auth-library-java/issues/1100)) ([1ff5772](https://github.com/googleapis/google-auth-library-java/commit/1ff57720609fdf27f28b9c543c1ef63b57892593))
+* Not loosing the access token when calling UserCredentials#ToBuil… ([#993](https://github.com/googleapis/google-auth-library-java/issues/993)) ([84afdb8](https://github.com/googleapis/google-auth-library-java/commit/84afdb8f8d41e781dc93f04626411e10b35689de))
+
+## [1.13.0](https://github.com/googleapis/google-auth-library-java/compare/v1.12.1...v1.13.0) (2022-11-15)
+
+
+### Features
+
+* Add smbios check for GCE residency detection ([#1092](https://github.com/googleapis/google-auth-library-java/issues/1092)) ([bfe7d93](https://github.com/googleapis/google-auth-library-java/commit/bfe7d932dbbbaf6b311c387834256519a0d1b9ad))
+
+
+### Bug Fixes
+
+* Empty string check for aws url validation ([#1089](https://github.com/googleapis/google-auth-library-java/issues/1089)) ([6f177a1](https://github.com/googleapis/google-auth-library-java/commit/6f177a1346ac481f34ab7cf343d552dcd88b7220))
+* Validate url domain for aws metadata urls ([#1079](https://github.com/googleapis/google-auth-library-java/issues/1079)) ([31fe461](https://github.com/googleapis/google-auth-library-java/commit/31fe461ac86e92fdff41bb17f0abc9b2a132676c))
+
+## [1.12.1](https://github.com/googleapis/google-auth-library-java/compare/v1.12.0...v1.12.1) (2022-10-18)
+
+
+### Bug Fixes
+
+* Resolve race condition reported in [#692](https://github.com/googleapis/google-auth-library-java/issues/692) ([#1031](https://github.com/googleapis/google-auth-library-java/issues/1031)) ([87a6606](https://github.com/googleapis/google-auth-library-java/commit/87a66067dff49d68f5b01cfe4c3f755fbf6b44fb))
+
+## [1.12.0](https://github.com/googleapis/google-auth-library-java/compare/v1.11.0...v1.12.0) (2022-10-14)
+
+
+### Features
+
+* Adding validation for psc endpoints ([#1042](https://github.com/googleapis/google-auth-library-java/issues/1042)) ([b37a565](https://github.com/googleapis/google-auth-library-java/commit/b37a565c6c1e7acb44baf2307c862c9df8be9345))
+
+
+### Bug Fixes
+
+* Fixed javadoc errors ([#945](https://github.com/googleapis/google-auth-library-java/issues/945)) ([1ddc994](https://github.com/googleapis/google-auth-library-java/commit/1ddc99481ae8b0f0eceb4cb442d5c6614ec8a411))
+
+
+### Documentation
+
+* **samples:** Modified comments in the samples and minor refactor ([#990](https://github.com/googleapis/google-auth-library-java/issues/990)) ([669ab04](https://github.com/googleapis/google-auth-library-java/commit/669ab042844e46d0503bbd31fd2da92a7963bad5))
+
+## [1.11.0](https://github.com/googleapis/google-auth-library-java/compare/v1.10.0...v1.11.0) (2022-09-08)
+
+
+### Features
+
+* Adds configurable token lifetime support ([#982](https://github.com/googleapis/google-auth-library-java/issues/982)) ([0198733](https://github.com/googleapis/google-auth-library-java/commit/0198733b9d294cbee95f1170f814fbfe94baa6fc))
+
+
+### Bug Fixes
+
+* Add retries to public key fetch ([#983](https://github.com/googleapis/google-auth-library-java/issues/983)) ([1200a39](https://github.com/googleapis/google-auth-library-java/commit/1200a39361e2a1767ef95306ba3ece1b749e82a8))
+* Add Test to validate 0x20 in token ([#971](https://github.com/googleapis/google-auth-library-java/issues/971)) ([612db0a](https://github.com/googleapis/google-auth-library-java/commit/612db0af3afa70b5400891ba3c7eab18ea5eb6bf))
+* Change revoke request from get to post ([#979](https://github.com/googleapis/google-auth-library-java/issues/979)) ([ead58b2](https://github.com/googleapis/google-auth-library-java/commit/ead58b22e04c00ece9f0ea55cbec20d2953f5460))
+* Setting the retry count to default value and enabling ioexceptions to retry ([#988](https://github.com/googleapis/google-auth-library-java/issues/988)) ([257071a](https://github.com/googleapis/google-auth-library-java/commit/257071aeb39c4441bd152813d727f83d433f346f))
+* Updates IdTokenVerifier so that it does not cache a failed public key response ([#967](https://github.com/googleapis/google-auth-library-java/issues/967)) ([1f4c9c7](https://github.com/googleapis/google-auth-library-java/commit/1f4c9c77a38fb6dfb751447361af9cf00964f96b))
+
+## [1.10.0](https://github.com/googleapis/google-auth-library-java/compare/v1.9.0...v1.10.0) (2022-08-05)
+
+
+### Features
+
+* workforce identity federation for pluggable auth ([#959](https://github.com/googleapis/google-auth-library-java/issues/959)) ([7f2c535](https://github.com/googleapis/google-auth-library-java/commit/7f2c535ab7c842a672d6761f4cd80df88e1a37ed))
+
+
+### Bug Fixes
+
+* updates executable response spec for executable-sourced credentials ([#955](https://github.com/googleapis/google-auth-library-java/issues/955)) ([48ff83d](https://github.com/googleapis/google-auth-library-java/commit/48ff83dc68e29dcae07fdea963cbbe5525f86a89))
+
+
+### Documentation
+
+* **samples:** added auth samples and tests ([#927](https://github.com/googleapis/google-auth-library-java/issues/927)) ([32c717f](https://github.com/googleapis/google-auth-library-java/commit/32c717fdf1a721f3e7ca3d75f03fcc229923689c))
+
+## [1.9.0](https://github.com/googleapis/google-auth-library-java/compare/v1.8.1...v1.9.0) (2022-08-02)
+
+
+### Features
+
+* integration tests for pluggable auth ([#939](https://github.com/googleapis/google-auth-library-java/issues/939)) ([22f37aa](https://github.com/googleapis/google-auth-library-java/commit/22f37aa687b7ffb4209a131860ccdd02f6fc4d42))
+
+
+### Bug Fixes
+
+* expiration time of the ImpersonatedCredentials token depending on the current host's timezone ([#932](https://github.com/googleapis/google-auth-library-java/issues/932)) ([73af08a](https://github.com/googleapis/google-auth-library-java/commit/73af08a1c5f14e18936e9dbd3d1ba29c2675961d))
+
+
+### Documentation
+
+* update wif documentation with enable-imdsv2 flag ([#940](https://github.com/googleapis/google-auth-library-java/issues/940)) ([acc1ce3](https://github.com/googleapis/google-auth-library-java/commit/acc1ce3603435f1c0cf23b8606af71b05e566f2f))
+
+## [1.8.1](https://github.com/googleapis/google-auth-library-java/compare/v1.8.0...v1.8.1) (2022-07-13)
+
+
+### Bug Fixes
+
+* enable longpaths support for windows test ([#1485](https://github.com/googleapis/google-auth-library-java/issues/1485)) ([#943](https://github.com/googleapis/google-auth-library-java/issues/943)) ([c21ec6c](https://github.com/googleapis/google-auth-library-java/commit/c21ec6c952b8bb8fb8bc2e2f1b260beb330a3cd2))
+
+## [1.8.0](https://github.com/googleapis/google-auth-library-java/compare/v1.7.0...v1.8.0) (2022-06-27)
+
+
+### Features
+
+* add build scripts for native image testing in Java 17 ([#1440](https://github.com/googleapis/google-auth-library-java/issues/1440)) ([#923](https://github.com/googleapis/google-auth-library-java/issues/923)) ([bbb51ce](https://github.com/googleapis/google-auth-library-java/commit/bbb51ce7a9265cb991739cd90e1ccf65675d05dc))
+* Adds Pluggable Auth support (WIF) ([#908](https://github.com/googleapis/google-auth-library-java/issues/908)) ([c3e8d16](https://github.com/googleapis/google-auth-library-java/commit/c3e8d169704943735c6b3df7bd0187f04fdd9aa5))
+
+
+### Documentation
+
+* updates README for Pluggable Auth ([#921](https://github.com/googleapis/google-auth-library-java/issues/921)) ([23716b8](https://github.com/googleapis/google-auth-library-java/commit/23716b82fb3000f5210bb5604127aad7ef52cb76))
+
+## [1.7.0](https://github.com/googleapis/google-auth-library-java/compare/v1.6.0...v1.7.0) (2022-05-12)
+
+
+### Features
+
+* Add ability to provide PrivateKey as Pkcs8 encoded string [#883](https://github.com/googleapis/google-auth-library-java/issues/883) ([#889](https://github.com/googleapis/google-auth-library-java/issues/889)) ([e0d6996](https://github.com/googleapis/google-auth-library-java/commit/e0d6996ac0db1bf75d92e5aba3eaab512affafe4))
+* Add iam endpoint override to ImpersonatedCredentials ([#910](https://github.com/googleapis/google-auth-library-java/issues/910)) ([97bfc4c](https://github.com/googleapis/google-auth-library-java/commit/97bfc4c8ceb199e775784ac3ed4fa992d4d2dcbf))
+
+
+### Bug Fixes
+
+* update branding in ExternalAccountCredentials ([#893](https://github.com/googleapis/google-auth-library-java/issues/893)) ([0200dbb](https://github.com/googleapis/google-auth-library-java/commit/0200dbb05cff06a333879cf99bac64adaada3239))
+
+## [1.6.0](https://github.com/googleapis/google-auth-library-java/compare/v1.5.3...v1.6.0) (2022-03-15)
+
+
+### Features
+
+* Add AWS Session Token to Metadata Requests ([#850](https://github.com/googleapis/google-auth-library-java/issues/850)) ([577e9a5](https://github.com/googleapis/google-auth-library-java/commit/577e9a52204b0d6026a302bb7efe2c6162d57945))
+
+
+### Bug Fixes
+
+* ImmutableSet converted to List for Impersonated Credentials ([#732](https://github.com/googleapis/google-auth-library-java/issues/732)) ([7dcd549](https://github.com/googleapis/google-auth-library-java/commit/7dcd549c4ef0617e657315b7a718368fbd162997))
+* update library docs ([#868](https://github.com/googleapis/google-auth-library-java/issues/868)) ([a081015](https://github.com/googleapis/google-auth-library-java/commit/a081015cb72ade91c022b58261c8d253e46a7793))
+
+### [1.5.3](https://github.com/googleapis/google-auth-library-java/compare/v1.5.2...v1.5.3) (2022-02-24)
+
+
+### Bug Fixes
+
+* **ci:** downgrade nexus-staging-maven-plugin to 1.6.8 ([#874](https://github.com/googleapis/google-auth-library-java/issues/874)) ([fc331d4](https://github.com/googleapis/google-auth-library-java/commit/fc331d466286d99cb3c6aa8977d34fd5f224eff7))
+
+### [1.5.2](https://github.com/googleapis/google-auth-library-java/compare/v1.5.1...v1.5.2) (2022-02-24)
+
+
+### Bug Fixes
+
+* downgrading nexus staging plugin 1.6.8 ([#871](https://github.com/googleapis/google-auth-library-java/issues/871)) ([e87224c](https://github.com/googleapis/google-auth-library-java/commit/e87224cca10d5d24523a5c3ac1e829fd51089f0c))
+
+### [1.5.1](https://github.com/googleapis/google-auth-library-java/compare/v1.5.0...v1.5.1) (2022-02-22)
+
+
+### Bug Fixes
+
+* **deps:** update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.3.2 ([#852](https://github.com/googleapis/google-auth-library-java/issues/852)) ([aa557c7](https://github.com/googleapis/google-auth-library-java/commit/aa557c7545941d712339b4b62a413997a54bcccc))
+
+## [1.5.0](https://github.com/googleapis/google-auth-library-java/compare/v1.4.0...v1.5.0) (2022-02-14)
+
+
+### Features
+
+* update retries and implement Retryable ([#750](https://github.com/googleapis/google-auth-library-java/issues/750)) ([f9a9b8a](https://github.com/googleapis/google-auth-library-java/commit/f9a9b8ace0199e6b75ed42c7bacfa3be30c34111))
+
+
+### Dependencies
+
+* **java:** update actions/github-script action to v5 ([#1339](https://github.com/googleapis/google-auth-library-java/issues/1339)) ([#843](https://github.com/googleapis/google-auth-library-java/issues/843)) ([ce44591](https://github.com/googleapis/google-auth-library-java/commit/ce445910198e7b78c9500ab148a1b6b99268185e))
+
+## [1.4.0](https://github.com/googleapis/google-auth-library-java/compare/v1.3.0...v1.4.0) (2022-01-19)
+
+
+### Features
+
+* setting the audience to always point to google token endpoint ([#833](https://github.com/googleapis/google-auth-library-java/issues/833)) ([33bfe7a](https://github.com/googleapis/google-auth-library-java/commit/33bfe7a788a524324cd9b0a54acc8917f6b75556))
+
+
+### Bug Fixes
+
+* (WIF) remove erroneous check for the subject token field name for text credential source ([#822](https://github.com/googleapis/google-auth-library-java/issues/822)) ([6d35c68](https://github.com/googleapis/google-auth-library-java/commit/6d35c681cf397ff2a90363184e26ee5850294c41))
+* **java:** add -ntp flag to native image testing command ([#1299](https://github.com/googleapis/google-auth-library-java/issues/1299)) ([#807](https://github.com/googleapis/google-auth-library-java/issues/807)) ([aa6654a](https://github.com/googleapis/google-auth-library-java/commit/aa6654a639ea15bcce7c7a6e86f170b1345895f0))
+* **java:** run Maven in plain console-friendly mode ([#1301](https://github.com/googleapis/google-auth-library-java/issues/1301)) ([#818](https://github.com/googleapis/google-auth-library-java/issues/818)) ([4df45d0](https://github.com/googleapis/google-auth-library-java/commit/4df45d0d03a973f1beff43d8965c26289f217f22))
+
+## [1.3.0](https://www.github.com/googleapis/google-auth-library-java/compare/v1.2.2...v1.3.0) (2021-11-10)
+
+
+### Features
+
+* next release from main branch is 1.3.0 ([#780](https://www.github.com/googleapis/google-auth-library-java/issues/780)) ([1149581](https://www.github.com/googleapis/google-auth-library-java/commit/1149581e63267e3553c74ba2114d849c5b24f27b))
+
+
+### Bug Fixes
+
+* **java:** java 17 dependency arguments ([#1266](https://www.github.com/googleapis/google-auth-library-java/issues/1266)) ([#779](https://www.github.com/googleapis/google-auth-library-java/issues/779)) ([9160a53](https://www.github.com/googleapis/google-auth-library-java/commit/9160a53e6507c1c938795e181c65ad80db1bcf11))
+* service account impersonation with workforce credentials ([#770](https://www.github.com/googleapis/google-auth-library-java/issues/770)) ([6449ef0](https://www.github.com/googleapis/google-auth-library-java/commit/6449ef0922053121a6732933ab9e246965fde3b7))
+
+### [1.2.2](https://www.github.com/googleapis/google-auth-library-java/compare/v1.2.1...v1.2.2) (2021-10-20)
+
+
+### Bug Fixes
+
+* environment variable is "AWS_SESSION_TOKEN" and not "Token" ([#772](https://www.github.com/googleapis/google-auth-library-java/issues/772)) ([c8c3073](https://www.github.com/googleapis/google-auth-library-java/commit/c8c3073790ca2f660eabd2c410b0e295f693040b))
+
+### [1.2.1](https://www.github.com/googleapis/google-auth-library-java/compare/v1.2.0...v1.2.1) (2021-10-11)
+
+
+### Bug Fixes
+
+* disabling self-signed jwt for domain wide delegation ([#754](https://www.github.com/googleapis/google-auth-library-java/issues/754)) ([ac70a27](https://www.github.com/googleapis/google-auth-library-java/commit/ac70a279bdaf681507d7815264a3f5e92fd2aaa6))
+
+## [1.2.0](https://www.github.com/googleapis/google-auth-library-java/compare/v1.1.0...v1.2.0) (2021-09-30)
+
+
+### Features
+
+* add support for Workforce Pools ([#729](https://www.github.com/googleapis/google-auth-library-java/issues/729)) ([5f3fed7](https://www.github.com/googleapis/google-auth-library-java/commit/5f3fed79e22f3c2d585c5b03c01791b0f8109929))
+
+
+### Bug Fixes
+
+* allow empty workforce_pool_user_project ([#752](https://www.github.com/googleapis/google-auth-library-java/issues/752)) ([e1cbce1](https://www.github.com/googleapis/google-auth-library-java/commit/e1cbce1a5cb269c6613bc6d40f06145bd45099c0))
+* timing of stale token refreshes on ComputeEngine ([#749](https://www.github.com/googleapis/google-auth-library-java/issues/749)) ([c813d55](https://www.github.com/googleapis/google-auth-library-java/commit/c813d55a78053ecbec1a9640e6c9814da87319eb))
+* workforce audience ([#741](https://www.github.com/googleapis/google-auth-library-java/issues/741)) ([a08cacc](https://www.github.com/googleapis/google-auth-library-java/commit/a08cacc7990b9058c8f1af3f9d8d816119562cc4))
+
+## [1.1.0](https://www.github.com/googleapis/google-auth-library-java/compare/v1.0.0...v1.1.0) (2021-08-17)
+
+
+### Features
+
+* downscoping with credential access boundaries ([#702](https://www.github.com/googleapis/google-auth-library-java/issues/702)) ([aa7ede1](https://www.github.com/googleapis/google-auth-library-java/commit/aa7ede1d1c688ba437798f4204820c0506d5d969))
+
+
+### Bug Fixes
+
+* add validation for the token URL and service account impersonation URL for Workload Identity Federation ([#717](https://www.github.com/googleapis/google-auth-library-java/issues/717)) ([23cb8ef](https://www.github.com/googleapis/google-auth-library-java/commit/23cb8ef778d012bbd452c1dfdac5f096d1af6c95))
+
+
+### Documentation
+
+* updates README for downscoping with CAB ([#716](https://www.github.com/googleapis/google-auth-library-java/issues/716)) ([68bceba](https://www.github.com/googleapis/google-auth-library-java/commit/68bceba21c05870f6eb616cc057ddf0521c581b8))
+
+## [1.0.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.27.0...v1.0.0) (2021-07-28)
+
+
+### ⚠ BREAKING CHANGES
+
+* updating google-auth-library-java min Java version to 1.8
+
+### Features
+
+* GA release of google-auth-library-java (ver 1.0.0) ([#704](https://www.github.com/googleapis/google-auth-library-java/issues/704)) ([3d9874f](https://www.github.com/googleapis/google-auth-library-java/commit/3d9874f1c91dfa10d6f72d41e922b3f1ec654943))
+* updating google-auth-library-java min Java version to 1.8 ([3d9874f](https://www.github.com/googleapis/google-auth-library-java/commit/3d9874f1c91dfa10d6f72d41e922b3f1ec654943))
+
+
+### Bug Fixes
+
+* Add shopt -s nullglob to dependencies script ([#693](https://www.github.com/googleapis/google-auth-library-java/issues/693)) ([c5aa708](https://www.github.com/googleapis/google-auth-library-java/commit/c5aa7084d9ca817a53cf6bac14d442adeeaeb310))
+* Update dependencies.sh to not break on mac ([c5aa708](https://www.github.com/googleapis/google-auth-library-java/commit/c5aa7084d9ca817a53cf6bac14d442adeeaeb310))
+
+## [0.27.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.26.0...v0.27.0) (2021-07-14)
+
+
+### Features
+
+* add Id token support for UserCredentials ([#650](https://www.github.com/googleapis/google-auth-library-java/issues/650)) ([5a8f467](https://www.github.com/googleapis/google-auth-library-java/commit/5a8f4676630854c53aa708a9c8b960770067f858))
+* add impersonation credentials to ADC ([#613](https://www.github.com/googleapis/google-auth-library-java/issues/613)) ([b9823f7](https://www.github.com/googleapis/google-auth-library-java/commit/b9823f70d7f3f7461b7de40bee06f5e7ba0e797c))
+* Adding functional tests for Service Account ([#685](https://www.github.com/googleapis/google-auth-library-java/issues/685)) ([dfe118c](https://www.github.com/googleapis/google-auth-library-java/commit/dfe118c261aadf137a3cf47a7acb9892c7a6db4d))
+* allow scopes for self signed jwt ([#689](https://www.github.com/googleapis/google-auth-library-java/issues/689)) ([f4980c7](https://www.github.com/googleapis/google-auth-library-java/commit/f4980c77566bbd5ef4c532acb199d7d484dbcd01))
+
+## [0.26.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.25.5...v0.26.0) (2021-05-20)
+
+
+### Features
+
+* add `gcf-owl-bot[bot]` to `ignoreAuthors` ([#674](https://www.github.com/googleapis/google-auth-library-java/issues/674)) ([359b20f](https://www.github.com/googleapis/google-auth-library-java/commit/359b20f24f88e09b6b104c61ca63a1b604ea64d2))
+* added getter for credentials object in HttpCredentialsAdapter ([#658](https://www.github.com/googleapis/google-auth-library-java/issues/658)) ([5a946ea](https://www.github.com/googleapis/google-auth-library-java/commit/5a946ea5e0d974611f2205f468236db4b931e486))
+* enable pre-emptive async oauth token refreshes ([#646](https://www.github.com/googleapis/google-auth-library-java/issues/646)) ([e3f4c7e](https://www.github.com/googleapis/google-auth-library-java/commit/e3f4c7eac0417705553ef8259599ec29fc8ad9b4))
+* Returning an issuer claim on request errors ([#656](https://www.github.com/googleapis/google-auth-library-java/issues/656)) ([95d70ae](https://www.github.com/googleapis/google-auth-library-java/commit/95d70ae0f5f4c985455f913ddef14ebe75500656))
+
+
+### Bug Fixes
+
+* use orginal url as audience for self signed jwt if scheme or host is null ([#642](https://www.github.com/googleapis/google-auth-library-java/issues/642)) ([b4e6f1a](https://www.github.com/googleapis/google-auth-library-java/commit/b4e6f1a0bd17dd31edc85ed4879cea75857fd747))
+
+### [0.25.5](https://www.github.com/googleapis/google-auth-library-java/compare/v0.25.4...v0.25.5) (2021-04-22)
+
+
+### Dependencies
+
+* update autovalue to 1.8.1 ([#638](https://www.github.com/googleapis/google-auth-library-java/issues/638)) ([62cd356](https://www.github.com/googleapis/google-auth-library-java/commit/62cd3564a93abe3cbbe083ac9b7aaebe4608b4bd))
+
+### [0.25.4](https://www.github.com/googleapis/google-auth-library-java/compare/v0.25.3...v0.25.4) (2021-04-15)
+
+
+### Bug Fixes
+
+* release scripts from issuing overlapping phases ([#634](https://www.github.com/googleapis/google-auth-library-java/issues/634)) ([b8d851e](https://www.github.com/googleapis/google-auth-library-java/commit/b8d851e1ac97b71e986c9afccca42021be3f9dd1))
+* typo ([#632](https://www.github.com/googleapis/google-auth-library-java/issues/632)) ([d860608](https://www.github.com/googleapis/google-auth-library-java/commit/d8606083b6632e26463aac0a0d1e92835d2fbcd0))
+
+### [0.25.3](https://www.github.com/googleapis/google-auth-library-java/compare/v0.25.2...v0.25.3) (2021-04-12)
+
+
+### Dependencies
+
+* update guava patch ([#628](https://www.github.com/googleapis/google-auth-library-java/issues/628)) ([8ff3207](https://www.github.com/googleapis/google-auth-library-java/commit/8ff320755e44f937590196bcbefa8c9537244af6))
+
+### [0.25.2](https://www.github.com/googleapis/google-auth-library-java/compare/v0.25.1...v0.25.2) (2021-03-18)
+
+
+### Bug Fixes
+
+* follow up fix service account credentials createScopedRequired ([#605](https://www.github.com/googleapis/google-auth-library-java/issues/605)) ([7ddac43](https://www.github.com/googleapis/google-auth-library-java/commit/7ddac43c418bb8b0cc3fd8d4f9d8752ad65bd842))
+* support AWS_DEFAULT_REGION env var ([#599](https://www.github.com/googleapis/google-auth-library-java/issues/599)) ([3d066ee](https://www.github.com/googleapis/google-auth-library-java/commit/3d066ee4755c20e2bd44b234dff71df1c4815aec))
+
+### [0.25.1](https://www.github.com/googleapis/google-auth-library-java/compare/v0.25.0...v0.25.1) (2021-03-18)
+
+
+### Bug Fixes
+
+* fix service account credentials createScopedRequired ([#601](https://www.github.com/googleapis/google-auth-library-java/issues/601)) ([0614482](https://www.github.com/googleapis/google-auth-library-java/commit/061448209da05ddfc75b40aae495c33d0ee7f1ee))
+
+## [0.25.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.24.1...v0.25.0) (2021-03-16)
+
+
+### Features
+
+* add self signed jwt support ([#572](https://www.github.com/googleapis/google-auth-library-java/issues/572)) ([efe103a](https://www.github.com/googleapis/google-auth-library-java/commit/efe103a2e688ca915ec9925a72c49bb2a1b3c3b5))
+
+### [0.24.1](https://www.github.com/googleapis/google-auth-library-java/compare/v0.24.0...v0.24.1) (2021-02-25)
+
+
+### Dependencies
+
+* update dependency com.google.http-client:google-http-client-bom to v1.39.0 ([#580](https://www.github.com/googleapis/google-auth-library-java/issues/580)) ([88718b0](https://www.github.com/googleapis/google-auth-library-java/commit/88718b0185ee6a3ff1168ac68621be0c5ff0efab))
+
+## [0.24.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.23.0...v0.24.0) (2021-02-19)
+
+
+### Features
+
+* add workload identity federation support ([#547](https://www.github.com/googleapis/google-auth-library-java/issues/547)) ([b8dde1e](https://www.github.com/googleapis/google-auth-library-java/commit/b8dde1e43f86a0a00741790c12d73f6cbda6251d))
+
+
+### Bug Fixes
+
+* don't log downloads ([#576](https://www.github.com/googleapis/google-auth-library-java/issues/576)) ([6181030](https://www.github.com/googleapis/google-auth-library-java/commit/61810306dc0e18500a4a6b2704e00842fbecd879))
+
+
+### Documentation
+
+* add instructions for using workload identity federation ([#564](https://www.github.com/googleapis/google-auth-library-java/issues/564)) ([2142db3](https://www.github.com/googleapis/google-auth-library-java/commit/2142db314666f298071ae30a7419b00d48d87476))
+
+## [0.23.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.22.2...v0.23.0) (2021-01-26)
+
+
+### ⚠ BREAKING CHANGES
+
+* privatize deprecated constructor (#473)
+
+### Features
+
+* allow custom lifespan for impersonated creds ([#515](https://www.github.com/googleapis/google-auth-library-java/issues/515)) ([0707ed4](https://www.github.com/googleapis/google-auth-library-java/commit/0707ed4bbb40fb775f196004ee30f8c695fe662b))
+* allow custom scopes for compute engine creds ([#514](https://www.github.com/googleapis/google-auth-library-java/issues/514)) ([edc8d6e](https://www.github.com/googleapis/google-auth-library-java/commit/edc8d6e0e7ca2c6749d026ba42854a09c4879fd6))
+* allow set lifetime for service account creds ([#516](https://www.github.com/googleapis/google-auth-library-java/issues/516)) ([427f2d5](https://www.github.com/googleapis/google-auth-library-java/commit/427f2d5610f0e8184a21b24531d2549a68c0b546))
+* promote IdToken and JWT features ([#538](https://www.github.com/googleapis/google-auth-library-java/issues/538)) ([b514fe0](https://www.github.com/googleapis/google-auth-library-java/commit/b514fe0cebe5a294e0cf97b7b5349e6a523dc7b2))
+
+
+### Bug Fixes
+
+* per google style, logger is lower case ([#529](https://www.github.com/googleapis/google-auth-library-java/issues/529)) ([ecfc6a2](https://www.github.com/googleapis/google-auth-library-java/commit/ecfc6a2ea6060e06629b5d422b23b842b917f55e))
+* privatize deprecated constructor ([#473](https://www.github.com/googleapis/google-auth-library-java/issues/473)) ([5804ff0](https://www.github.com/googleapis/google-auth-library-java/commit/5804ff03a531268831ac797ab262638a3119c14f))
+* remove deprecated methods ([#537](https://www.github.com/googleapis/google-auth-library-java/issues/537)) ([427963e](https://www.github.com/googleapis/google-auth-library-java/commit/427963e04702d8b73eca5ed555539b11bbe97342))
+* replace non-precondition use of Preconditions ([#539](https://www.github.com/googleapis/google-auth-library-java/issues/539)) ([f2ab4f1](https://www.github.com/googleapis/google-auth-library-java/commit/f2ab4f14262d54de0fde85494cfd92cf01a30cbe))
+* switch to GSON ([#531](https://www.github.com/googleapis/google-auth-library-java/issues/531)) ([1b98d5c](https://www.github.com/googleapis/google-auth-library-java/commit/1b98d5c86fc5e56187c977e7f43c39bb62483d40))
+* use default timeout if given 0 for ImpersonatedCredentials ([#527](https://www.github.com/googleapis/google-auth-library-java/issues/527)) ([ec74870](https://www.github.com/googleapis/google-auth-library-java/commit/ec74870c372a33d4157b45bb5d59ad7464fb2238))
+
+
+### Dependencies
+
+* update dependency com.google.appengine:appengine-api-1.0-sdk to v1.9.84 ([#422](https://www.github.com/googleapis/google-auth-library-java/issues/422)) ([b262c45](https://www.github.com/googleapis/google-auth-library-java/commit/b262c4587b058e6837429ee05f1b6a63620ee598))
+* update dependency com.google.guava:guava to v30.1-android ([#522](https://www.github.com/googleapis/google-auth-library-java/issues/522)) ([4090d1c](https://www.github.com/googleapis/google-auth-library-java/commit/4090d1cb50041bceb1cd975d1a9249a412df936f))
+
+
+### Documentation
+
+* fix wording in jwtWithClaims Javadoc ([#536](https://www.github.com/googleapis/google-auth-library-java/issues/536)) ([af21727](https://www.github.com/googleapis/google-auth-library-java/commit/af21727815263fb5ffc07ede953cf042fac3ac2b))
+
+### [0.22.2](https://www.github.com/googleapis/google-auth-library-java/compare/v0.22.1...v0.22.2) (2020-12-11)
+
+
+### Bug Fixes
+
+* quotaProjectId should be applied for cached `getRequestMetadata(URI, Executor, RequestMetadataCallback)` ([#509](https://www.github.com/googleapis/google-auth-library-java/issues/509)) ([0a8412f](https://www.github.com/googleapis/google-auth-library-java/commit/0a8412fcf9de4ac568b9f88618e44087dd31b144))
+
+### [0.22.1](https://www.github.com/googleapis/google-auth-library-java/compare/v0.22.0...v0.22.1) (2020-11-05)
+
+
+### Bug Fixes
+
+* remove 1 hour limit for impersonated token ([#490](https://www.github.com/googleapis/google-auth-library-java/issues/490)) ([927e3d5](https://www.github.com/googleapis/google-auth-library-java/commit/927e3d5598e2d2b06512b27f4210994c65b26f59))
+
+
+### Dependencies
+
+* update dependency com.google.guava:guava to v30 ([#497](https://www.github.com/googleapis/google-auth-library-java/issues/497)) ([0551649](https://www.github.com/googleapis/google-auth-library-java/commit/055164969d175718ee8f2c0369b84bcddc1d7134))
+* update dependency com.google.http-client:google-http-client-bom to v1.38.0 ([#503](https://www.github.com/googleapis/google-auth-library-java/issues/503)) ([46f20bc](https://www.github.com/googleapis/google-auth-library-java/commit/46f20bca8b5951ebea6a963b3affde2b92d403c7))
+
+## [0.22.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.21.1...v0.22.0) (2020-10-13)
+
+
+### Features
+
+* add logging at FINE level for each step of ADC ([#435](https://www.github.com/googleapis/google-auth-library-java/issues/435)) ([7d145b2](https://www.github.com/googleapis/google-auth-library-java/commit/7d145b2371033093ea13fd05520c90970a5ef363))
+
+
+### Documentation
+
+* remove bad javadoc tags ([#478](https://www.github.com/googleapis/google-auth-library-java/issues/478)) ([a329c41](https://www.github.com/googleapis/google-auth-library-java/commit/a329c4171735c3d4ee574978e6c3742b96c01f74))
+
+
+### Dependencies
+
+* update dependency com.google.http-client:google-http-client-bom to v1.37.0 ([#486](https://www.github.com/googleapis/google-auth-library-java/issues/486)) ([3027fbf](https://www.github.com/googleapis/google-auth-library-java/commit/3027fbfaf017f5aa5a22cc51cd38a522597729c0))
+
+### [0.21.1](https://www.github.com/googleapis/google-auth-library-java/compare/v0.21.0...v0.21.1) (2020-07-07)
+
+
+### Dependencies
+
+* update google-http-client to 1.36.0 ([#447](https://www.github.com/googleapis/google-auth-library-java/issues/447)) ([b913d19](https://www.github.com/googleapis/google-auth-library-java/commit/b913d194259e4f93bb401a844480f56b48dad3bd)), closes [#446](https://www.github.com/googleapis/google-auth-library-java/issues/446)
+
+## [0.21.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.20.0...v0.21.0) (2020-06-24)
+
+
+### Features
+
+* add TokenVerifier class that can verify RS256/ES256 tokens ([#420](https://www.github.com/googleapis/google-auth-library-java/issues/420)) ([5014ac7](https://www.github.com/googleapis/google-auth-library-java/commit/5014ac72a59d877ef95c616d0b33792b9fc70c25))
+
+
+### Dependencies
+
+* update autovalue packages to v1.7.2 ([#429](https://www.github.com/googleapis/google-auth-library-java/issues/429)) ([5758364](https://www.github.com/googleapis/google-auth-library-java/commit/575836405bd5803d6202bd0018609184d6a15831))
+* update dependency com.google.http-client:google-http-client-bom to v1.35.0 ([#427](https://www.github.com/googleapis/google-auth-library-java/issues/427)) ([5494ec0](https://www.github.com/googleapis/google-auth-library-java/commit/5494ec0a73319fb955b3d7ba025aea9607020c4e))
+* update Guava to 29.0-android ([#426](https://www.github.com/googleapis/google-auth-library-java/issues/426)) ([0cd3c2e](https://www.github.com/googleapis/google-auth-library-java/commit/0cd3c2ec0aef3ff0f0379b32f9d05126442219b6))
+
+## [0.20.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.19.0...v0.20.0) (2020-01-15)
+
+
+### Features
+
+* updated `JwtClaims.Builder` methods to `public` ([#396](https://www.github.com/googleapis/google-auth-library-java/issues/396)) ([9e5de14](https://www.github.com/googleapis/google-auth-library-java/commit/9e5de14263a01d746af2fc192cf1b82a2acff35c))
+
+
+### Dependencies
+
+* update guava to 28.2-android ([#389](https://www.github.com/googleapis/google-auth-library-java/issues/389)) ([70bd8ff](https://www.github.com/googleapis/google-auth-library-java/commit/70bd8ff15a9b0cb1dab9f350bd49dd60b2da33c7))
+
+## [0.19.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.18.0...v0.19.0) (2019-12-13)
+
+
+### Features
+
+* support reading in quotaProjectId for billing ([#383](https://www.github.com/googleapis/google-auth-library-java/issues/383)) ([f38c3c8](https://www.github.com/googleapis/google-auth-library-java/commit/f38c3c84748fadc1591f092edd1974539cf7b644))
+
+
+### Dependencies
+
+* update appengine-sdk to 1.9.76 ([#366](https://www.github.com/googleapis/google-auth-library-java/issues/366)) ([590883d](https://www.github.com/googleapis/google-auth-library-java/commit/590883d57158275b988b5e6c7f6d608eaa3c08ad))
+* update autovalue packages to v1.7 ([#365](https://www.github.com/googleapis/google-auth-library-java/issues/365)) ([42a1694](https://www.github.com/googleapis/google-auth-library-java/commit/42a169463ab3c36552e2eea605571ee9808f346c))
+* update dependency com.google.appengine:appengine to v1.9.77 ([#377](https://www.github.com/googleapis/google-auth-library-java/issues/377)) ([c3c950e](https://www.github.com/googleapis/google-auth-library-java/commit/c3c950e7d906aaa4187305a5fd9b05785e19766a))
+* update dependency com.google.http-client:google-http-client-bom to v1.33.0 ([#374](https://www.github.com/googleapis/google-auth-library-java/issues/374)) ([af0af50](https://www.github.com/googleapis/google-auth-library-java/commit/af0af5061f4544b8b5bb43c82d2ab66c08143b90))
+
+
+### Documentation
+
+* remove outdated comment on explicit IP address ([#370](https://www.github.com/googleapis/google-auth-library-java/issues/370)) ([71faa5f](https://www.github.com/googleapis/google-auth-library-java/commit/71faa5f6f26ef2f267743248b828d636d99a9d50))
+* xml syntax error in bom/README.md ([#372](https://www.github.com/googleapis/google-auth-library-java/issues/372)) ([ff8606a](https://www.github.com/googleapis/google-auth-library-java/commit/ff8606a608f9261a9714ceda823479f156f65643)), closes [#371](https://www.github.com/googleapis/google-auth-library-java/issues/371)
+
+### [0.18.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.17.2...v0.18.0) (2019-10-09)
+
+
+### Bug Fixes
+
+* make JwtClaims.newBuilder() public ([#350](https://www.github.com/googleapis/google-auth-library-java/issues/350)) ([6ab8758](https://www.github.com/googleapis/google-auth-library-java/commit/6ab8758))
+* move autovalue into annotation processor path instead of classpath ([#358](https://www.github.com/googleapis/google-auth-library-java/issues/358)) ([a82d348](https://www.github.com/googleapis/google-auth-library-java/commit/a82d348))
+
+
+### Dependencies
+
+* update Guava to 28.1 ([#353](https://www.github.com/googleapis/google-auth-library-java/issues/353)) ([f4f05be](https://www.github.com/googleapis/google-auth-library-java/commit/f4f05be))
+
+
+### Documentation
+
+* fix include instructions in google-auth-library-bom README ([#352](https://www.github.com/googleapis/google-auth-library-java/issues/352)) ([f649735](https://www.github.com/googleapis/google-auth-library-java/commit/f649735))
+
+### [0.17.4](https://www.github.com/googleapis/google-auth-library-java/compare/v0.18.0...v0.17.4) (2019-10-08)
+
+
+### Bug Fixes
+
+* make JwtClaims.newBuilder() public ([#350](https://www.github.com/googleapis/google-auth-library-java/issues/350)) ([6ab8758](https://www.github.com/googleapis/google-auth-library-java/commit/6ab8758))
+* move autovalue into annotation processor path instead of classpath ([#358](https://www.github.com/googleapis/google-auth-library-java/issues/358)) ([a82d348](https://www.github.com/googleapis/google-auth-library-java/commit/a82d348))
+
+
+### Dependencies
+
+* update Guava to 28.1 ([#353](https://www.github.com/googleapis/google-auth-library-java/issues/353)) ([f4f05be](https://www.github.com/googleapis/google-auth-library-java/commit/f4f05be))
+
+
+### Documentation
+
+* fix include instructions in google-auth-library-bom README ([#352](https://www.github.com/googleapis/google-auth-library-java/issues/352)) ([f649735](https://www.github.com/googleapis/google-auth-library-java/commit/f649735))
+
+### [0.17.2](https://www.github.com/googleapis/google-auth-library-java/compare/v0.17.1...v0.17.2) (2019-09-24)
+
+
+### Bug Fixes
+
+* typo in BOM dependency ([#345](https://www.github.com/googleapis/google-auth-library-java/issues/345)) ([a1d63bb](https://www.github.com/googleapis/google-auth-library-java/commit/a1d63bb))
+
+### [0.17.1](https://www.github.com/googleapis/google-auth-library-java/compare/v0.17.0...v0.17.1) (2019-08-22)
+
+
+### Bug Fixes
+
+* allow unset/null privateKeyId for JwtCredentials ([#336](https://www.github.com/googleapis/google-auth-library-java/issues/336)) ([d28a6ed](https://www.github.com/googleapis/google-auth-library-java/commit/d28a6ed))
+
+## [0.17.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.16.2...v0.17.0) (2019-08-16)
+
+
+### Bug Fixes
+
+* cleanup unused code and deprecation warnings ([#315](https://www.github.com/googleapis/google-auth-library-java/issues/315)) ([7fd94c0](https://www.github.com/googleapis/google-auth-library-java/commit/7fd94c0))
+* Fix declared dependencies from merge issue ([#291](https://www.github.com/googleapis/google-auth-library-java/issues/291)) ([35abf13](https://www.github.com/googleapis/google-auth-library-java/commit/35abf13))
+* throw SigningException as documented ([#316](https://www.github.com/googleapis/google-auth-library-java/issues/316)) ([a1ab97c](https://www.github.com/googleapis/google-auth-library-java/commit/a1ab97c))
+* typo in ComputeEngineCredentials exception message ([#313](https://www.github.com/googleapis/google-auth-library-java/issues/313)) ([1a16f38](https://www.github.com/googleapis/google-auth-library-java/commit/1a16f38))
+
+
+### Features
+
+* add Automatic-Module-Name to manifest ([#326](https://www.github.com/googleapis/google-auth-library-java/issues/326)) ([29f58b4](https://www.github.com/googleapis/google-auth-library-java/commit/29f58b4)), closes [#324](https://www.github.com/googleapis/google-auth-library-java/issues/324) [#324](https://www.github.com/googleapis/google-auth-library-java/issues/324)
+* add IDTokenCredential support ([#303](https://www.github.com/googleapis/google-auth-library-java/issues/303)) ([a87e3fd](https://www.github.com/googleapis/google-auth-library-java/commit/a87e3fd))
+* add JwtCredentials with custom claims ([#290](https://www.github.com/googleapis/google-auth-library-java/issues/290)) ([3f37172](https://www.github.com/googleapis/google-auth-library-java/commit/3f37172))
+* allow arbitrary additional claims for JwtClaims ([#331](https://www.github.com/googleapis/google-auth-library-java/issues/331)) ([888c61c](https://www.github.com/googleapis/google-auth-library-java/commit/888c61c))
+* Implement ServiceAccountSigner for ImpersonatedCredentials ([#279](https://www.github.com/googleapis/google-auth-library-java/issues/279)) ([70767e3](https://www.github.com/googleapis/google-auth-library-java/commit/70767e3))
+
+
+### Reverts
+
+* "build: run in debug mode ([#319](https://www.github.com/googleapis/google-auth-library-java/issues/319))" ([#320](https://www.github.com/googleapis/google-auth-library-java/issues/320)) ([de79e14](https://www.github.com/googleapis/google-auth-library-java/commit/de79e14))
+
+## [0.16.2](https://www.github.com/googleapis/google-auth-library-java/compare/v0.16.1...v0.16.2) (2019-06-26)
+
+
+### Bug Fixes
+
+* Add metadata-flavor header to metadata server ping for compute engine ([#283](https://github.com/googleapis/google-auth-library-java/pull/283))
+
+
+### Dependencies
+
+* Import http client bom for dependency management ([#268](https://github.com/googleapis/google-auth-library-java/pull/268))
+
+
+### Documentation
+
+* README section for interop with google-http-client ([#275](https://github.com/googleapis/google-auth-library-java/pull/275))
+
+
+## [0.16.1](https://www.github.com/googleapis/google-auth-library-java/compare/v0.16.0...v0.16.1) (2019-06-06)
+
+
+### Dependencies
+
+* Update dependency com.google.http-client:google-http-client to v1.30.1 ([#265](https://github.com/googleapis/google-auth-library-java/pull/265))
+
+
+## [0.16.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.15.0...v0.16.0) (2019-06-04)
+
+
+### Features
+
+* Add google-auth-library-bom artifact ([#256](https://github.com/googleapis/google-auth-library-java/pull/256))
+
+
+### Dependencies
+
+* Update dependency com.google.http-client:google-http-client to v1.30.0 ([#261](https://github.com/googleapis/google-auth-library-java/pull/261))
+* Update dependency com.google.http-client:google-http-client to v1.29.2 ([#259](https://github.com/googleapis/google-auth-library-java/pull/259))
+* Update dependency org.sonatype.plugins:nexus-staging-maven-plugin to v1.6.8 ([#257](https://github.com/googleapis/google-auth-library-java/pull/257))
+* Update to latest app engine SDK version ([#258](https://github.com/googleapis/google-auth-library-java/pull/258))
+* Update dependency org.apache.maven.plugins:maven-source-plugin to v3.1.0 ([#254](https://github.com/googleapis/google-auth-library-java/pull/254))
+* Update dependency org.jacoco:jacoco-maven-plugin to v0.8.4 ([#255](https://github.com/googleapis/google-auth-library-java/pull/255))
+* Update dependency org.apache.maven.plugins:maven-jar-plugin to v3.1.2 ([#252](https://github.com/googleapis/google-auth-library-java/pull/252))
+* Update dependency org.apache.maven.plugins:maven-source-plugin to v2.4 ([#253](https://github.com/googleapis/google-auth-library-java/pull/253))
+
+
+### Documentation
+
+* Javadoc publish kokoro job uses docpublisher ([#243](https://github.com/googleapis/google-auth-library-java/pull/243))
+
+
+## [0.15.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.14.0...v0.15.0) (2019-03-27)
+
+
+### Bug Fixes
+
+* createScoped: make overload call implementation ([#229](https://github.com/googleapis/google-auth-library-java/pull/229))
+
+
+### Reverts
+
+* Add back in deprecated methods in ServiceAccountJwtAccessCredentials ([#238](https://github.com/googleapis/google-auth-library-java/pull/238))
+
+
+## [0.14.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.13.0...v0.14.0) (2019-03-26)
+
+
+### Bug Fixes
+
+* update default metadata url ([#230](https://github.com/googleapis/google-auth-library-java/pull/230))
+* Remove deprecated methods ([#190](https://github.com/googleapis/google-auth-library-java/pull/190))
+* Update Sign Blob API ([#232](https://github.com/googleapis/google-auth-library-java/pull/232))
+
+
+### Dependencies
+
+* Upgrade http client to 1.29.0. ([#235](https://github.com/googleapis/google-auth-library-java/pull/235))
+* update deps ([#234](https://github.com/googleapis/google-auth-library-java/pull/234))
+
+
+## [0.13.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.12.0...v0.13.0) (2019-01-17)
+
+
+### Bug Fixes
+
+* Use OutputStream directly instead of PrintWriter ([#220](https://github.com/googleapis/google-auth-library-java/pull/220))
+* Improve log output when detecting GCE ([#214](https://github.com/googleapis/google-auth-library-java/pull/214))
+
+
+### Features
+
+* Overload GoogleCredentials.createScoped with variadic arguments ([#218](https://github.com/googleapis/google-auth-library-java/pull/218))
+
+
+### Dependencies
+
+* Update google-http-client version, guava, and maven surefire plugin ([#221](https://github.com/googleapis/google-auth-library-java/pull/221))
+
+
+## [0.12.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.11.0...v0.12.0) (2018-12-19)
+
+
+### Bug Fixes
+
+* Show error message in case of problems with getting access token ([#206](https://github.com/googleapis/google-auth-library-java/pull/206))
+* Add note about `NO_GCE_CHECK` to metadata 404 error message ([#205](https://github.com/googleapis/google-auth-library-java/pull/205))
+
+
+### Features
+
+* Add ImpersonatedCredentials ([#211](https://github.com/googleapis/google-auth-library-java/pull/211))
+* Add option to suppress end user credentials warning. ([#207](https://github.com/googleapis/google-auth-library-java/pull/207))
+
+
+### Dependencies
+
+* Update google-http-java-client dependency to 1.27.0 ([#208](https://github.com/googleapis/google-auth-library-java/pull/208))
+
+
+### Documentation
+
+* README grammar fix ([#192](https://github.com/googleapis/google-auth-library-java/pull/192))
+* Add unstable badge to README ([#184](https://github.com/googleapis/google-auth-library-java/pull/184))
+* Update README with instructions on installing the App Engine SDK and running the tests ([#209](https://github.com/googleapis/google-auth-library-java/pull/209))
+
+
+## [0.11.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.10.0...v0.11.0) (2018-08-23)
+
+
+### Bug Fixes
+
+* Update auth token urls (#174)
+
+
+### Dependencies
+
+* Update dependencies (guava) (#170)
+* Bumping google-http-client version to 1.24.1 (#171)
+
+
+### Documentation
+
+* Documentation for ComputeEngineCredential signing. (#176)
+* Fix README link (#169)
+
+
+## [0.10.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.9.1...v0.10.0) (2018-06-12)
+
+
+### Bug Fixes
+
+* Read token_uri from service account JSON (#160)
+* Log warning if default credentials uses a user token from gcloud sdk (#166)
+
+
+### Features
+
+* Add OAuth2Credentials#refreshIfExpired() (#163)
+* ComputeEngineCredentials implements ServiceAccountSigner (#141)
+
+
+### Documentation
+
+* Versionless Javadocs (#164)
+* Fix documentation for `getAccessToken()` returning cached value (#162)
+
+
+## [0.9.1](https://www.github.com/googleapis/google-auth-library-java/compare/v0.9.0...v0.9.1) (2018-04-09)
+
+
+### Features
+
+* Add caching for JWT tokens (#151)
+
+
+## [0.9.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.8.0...v0.9.0) (2017-11-02)
+
+
+### Bug Fixes
+
+* Fix NPE deserializing ServiceAccountCredentials (#132)
+
+
+### Features
+
+* Surface cleanup (#136)
+* Providing a method to remove CredentialsChangedListeners (#130)
+* Implemented in-memory TokenStore and added opportunity to save user credentials into file (#129)
+
+
+### Documentation
+
+* Fixes comment typos. (#131)
+
+
+## [0.8.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.7.1...v0.8.0) (2017-09-08)
+
+
+### Bug Fixes
+
+* Extracting the project_id field from service account JSON files (#118)
+* Fixing an Integer Overflow Issue (#121)
+* use metadata server to get credentials for GAE 8 standard environment (#122)
+
+
+### Features
+
+* Switch OAuth2 HTTP surface to use builder pattern (#123)
+* Add builder pattern to AppEngine credentials (#125)
+
+
+### Documentation
+
+* Fix API Documentation link rendering (#112)
+
+
+## [0.7.1](https://www.github.com/googleapis/google-auth-library-java/compare/v0.7.0...v0.7.1) (2017-07-14)
+
+
+### Bug Fixes
+
+* Mitigate occasional failures in looking up Application Default Credentials on a Google Compute Engine (GCE) Virtual Machine (#110)
+
+
+## [0.7.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.6.1...v0.7.0) (2017-06-06)
+
+
+### Bug Fixes
+
+* Retry HTTP errors in `ServiceAccountCredentials.refreshAccessToken()` to avoid propagating failures (#100 addresses #91)
+
+
+### Features
+
+* Add `GoogleCredentials.createDelegated()` method to allow using domain-wide delegation with service accounts (#102)
+* Allow bypassing App Engine credential check using environment variable, to allow Application Default Credentials to detect GCE when running on GAE Flex (#103)
diff --git a/google-auth-library-java/README.md b/google-auth-library-java/README.md
new file mode 100644
index 000000000000..6795cd0a491e
--- /dev/null
+++ b/google-auth-library-java/README.md
@@ -0,0 +1,78 @@
+# Google Auth Library
+
+Open source authentication client library for Java.
+
+[](http://github.com/badges/stability-badges)
+[](https://img.shields.io/maven-central/v/com.google.auth/google-auth-library-credentials.svg)
+
+## Documentation
+
+See the [official guide](https://cloud.google.com/java/getting-started/getting-started-with-google-auth-library) for ways
+to authenticate to Google Cloud and for more information about the Google Auth Library.
+
+See the [API Documentation](https://cloud.google.com/java/docs/reference/google-auth-library/latest/overview.html) to see
+the Javadocs for Google Auth Library.
+
+## Versioning
+
+This library follows [Semantic Versioning](http://semver.org/), but with some
+additional qualifications:
+
+1. Components marked with `@ObsoleteApi` are stable for usage in the current major version,
+ but will be marked with `@Deprecated` in a future major version.
+ **NOTE**: We reserve the right to mark anything as `@Deprecated` and introduce breaking
+ changes in a minor version to fix any ***critical bugs and
+ vulnerabilities***.
+
+2. Components marked with `@InternalApi` are technically public, but are only
+ public for technical reasons, because of the limitations of Java's access
+ modifiers. For the purposes of semver, they should be considered private.
+
+3. Components marked with `@InternalExtensionOnly` are stable for usage, but
+ not for extension. Thus, methods will not be removed from interfaces marked
+ with this annotation, but methods can be added, thus breaking any
+ code implementing the interface. See the javadocs for more details on other
+ consequences of this annotation.
+
+4. Components marked with `@BetaApi` are considered to be "0.x" features inside
+ a "1.x" library. This means they can change between minor and patch releases
+ in incompatible ways. These features should not be used by any library "B"
+ that itself has consumers, unless the components of library B that use
+ `@BetaApi` features are also marked with `@BetaApi`. Features marked as
+ `@BetaApi` are on a path to eventually become "1.x" features with the marker
+ removed.
+
+## Contributing
+
+Contributions to this library are always welcome and highly encouraged.
+
+See [CONTRIBUTING](CONTRIBUTING.md) documentation for more information on how to get started.
+
+Please note that this project is released with a Contributor Code of Conduct. By participating in
+this project you agree to abide by its terms. See [Code of Conduct](CODE_OF_CONDUCT.md) for more
+information.
+
+## Running the Tests
+
+To run the tests you will need:
+
+* Maven 3+
+
+```bash
+$ mvn test
+```
+
+## License
+
+BSD 3-Clause - See [LICENSE](LICENSE) for more information.
+
+[appengine-sdk-versions]: https://search.maven.org/search?q=g:com.google.appengine%20AND%20a:appengine-api-1.0-sdk&core=gav
+[appengine-sdk-install]: https://github.com/googleapis/google-auth-library-java/blob/main/README.md#google-auth-library-appengine
+[appengine-app-identity-service]: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/appidentity/AppIdentityService
+[apiary-clients]: https://search.maven.org/search?q=g:com.google.apis
+[http-credentials-adapter]: https://googleapis.dev/java/google-auth-library/latest/index.html?com/google/auth/http/HttpCredentialsAdapter.html
+[http-request-initializer]: https://googleapis.dev/java/google-http-client/latest/index.html?com/google/api/client/http/HttpRequestInitializer.html
+[token-verifier]: https://googleapis.dev/java/google-auth-library/latest/index.html?com/google/auth/oauth2/TokenVerifier.html
+[token-verifier-builder]: https://googleapis.dev/java/google-auth-library/latest/index.html?com/google/auth/oauth2/TokenVerifier.Builder.html
+[http-transport-factory]: https://googleapis.dev/java/google-auth-library/latest/index.html?com/google/auth/http/HttpTransportFactory.html
+[google-credentials]: https://googleapis.dev/java/google-auth-library/latest/index.html?com/google/auth/oauth2/GoogleCredentials.html
diff --git a/google-auth-library-java/appengine/java/com/google/auth/appengine/AppEngineCredentials.java b/google-auth-library-java/appengine/java/com/google/auth/appengine/AppEngineCredentials.java
new file mode 100644
index 000000000000..1cb1af1379c1
--- /dev/null
+++ b/google-auth-library-java/appengine/java/com/google/auth/appengine/AppEngineCredentials.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2015, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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.appengine;
+
+import com.google.appengine.api.appidentity.AppIdentityService;
+import com.google.appengine.api.appidentity.AppIdentityService.GetAccessTokenResult;
+import com.google.appengine.api.appidentity.AppIdentityServiceFactory;
+import com.google.auth.ServiceAccountSigner;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * OAuth2 credentials representing the built-in service account for Google App Engine. You should
+ * only use this class if you are running on AppEngine and are using urlfetch.
+ *
+ *
Fetches access tokens from the App Identity service.
+ */
+public class AppEngineCredentials extends GoogleCredentials implements ServiceAccountSigner {
+
+ private static final long serialVersionUID = -2627708355455064660L;
+
+ private final String appIdentityServiceClassName;
+ private final Collection scopes;
+ private final boolean scopesRequired;
+
+ private transient AppIdentityService appIdentityService;
+
+ private AppEngineCredentials(Collection scopes, AppIdentityService appIdentityService) {
+ this.scopes = scopes == null ? ImmutableSet.of() : ImmutableList.copyOf(scopes);
+ this.appIdentityService =
+ appIdentityService != null
+ ? appIdentityService
+ : AppIdentityServiceFactory.getAppIdentityService();
+ this.appIdentityServiceClassName = this.appIdentityService.getClass().getName();
+ scopesRequired = this.scopes.isEmpty();
+ }
+
+ /** Refresh the access token by getting it from the App Identity service */
+ @Override
+ public AccessToken refreshAccessToken() throws IOException {
+ if (createScopedRequired()) {
+ throw new IOException("AppEngineCredentials requires createScoped call before use.");
+ }
+ GetAccessTokenResult accessTokenResponse = appIdentityService.getAccessToken(scopes);
+ String accessToken = accessTokenResponse.getAccessToken();
+ Date expirationTime = accessTokenResponse.getExpirationTime();
+ return new AccessToken(accessToken, expirationTime);
+ }
+
+ @Override
+ public boolean createScopedRequired() {
+ return scopesRequired;
+ }
+
+ @Override
+ public GoogleCredentials createScoped(Collection scopes) {
+ return new AppEngineCredentials(scopes, appIdentityService);
+ }
+
+ @Override
+ public String getAccount() {
+ return appIdentityService.getServiceAccountName();
+ }
+
+ @Override
+ public byte[] sign(byte[] toSign) {
+ return appIdentityService.signForApp(toSign).getSignature();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(scopes, scopesRequired, appIdentityServiceClassName);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("scopes", scopes)
+ .add("scopesRequired", scopesRequired)
+ .add("appIdentityServiceClassName", appIdentityServiceClassName)
+ .toString();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof AppEngineCredentials)) {
+ return false;
+ }
+ AppEngineCredentials other = (AppEngineCredentials) obj;
+ return this.scopesRequired == other.scopesRequired
+ && Objects.equals(this.scopes, other.scopes)
+ && Objects.equals(this.appIdentityServiceClassName, other.appIdentityServiceClassName);
+ }
+
+ private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
+ input.defaultReadObject();
+ try {
+ // Load the class without initializing it (second argument: false) to prevent
+ // static initializers from running (preventing gadget chain attacks). Use the class loader
+ // of HttpTransportFactory to ensure the class is loaded from the same context as the library
+ // to try to prevent any class loading manipulation.
+ Class> clazz =
+ Class.forName(
+ appIdentityServiceClassName, false, AppIdentityService.class.getClassLoader());
+
+ // Check that the class is an instance of `AppIdentityService` to prevent loading of
+ // arbitrary classes.
+ if (!AppIdentityService.class.isAssignableFrom(clazz)) {
+ throw new IOException(
+ String.format(
+ "The class, %s, is not assignable from %s.",
+ appIdentityServiceClassName, AppIdentityService.class.getName()));
+ }
+ Constructor> constructor = clazz.getConstructor();
+ appIdentityService = (AppIdentityService) constructor.newInstance();
+ } catch (InstantiationException
+ | IllegalAccessException
+ | NoSuchMethodException
+ | InvocationTargetException e) {
+ throw new IOException(e);
+ }
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ @Override
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ public static class Builder extends GoogleCredentials.Builder {
+
+ private Collection scopes;
+ private AppIdentityService appIdentityService;
+
+ protected Builder() {}
+
+ protected Builder(AppEngineCredentials credentials) {
+ this.scopes = credentials.scopes;
+ this.appIdentityService = credentials.appIdentityService;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setScopes(Collection scopes) {
+ this.scopes = scopes;
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setAppIdentityService(AppIdentityService appIdentityService) {
+ this.appIdentityService = appIdentityService;
+ return this;
+ }
+
+ public Collection getScopes() {
+ return scopes;
+ }
+
+ public AppIdentityService getAppIdentityService() {
+ return appIdentityService;
+ }
+
+ @Override
+ public AppEngineCredentials build() {
+ return new AppEngineCredentials(scopes, appIdentityService);
+ }
+ }
+}
diff --git a/google-auth-library-java/appengine/javatests/com/google/auth/appengine/AppEngineCredentialsTest.java b/google-auth-library-java/appengine/javatests/com/google/auth/appengine/AppEngineCredentialsTest.java
new file mode 100644
index 000000000000..05f81d57825f
--- /dev/null
+++ b/google-auth-library-java/appengine/javatests/com/google/auth/appengine/AppEngineCredentialsTest.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2015, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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.appengine;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.BaseSerializationTest;
+import com.google.auth.oauth2.GoogleCredentials;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for AppEngineCredentials */
+class AppEngineCredentialsTest extends BaseSerializationTest {
+
+ private static final Collection SCOPES =
+ Collections.unmodifiableCollection(Arrays.asList("scope1", "scope2"));
+ private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo");
+ private static final String EXPECTED_ACCOUNT = "serviceAccount";
+
+ @Test
+ void constructor_usesAppIdentityService() throws IOException {
+ String expectedAccessToken = "ExpectedAccessToken";
+
+ MockAppIdentityService appIdentity = new MockAppIdentityService();
+ appIdentity.setAccessTokenText(expectedAccessToken);
+ Credentials credentials =
+ AppEngineCredentials.newBuilder()
+ .setScopes(SCOPES)
+ .setAppIdentityService(appIdentity)
+ .build();
+
+ Map> metadata = credentials.getRequestMetadata(CALL_URI);
+
+ assertEquals(1, appIdentity.getGetAccessTokenCallCount());
+ assertContainsBearerToken(metadata, expectedAccessToken);
+ }
+
+ @Test
+ void refreshAccessToken_sameAs() throws IOException {
+ String expectedAccessToken = "ExpectedAccessToken";
+
+ MockAppIdentityService appIdentity = new MockAppIdentityService();
+ appIdentity.setAccessTokenText(expectedAccessToken);
+ appIdentity.setExpiration(new Date(System.currentTimeMillis() + 60L * 60L * 100L));
+ AppEngineCredentials credentials =
+ AppEngineCredentials.newBuilder()
+ .setScopes(SCOPES)
+ .setAppIdentityService(appIdentity)
+ .build();
+ AccessToken accessToken = credentials.refreshAccessToken();
+ assertEquals(appIdentity.getAccessTokenText(), accessToken.getTokenValue());
+ assertEquals(appIdentity.getExpiration(), accessToken.getExpirationTime());
+ }
+
+ @Test
+ void getAccount_sameAs() {
+ MockAppIdentityService appIdentity = new MockAppIdentityService();
+ appIdentity.setServiceAccountName(EXPECTED_ACCOUNT);
+ AppEngineCredentials credentials =
+ AppEngineCredentials.newBuilder()
+ .setScopes(SCOPES)
+ .setAppIdentityService(appIdentity)
+ .build();
+ assertEquals(EXPECTED_ACCOUNT, credentials.getAccount());
+ }
+
+ @Test
+ void sign_sameAs() {
+ byte[] expectedSignature = {0xD, 0xE, 0xA, 0xD};
+ MockAppIdentityService appIdentity = new MockAppIdentityService();
+ appIdentity.setSignature(expectedSignature);
+ AppEngineCredentials credentials =
+ AppEngineCredentials.newBuilder()
+ .setScopes(SCOPES)
+ .setAppIdentityService(appIdentity)
+ .build();
+ assertArrayEquals(expectedSignature, credentials.sign(expectedSignature));
+ }
+
+ @Test
+ void createScoped_clonesWithScopes() throws IOException {
+ String expectedAccessToken = "ExpectedAccessToken";
+ Collection emptyScopes = Collections.emptyList();
+
+ MockAppIdentityService appIdentity = new MockAppIdentityService();
+ appIdentity.setAccessTokenText(expectedAccessToken);
+
+ AppEngineCredentials credentials =
+ AppEngineCredentials.newBuilder()
+ .setScopes(emptyScopes)
+ .setAppIdentityService(appIdentity)
+ .build();
+ assertTrue(credentials.createScopedRequired());
+ try {
+ credentials.getRequestMetadata(CALL_URI);
+ fail("Should not be able to use credential without scopes.");
+ } catch (Exception expected) {
+ }
+ assertEquals(0, appIdentity.getGetAccessTokenCallCount());
+
+ GoogleCredentials scopedCredentials = credentials.createScoped(SCOPES);
+ assertNotSame(credentials, scopedCredentials);
+
+ Map> metadata = scopedCredentials.getRequestMetadata(CALL_URI);
+
+ assertEquals(1, appIdentity.getGetAccessTokenCallCount());
+ assertContainsBearerToken(metadata, expectedAccessToken);
+ }
+
+ @Test
+ void equals_true() {
+ Collection emptyScopes = Collections.emptyList();
+ MockAppIdentityService appIdentity = new MockAppIdentityService();
+
+ AppEngineCredentials credentials =
+ AppEngineCredentials.newBuilder()
+ .setScopes(emptyScopes)
+ .setAppIdentityService(appIdentity)
+ .build();
+ AppEngineCredentials otherCredentials =
+ AppEngineCredentials.newBuilder()
+ .setScopes(emptyScopes)
+ .setAppIdentityService(appIdentity)
+ .build();
+ assertEquals(credentials, otherCredentials);
+ assertEquals(otherCredentials, credentials);
+ }
+
+ @Test
+ void equals_false_scopes() {
+ Collection emptyScopes = Collections.emptyList();
+ Collection scopes = Collections.singleton("SomeScope");
+ MockAppIdentityService appIdentity = new MockAppIdentityService();
+
+ AppEngineCredentials credentials =
+ AppEngineCredentials.newBuilder()
+ .setScopes(emptyScopes)
+ .setAppIdentityService(appIdentity)
+ .build();
+ AppEngineCredentials otherCredentials =
+ AppEngineCredentials.newBuilder()
+ .setScopes(scopes)
+ .setAppIdentityService(appIdentity)
+ .build();
+ assertNotEquals(credentials, otherCredentials);
+ assertNotEquals(otherCredentials, credentials);
+ }
+
+ @Test
+ void toString_containsFields() {
+ String expectedToString =
+ String.format(
+ "AppEngineCredentials{scopes=[%s], scopesRequired=%b, appIdentityServiceClassName=%s}",
+ "SomeScope", false, MockAppIdentityService.class.getName());
+ Collection scopes = Collections.singleton("SomeScope");
+ MockAppIdentityService appIdentity = new MockAppIdentityService();
+
+ AppEngineCredentials credentials =
+ AppEngineCredentials.newBuilder()
+ .setScopes(scopes)
+ .setAppIdentityService(appIdentity)
+ .build();
+
+ assertEquals(expectedToString, credentials.toString());
+ }
+
+ @Test
+ void hashCode_equals() {
+ Collection emptyScopes = Collections.emptyList();
+ MockAppIdentityService appIdentity = new MockAppIdentityService();
+ AppEngineCredentials credentials =
+ AppEngineCredentials.newBuilder()
+ .setScopes(emptyScopes)
+ .setAppIdentityService(appIdentity)
+ .build();
+ AppEngineCredentials otherCredentials =
+ AppEngineCredentials.newBuilder()
+ .setScopes(emptyScopes)
+ .setAppIdentityService(appIdentity)
+ .build();
+ assertEquals(credentials.hashCode(), otherCredentials.hashCode());
+ }
+
+ @Test
+ void serialize() throws IOException, ClassNotFoundException {
+ Collection scopes = Collections.singleton("SomeScope");
+ MockAppIdentityService appIdentity = new MockAppIdentityService();
+ AppEngineCredentials credentials =
+ AppEngineCredentials.newBuilder()
+ .setScopes(scopes)
+ .setAppIdentityService(appIdentity)
+ .build();
+ GoogleCredentials deserializedCredentials = serializeAndDeserialize(credentials);
+ assertEquals(credentials, deserializedCredentials);
+ assertEquals(credentials.hashCode(), deserializedCredentials.hashCode());
+ assertEquals(credentials.toString(), deserializedCredentials.toString());
+ }
+
+ private static void assertContainsBearerToken(Map> metadata, String token) {
+ assertNotNull(metadata);
+ assertNotNull(token);
+ String expectedValue = "Bearer " + token;
+ List authorizations = metadata.get("Authorization");
+ assertNotNull(authorizations, "Authorization headers not found");
+ assertTrue(authorizations.contains(expectedValue), "Bearer token not found");
+ }
+}
diff --git a/google-auth-library-java/appengine/javatests/com/google/auth/appengine/AppEngineDeserializationSecurityTest.java b/google-auth-library-java/appengine/javatests/com/google/auth/appengine/AppEngineDeserializationSecurityTest.java
new file mode 100644
index 000000000000..414089c0e173
--- /dev/null
+++ b/google-auth-library-java/appengine/javatests/com/google/auth/appengine/AppEngineDeserializationSecurityTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2026, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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.appengine;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.lang.reflect.Field;
+import java.util.Collections;
+import org.junit.jupiter.api.Test;
+
+class AppEngineDeserializationSecurityTest {
+
+ /** A class that does not implement HttpTransportFactory. */
+ static class ArbitraryClass {}
+
+ @Test
+ void testArbitraryClassInstantiationPrevented() throws Exception {
+ AppEngineCredentials credentials =
+ AppEngineCredentials.newBuilder().setScopes(Collections.singleton("scope")).build();
+
+ // Use reflection to set appIdentityServiceClassName to ArbitraryClass
+ // as the setter must be of AppIdentityService
+ Field classNameField =
+ AppEngineCredentials.class.getDeclaredField("appIdentityServiceClassName");
+ classNameField.setAccessible(true);
+ classNameField.set(credentials, ArbitraryClass.class.getName());
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(bos);
+ oos.writeObject(credentials);
+
+ ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
+ ObjectInputStream ois = new ObjectInputStream(bis);
+
+ assertThrows(IOException.class, ois::readObject);
+
+ bos.close();
+ oos.close();
+ bis.close();
+ ois.close();
+ }
+
+ @Test
+ void testValidServiceDeserialization() throws Exception {
+ MockAppIdentityService mockService = new MockAppIdentityService();
+ AppEngineCredentials credentials =
+ AppEngineCredentials.newBuilder()
+ .setScopes(Collections.singleton("scope"))
+ .setAppIdentityService(mockService)
+ .build();
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(bos);
+ oos.writeObject(credentials);
+ oos.close();
+
+ ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
+ ObjectInputStream ois = new ObjectInputStream(bis);
+
+ AppEngineCredentials deserialized = (AppEngineCredentials) ois.readObject();
+
+ assertNotNull(deserialized);
+ Field serviceField = AppEngineCredentials.class.getDeclaredField("appIdentityService");
+ serviceField.setAccessible(true);
+ Object service = serviceField.get(deserialized);
+ assertEquals(MockAppIdentityService.class, service.getClass());
+
+ bos.close();
+ oos.close();
+ bis.close();
+ ois.close();
+ }
+
+ @Test
+ void testNonExistentClassDeserialization() throws Exception {
+ AppEngineCredentials credentials =
+ AppEngineCredentials.newBuilder().setScopes(Collections.singleton("scope")).build();
+
+ // 2. Use reflection to set appIdentityServiceClassName to non-existent class
+ Field classNameField =
+ AppEngineCredentials.class.getDeclaredField("appIdentityServiceClassName");
+ classNameField.setAccessible(true);
+ classNameField.set(credentials, "com.google.nonexistent.Class");
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(bos);
+ oos.writeObject(credentials);
+ oos.close();
+
+ ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
+ ObjectInputStream ois = new ObjectInputStream(bis);
+
+ assertThrows(ClassNotFoundException.class, ois::readObject);
+
+ bos.close();
+ oos.close();
+ bis.close();
+ ois.close();
+ }
+}
diff --git a/google-auth-library-java/appengine/javatests/com/google/auth/appengine/MockAppIdentityService.java b/google-auth-library-java/appengine/javatests/com/google/auth/appengine/MockAppIdentityService.java
new file mode 100644
index 000000000000..c0befa4c7fa9
--- /dev/null
+++ b/google-auth-library-java/appengine/javatests/com/google/auth/appengine/MockAppIdentityService.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2015, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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.appengine;
+
+import com.google.appengine.api.appidentity.AppIdentityService;
+import com.google.appengine.api.appidentity.AppIdentityServiceFailureException;
+import com.google.appengine.api.appidentity.PublicCertificate;
+import java.util.Collection;
+import java.util.Date;
+
+/** Mock implementation of AppIdentityService interface for testing. */
+public class MockAppIdentityService implements AppIdentityService {
+
+ private int getAccessTokenCallCount = 0;
+ private String accessTokenText = null;
+ private Date expiration = null;
+ private String serviceAccountName = null;
+ private SigningResult signingResult = null;
+
+ public MockAppIdentityService() {}
+
+ public int getGetAccessTokenCallCount() {
+ return getAccessTokenCallCount;
+ }
+
+ public String getAccessTokenText() {
+ return accessTokenText;
+ }
+
+ public void setAccessTokenText(String text) {
+ accessTokenText = text;
+ }
+
+ public Date getExpiration() {
+ return expiration;
+ }
+
+ public void setExpiration(Date expiration) {
+ this.expiration = expiration;
+ }
+
+ @Override
+ public SigningResult signForApp(byte[] signBlob) {
+ return signingResult;
+ }
+
+ public void setSignature(byte[] signature) {
+ this.signingResult = new SigningResult("keyName", signature);
+ }
+
+ @Override
+ public Collection getPublicCertificatesForApp() {
+ return null;
+ }
+
+ @Override
+ public GetAccessTokenResult getAccessToken(Iterable scopes) {
+ getAccessTokenCallCount++;
+ int scopeCount = 0;
+ for (String scope : scopes) {
+ if (scope != null) {
+ scopeCount++;
+ }
+ }
+ if (scopeCount == 0) {
+ throw new AppIdentityServiceFailureException("No scopes specified.");
+ }
+ return new GetAccessTokenResult(accessTokenText, expiration);
+ }
+
+ @Override
+ public GetAccessTokenResult getAccessTokenUncached(Iterable scopes) {
+ return null;
+ }
+
+ @Override
+ public String getServiceAccountName() {
+ return serviceAccountName;
+ }
+
+ public void setServiceAccountName(String serviceAccountName) {
+ this.serviceAccountName = serviceAccountName;
+ }
+
+ @Override
+ public ParsedAppId parseFullAppId(String fullAppId) {
+ return null;
+ }
+
+ @Override
+ public String getDefaultGcsBucketName() {
+ return null;
+ }
+}
diff --git a/google-auth-library-java/appengine/pom.xml b/google-auth-library-java/appengine/pom.xml
new file mode 100644
index 000000000000..4c590daf403f
--- /dev/null
+++ b/google-auth-library-java/appengine/pom.xml
@@ -0,0 +1,89 @@
+
+
+ 4.0.0
+
+
+ com.google.auth
+ google-auth-library-parent
+ 1.43.1-SNAPSHOT
+ ../pom.xml
+
+
+ google-auth-library-appengine
+ Google Auth Library for Java - Google App Engine
+
+
+
+ ossrh
+ https://google.oss.sonatype.org/content/repositories/snapshots
+
+
+
+
+ java
+ javatests
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ com.google.auth.appengine
+
+
+
+
+
+
+
+
+
+ com.google.auth
+ google-auth-library-credentials
+
+
+ com.google.auth
+ google-auth-library-oauth2-http
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ provided
+
+
+ com.google.guava
+ guava
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ com.google.auth
+ google-auth-library-oauth2-http
+ test
+ test-jar
+ testlib
+
+
+ com.google.errorprone
+ error_prone_annotations
+ compile
+
+
+
diff --git a/google-auth-library-java/bom/README.md b/google-auth-library-java/bom/README.md
new file mode 100644
index 000000000000..3e1a0964c3b2
--- /dev/null
+++ b/google-auth-library-java/bom/README.md
@@ -0,0 +1,28 @@
+# Google Auth Library Bill of Materials
+
+The `google-auth-library-bom` is a pom that can be used to import consistent versions of
+`google-auth-library` components plus its dependencies.
+
+To use it in Maven, add the following to your `pom.xml`:
+
+[//]: # ({x-version-update-start:google-auth-library-bom:released})
+```xml
+
+
+
+ com.google.auth
+ google-auth-library-bom
+ 0.17.1
+ pom
+ import
+
+
+
+```
+[//]: # ({x-version-update-end})
+
+## License
+
+Apache 2.0 - See [LICENSE] for more information.
+
+[LICENSE]: https://github.com/googleapis/google-auth-library-java/blob/main/LICENSE
diff --git a/google-auth-library-java/bom/pom.xml b/google-auth-library-java/bom/pom.xml
new file mode 100644
index 000000000000..3369e1f4fe25
--- /dev/null
+++ b/google-auth-library-java/bom/pom.xml
@@ -0,0 +1,177 @@
+
+
+ 4.0.0
+ com.google.auth
+ google-auth-library-bom
+ 1.43.1-SNAPSHOT
+ pom
+ Google Auth Library for Java BOM
+
+ BOM for Google Auth Library for Java
+
+ https://github.com/googleapis/google-cloud-java
+
+
+
+ sonatype-nexus-snapshots
+ https://google.oss.sonatype.org/content/repositories/snapshots
+
+
+
+
+
+ Apache-2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ Google
+ http://www.google.com/
+
+
+
+ scm:git:https://github.com/googleapis/google-cloud-java.git
+ scm:git:https://github.com/googleapis/google-cloud-java.git
+ https://github.com/googleapis/google-cloud-java
+
+
+
+
+ Jeff Ching
+ chingor@google.com
+
+ developer
+
+
+
+
+
+
+
+
+
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.7.0
+ true
+
+ ossrh
+ https://google.oss.sonatype.org/
+ false
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.11.2
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+ 3.21.0
+
+ true
+
+
+
+
+ com.spotify.fmt
+ fmt-maven-plugin
+ 2.25
+
+ true
+
+
+
+
+
+
+
+
+ release-sonatype
+
+
+
+ !artifact-registry-url
+
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+
+
+
+
+
+
+ release-gcp-artifact-registry
+
+ artifactregistry://undefined-artifact-registry-url-value
+
+
+
+ gcp-artifact-registry-repository
+ ${artifact-registry-url}
+
+
+ gcp-artifact-registry-repository
+ ${artifact-registry-url}
+
+
+
+
+ release-sign-artifacts
+
+
+ performRelease
+ true
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.7
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
+
+
+
diff --git a/google-auth-library-java/cab-token-generator/gencode-guide.md b/google-auth-library-java/cab-token-generator/gencode-guide.md
new file mode 100644
index 000000000000..56a347096b54
--- /dev/null
+++ b/google-auth-library-java/cab-token-generator/gencode-guide.md
@@ -0,0 +1,17 @@
+Code under `cab-token-generator/java/com/google/auth/credentialaccessboundary/protobuf` are generated manually.
+To re-generate, follow steps below:
+
+Determine the protoc version to use for generate, this guide will use v33.2 as example.
+Steps to generate the java code using protoc 33.2.
+1. Download these files from https://github.com/protocolbuffers/protobuf/releases/tag/v33.2
+```
+src/google/protobuf/duration.proto
+src/google/protobuf/struct.proto
+src/google/protobuf/timestamp.proto
+```
+2. Create a workspace in g3, copy the above files in `google/protobuf` directory.
+3. Run the following command to generate java code:
+```sh
+# in google3 directory
+~/.local/bin/protoc --java_out ~/Downloads/java-output -I. cloud/identity/unifiedauth/proto/client_side_access_boundary.proto
+```
diff --git a/google-auth-library-java/cab-token-generator/java/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactory.java b/google-auth-library-java/cab-token-generator/java/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactory.java
new file mode 100644
index 000000000000..cb1e1caa89b8
--- /dev/null
+++ b/google-auth-library-java/cab-token-generator/java/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactory.java
@@ -0,0 +1,772 @@
+/*
+ * Copyright 2025, 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.credentialaccessboundary;
+
+import static com.google.auth.oauth2.OAuth2Credentials.getFromServiceLoader;
+import static com.google.auth.oauth2.OAuth2Utils.TOKEN_EXCHANGE_URL_FORMAT;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.api.client.util.Clock;
+import com.google.auth.Credentials;
+import com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundary;
+import com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryRule;
+import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.CredentialAccessBoundary;
+import com.google.auth.oauth2.CredentialAccessBoundary.AccessBoundaryRule;
+import com.google.auth.oauth2.DownscopedCredentials;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.auth.oauth2.OAuth2CredentialsWithRefresh;
+import com.google.auth.oauth2.OAuth2Utils;
+import com.google.auth.oauth2.StsRequestHandler;
+import com.google.auth.oauth2.StsTokenExchangeRequest;
+import com.google.auth.oauth2.StsTokenExchangeResponse;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.util.concurrent.AbstractFuture;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListenableFutureTask;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.RegistryConfiguration;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import com.google.crypto.tink.aead.AeadConfig;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import dev.cel.common.CelAbstractSyntaxTree;
+import dev.cel.common.CelOptions;
+import dev.cel.common.CelProtoAbstractSyntaxTree;
+import dev.cel.common.CelValidationException;
+import dev.cel.compiler.CelCompiler;
+import dev.cel.compiler.CelCompilerFactory;
+import dev.cel.expr.Expr;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import javax.annotation.Nullable;
+
+/**
+ * A factory for generating downscoped access tokens using a client-side approach.
+ *
+ *
Downscoped tokens enable the ability to downscope, or restrict, the Identity and Access
+ * Management (IAM) permissions that a short-lived credential can use for accessing Google Cloud
+ * Storage. This factory allows clients to efficiently generate multiple downscoped tokens locally,
+ * minimizing calls to the Security Token Service (STS). This client-side approach is particularly
+ * beneficial when Credential Access Boundary rules change frequently or when many unique downscoped
+ * tokens are required. For scenarios where rules change infrequently or a single downscoped
+ * credential is reused many times, the server-side approach using {@link DownscopedCredentials} is
+ * more appropriate.
+ *
+ *
To downscope permissions you must define a {@link CredentialAccessBoundary} which specifies
+ * the upper bound of permissions that the credential can access. You must also provide a source
+ * credential which will be used to acquire the downscoped credential.
+ *
+ *
The factory can be configured with options such as the {@code refreshMargin} and {@code
+ * minimumTokenLifetime}. The {@code refreshMargin} controls how far in advance of the underlying
+ * credentials' expiry a refresh is attempted. The {@code minimumTokenLifetime} ensures that
+ * generated tokens have a minimum usable lifespan. See the {@link Builder} class for more details
+ * on these options.
+ *
+ *
+ *
+ * Note that {@link OAuth2CredentialsWithRefresh} can instead be used to consume the downscoped
+ * token, allowing for automatic token refreshes by providing a {@link
+ * OAuth2CredentialsWithRefresh.OAuth2RefreshHandler}.
+ */
+public class ClientSideCredentialAccessBoundaryFactory {
+ static final Duration DEFAULT_REFRESH_MARGIN = Duration.ofMinutes(45);
+ static final Duration DEFAULT_MINIMUM_TOKEN_LIFETIME = Duration.ofMinutes(30);
+ private final GoogleCredentials sourceCredential;
+ private final transient HttpTransportFactory transportFactory;
+ private final String tokenExchangeEndpoint;
+ private final Duration minimumTokenLifetime;
+ private final Duration refreshMargin;
+ private RefreshTask refreshTask;
+ private final Object refreshLock = new byte[0];
+ private IntermediateCredentials intermediateCredentials = null;
+ private final Clock clock;
+ private final CelCompiler celCompiler;
+
+ enum RefreshType {
+ NONE,
+ ASYNC,
+ BLOCKING
+ }
+
+ private ClientSideCredentialAccessBoundaryFactory(Builder builder) {
+ this.transportFactory = builder.transportFactory;
+ this.sourceCredential = builder.sourceCredential;
+ this.tokenExchangeEndpoint = builder.tokenExchangeEndpoint;
+ this.refreshMargin = builder.refreshMargin;
+ this.minimumTokenLifetime = builder.minimumTokenLifetime;
+ this.clock = builder.clock;
+
+ // Initializes the Tink AEAD registry for encrypting the client-side restrictions.
+ try {
+ AeadConfig.register();
+ } catch (GeneralSecurityException e) {
+ throw new IllegalStateException("Error occurred when registering Tink", e);
+ }
+
+ CelOptions options = CelOptions.current().build();
+ this.celCompiler = CelCompilerFactory.standardCelCompilerBuilder().setOptions(options).build();
+ }
+
+ /**
+ * Generates a downscoped access token given the {@link CredentialAccessBoundary}.
+ *
+ * @param accessBoundary The credential access boundary that defines the restrictions for the
+ * generated CAB token.
+ * @return The downscoped access token in an {@link AccessToken} object
+ * @throws IOException If an I/O error occurs while refreshing the source credentials
+ * @throws CelValidationException If the availability condition is an invalid CEL expression
+ * @throws GeneralSecurityException If an error occurs during encryption
+ */
+ public AccessToken generateToken(CredentialAccessBoundary accessBoundary)
+ throws IOException, CelValidationException, GeneralSecurityException {
+ this.refreshCredentialsIfRequired();
+
+ String intermediateToken;
+ String sessionKey;
+ Date intermediateTokenExpirationTime;
+
+ synchronized (refreshLock) {
+ intermediateToken = this.intermediateCredentials.intermediateAccessToken.getTokenValue();
+ intermediateTokenExpirationTime =
+ this.intermediateCredentials.intermediateAccessToken.getExpirationTime();
+ sessionKey = this.intermediateCredentials.accessBoundarySessionKey;
+ }
+
+ byte[] rawRestrictions = this.serializeCredentialAccessBoundary(accessBoundary);
+
+ byte[] encryptedRestrictions = this.encryptRestrictions(rawRestrictions, sessionKey);
+
+ // withoutPadding() is used to stay consistent with server-side CAB
+ // withoutPadding() avoids additional URL encoded token issues (i.e. extra equal signs `=` in
+ // the path)
+ String tokenValue =
+ intermediateToken
+ + "."
+ + Base64.getUrlEncoder().withoutPadding().encodeToString(encryptedRestrictions);
+
+ return new AccessToken(tokenValue, intermediateTokenExpirationTime);
+ }
+
+ /**
+ * Refreshes the intermediate access token and access boundary session key if required.
+ *
+ *
This method checks the expiration time of the current intermediate access token and
+ * initiates a refresh if necessary. The refresh process also refreshes the underlying source
+ * credentials.
+ *
+ * @throws IOException If an error occurs during the refresh process, such as network issues,
+ * invalid credentials, or problems with the token exchange endpoint.
+ */
+ @VisibleForTesting
+ void refreshCredentialsIfRequired() throws IOException {
+ RefreshType refreshType = determineRefreshType();
+
+ if (refreshType == RefreshType.NONE) {
+ // No refresh needed, token is still valid.
+ return;
+ }
+
+ // If a refresh is required, create or retrieve the refresh task.
+ RefreshTask currentRefreshTask = getOrCreateRefreshTask();
+
+ // Handle the refresh based on the determined refresh type.
+ switch (refreshType) {
+ case BLOCKING:
+ if (currentRefreshTask.isNew) {
+ // Start a new refresh task only if the task is new.
+ MoreExecutors.directExecutor().execute(currentRefreshTask.task);
+ }
+ try {
+ // Wait for the refresh task to complete.
+ currentRefreshTask.task.get();
+ } catch (InterruptedException e) {
+ // Restore the interrupted status and throw an exception.
+ Thread.currentThread().interrupt();
+ throw new IOException(
+ "Interrupted while asynchronously refreshing the intermediate credentials", e);
+ } catch (ExecutionException e) {
+ // Unwrap the underlying cause of the execution exception.
+ Throwable cause = e.getCause();
+ if (cause instanceof IOException) {
+ throw (IOException) cause;
+ } else if (cause instanceof RuntimeException) {
+ throw (RuntimeException) cause;
+ } else {
+ // Wrap other exceptions in an IOException.
+ throw new IOException("Unexpected error refreshing intermediate credentials", cause);
+ }
+ }
+ break;
+ case ASYNC:
+ if (currentRefreshTask.isNew) {
+ // Starts a new background thread for the refresh task if it's a new task.
+ // We create a new thread because the Auth Library doesn't currently include a background
+ // executor. Introducing an executor would add complexity in managing its lifecycle and
+ // could potentially lead to memory leaks.
+ // We limit the number of concurrent refresh threads to 1, so the overhead of creating new
+ // threads for asynchronous calls should be acceptable.
+ new Thread(currentRefreshTask.task).start();
+ } // (No else needed - if not new, another thread is handling the refresh)
+ break;
+ default:
+ // This should not happen unless RefreshType enum is extended and this method is not
+ // updated.
+ throw new IllegalStateException("Unexpected refresh type: " + refreshType);
+ }
+ }
+
+ private RefreshType determineRefreshType() {
+ AccessToken intermediateAccessToken;
+ synchronized (refreshLock) {
+ if (intermediateCredentials == null
+ || intermediateCredentials.intermediateAccessToken == null) {
+ // A blocking refresh is needed if the intermediate access token doesn't exist.
+ return RefreshType.BLOCKING;
+ }
+ intermediateAccessToken = intermediateCredentials.intermediateAccessToken;
+ }
+
+ Date expirationTime = intermediateAccessToken.getExpirationTime();
+ if (expirationTime == null) {
+ // Token does not expire, no refresh needed.
+ return RefreshType.NONE;
+ }
+
+ Duration remaining = Duration.ofMillis(expirationTime.getTime() - clock.currentTimeMillis());
+
+ if (remaining.compareTo(minimumTokenLifetime) <= 0) {
+ // Intermediate token has expired or remaining lifetime is less than the minimum required
+ // for CAB token generation. A blocking refresh is necessary.
+ return RefreshType.BLOCKING;
+ } else if (remaining.compareTo(refreshMargin) <= 0) {
+ // The token is nearing expiration, an async refresh is needed.
+ return RefreshType.ASYNC;
+ }
+ // Token is still fresh, no refresh needed.
+ return RefreshType.NONE;
+ }
+
+ /**
+ * Atomically creates a single flight refresh task.
+ *
+ *
Only a single refresh task can be scheduled at a time. If there is an existing task, it will
+ * be returned for subsequent invocations. However, if a new task is created, it is the
+ * responsibility of the caller to execute it. The task will clear the single flight slot upon
+ * completion.
+ */
+ private RefreshTask getOrCreateRefreshTask() {
+ synchronized (refreshLock) {
+ if (refreshTask != null) {
+ // An existing refresh task is already in progress. Return a NEW RefreshTask instance with
+ // the existing task, but set isNew to false. This indicates to the caller that a new
+ // refresh task was NOT created.
+ return new RefreshTask(refreshTask.task, false);
+ }
+
+ final ListenableFutureTask task =
+ ListenableFutureTask.create(this::fetchIntermediateCredentials);
+
+ // Store the new refresh task in the refreshTask field before returning. This ensures that
+ // subsequent calls to this method will return the existing task while it's still in progress.
+ refreshTask = new RefreshTask(task, true);
+ return refreshTask;
+ }
+ }
+
+ /**
+ * Fetches the credentials by refreshing the source credential and exchanging it for an
+ * intermediate access token using the STS endpoint.
+ *
+ *
The source credential is refreshed, and a token exchange request is made to the STS endpoint
+ * to obtain an intermediate access token and an associated access boundary session key. This
+ * ensures the intermediate access token meets this factory's refresh margin and minimum lifetime
+ * requirements.
+ *
+ * @return The fetched {@link IntermediateCredentials} containing the intermediate access token
+ * and access boundary session key.
+ * @throws IOException If an error occurs during credential refresh or token exchange.
+ */
+ @VisibleForTesting
+ IntermediateCredentials fetchIntermediateCredentials() throws IOException {
+ try {
+ // Force a refresh on the source credentials. The intermediate token's lifetime is tied to the
+ // source credential's expiration. The factory's refreshMargin might be different from the
+ // refreshMargin on source credentials. This ensures the intermediate access token
+ // meets this factory's refresh margin and minimum lifetime requirements.
+ sourceCredential.refresh();
+ } catch (IOException e) {
+ throw new IOException("Unable to refresh the provided source credential.", e);
+ }
+
+ AccessToken sourceAccessToken = sourceCredential.getAccessToken();
+ if (sourceAccessToken == null || Strings.isNullOrEmpty(sourceAccessToken.getTokenValue())) {
+ throw new IllegalStateException("The source credential does not have an access token.");
+ }
+
+ StsTokenExchangeRequest request =
+ StsTokenExchangeRequest.newBuilder(
+ sourceAccessToken.getTokenValue(), OAuth2Utils.TOKEN_TYPE_ACCESS_TOKEN)
+ .setRequestTokenType(OAuth2Utils.TOKEN_TYPE_ACCESS_BOUNDARY_INTERMEDIARY_TOKEN)
+ .build();
+
+ StsRequestHandler handler =
+ StsRequestHandler.newBuilder(
+ tokenExchangeEndpoint, request, transportFactory.create().createRequestFactory())
+ .build();
+
+ StsTokenExchangeResponse response = handler.exchangeToken();
+ return new IntermediateCredentials(
+ getTokenFromResponse(response, sourceAccessToken), response.getAccessBoundarySessionKey());
+ }
+
+ /**
+ * Extracts the access token from the STS exchange response and sets the appropriate expiration
+ * time.
+ *
+ * @param response The STS token exchange response.
+ * @param sourceAccessToken The original access token used for the exchange.
+ * @return The intermediate access token.
+ */
+ private static AccessToken getTokenFromResponse(
+ StsTokenExchangeResponse response, AccessToken sourceAccessToken) {
+ AccessToken intermediateToken = response.getAccessToken();
+
+ // The STS endpoint will only return the expiration time for the intermediate token
+ // if the original access token represents a service account.
+ // The intermediate token's expiration time will always match the source credential expiration.
+ // When no expires_in is returned, we can copy the source credential's expiration time.
+ if (intermediateToken.getExpirationTime() == null
+ && sourceAccessToken.getExpirationTime() != null) {
+ return new AccessToken(
+ intermediateToken.getTokenValue(), sourceAccessToken.getExpirationTime());
+ }
+
+ // Return original if no modification needed.
+ return intermediateToken;
+ }
+
+ /**
+ * Completes the refresh task by storing the results and clearing the single flight slot.
+ *
+ *
This method is called when a refresh task finishes. It stores the refreshed credentials if
+ * successful. The single-flight "slot" is cleared, allowing subsequent refresh attempts. Any
+ * exceptions during the refresh are caught and suppressed to prevent indefinite blocking of
+ * subsequent refresh attempts.
+ */
+ private void finishRefreshTask(ListenableFuture finishedTask)
+ throws ExecutionException {
+ synchronized (refreshLock) {
+ try {
+ this.intermediateCredentials = Futures.getDone(finishedTask);
+ } finally {
+ if (this.refreshTask != null && this.refreshTask.task == finishedTask) {
+ this.refreshTask = null;
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ String getTokenExchangeEndpoint() {
+ return tokenExchangeEndpoint;
+ }
+
+ @VisibleForTesting
+ HttpTransportFactory getTransportFactory() {
+ return transportFactory;
+ }
+
+ @VisibleForTesting
+ Duration getRefreshMargin() {
+ return refreshMargin;
+ }
+
+ @VisibleForTesting
+ Duration getMinimumTokenLifetime() {
+ return minimumTokenLifetime;
+ }
+
+ /**
+ * Holds intermediate credentials obtained from the STS token exchange endpoint.
+ *
+ *
These credentials include an intermediate access token and an access boundary session key.
+ */
+ @VisibleForTesting
+ static class IntermediateCredentials {
+ private final AccessToken intermediateAccessToken;
+ private final String accessBoundarySessionKey;
+
+ IntermediateCredentials(AccessToken accessToken, String accessBoundarySessionKey) {
+ this.intermediateAccessToken = accessToken;
+ this.accessBoundarySessionKey = accessBoundarySessionKey;
+ }
+
+ String getAccessBoundarySessionKey() {
+ return accessBoundarySessionKey;
+ }
+
+ AccessToken getIntermediateAccessToken() {
+ return intermediateAccessToken;
+ }
+ }
+
+ /**
+ * Represents a task for refreshing intermediate credentials, ensuring that only one refresh
+ * operation is in progress at a time.
+ *
+ *
The {@code isNew} flag indicates whether this is a newly initiated refresh operation or an
+ * existing one already in progress. This distinction is used to prevent redundant refreshes.
+ */
+ class RefreshTask extends AbstractFuture implements Runnable {
+ private final ListenableFutureTask task;
+ final boolean isNew;
+
+ RefreshTask(ListenableFutureTask task, boolean isNew) {
+ this.task = task;
+ this.isNew = isNew;
+
+ // Add listener to update factory's credentials when the task completes.
+ task.addListener(
+ () -> {
+ try {
+ finishRefreshTask(task);
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ RefreshTask.this.setException(cause);
+ }
+ },
+ MoreExecutors.directExecutor());
+
+ // Add callback to set the result or exception based on the outcome.
+ Futures.addCallback(
+ task,
+ new FutureCallback() {
+ @Override
+ public void onSuccess(IntermediateCredentials result) {
+ RefreshTask.this.set(result);
+ }
+
+ @Override
+ public void onFailure(@Nullable Throwable t) {
+ RefreshTask.this.setException(
+ t != null ? t : new IOException("Refresh failed with null Throwable."));
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ @Override
+ public void run() {
+ task.run();
+ }
+ }
+
+ /** Serializes a {@link CredentialAccessBoundary} object into Protobuf wire format. */
+ @VisibleForTesting
+ byte[] serializeCredentialAccessBoundary(CredentialAccessBoundary credentialAccessBoundary)
+ throws CelValidationException {
+ List rules = credentialAccessBoundary.getAccessBoundaryRules();
+ ClientSideAccessBoundary.Builder accessBoundaryBuilder = ClientSideAccessBoundary.newBuilder();
+
+ for (AccessBoundaryRule rule : rules) {
+ ClientSideAccessBoundaryRule.Builder ruleBuilder =
+ accessBoundaryBuilder
+ .addAccessBoundaryRulesBuilder()
+ .addAllAvailablePermissions(rule.getAvailablePermissions())
+ .setAvailableResource(rule.getAvailableResource());
+
+ // Availability condition is an optional field from the CredentialAccessBoundary
+ // CEL compilation is only performed if there is a non-empty availability condition.
+ if (rule.getAvailabilityCondition() != null) {
+ String availabilityCondition = rule.getAvailabilityCondition().getExpression();
+
+ Expr availabilityConditionExpr = this.compileCel(availabilityCondition);
+ ruleBuilder.setCompiledAvailabilityCondition(availabilityConditionExpr);
+ }
+ }
+
+ return accessBoundaryBuilder.build().toByteArray();
+ }
+
+ /** Compiles CEL expression from String to an {@link Expr} proto object. */
+ private Expr compileCel(String expr) throws CelValidationException {
+ CelAbstractSyntaxTree ast = celCompiler.parse(expr).getAst();
+
+ CelProtoAbstractSyntaxTree astProto = CelProtoAbstractSyntaxTree.fromCelAst(ast);
+
+ return astProto.getExpr();
+ }
+
+ /** Encrypts the given bytes using a sessionKey using Tink Aead. */
+ private byte[] encryptRestrictions(byte[] restriction, String sessionKey)
+ throws GeneralSecurityException {
+ byte[] rawKey;
+
+ try {
+ rawKey = Base64.getDecoder().decode(sessionKey);
+ } catch (IllegalArgumentException e) {
+ // Session key from the server is expected to be Base64 encoded.
+ throw new IllegalStateException("Session key is not Base64 encoded", e);
+ }
+
+ KeysetHandle keysetHandle =
+ TinkProtoKeysetFormat.parseKeyset(rawKey, InsecureSecretKeyAccess.get());
+
+ Aead aead = keysetHandle.getPrimitive(RegistryConfiguration.get(), Aead.class);
+
+ // For downscoped access token encryption, empty associated data is expected.
+ // Tink requires a byte[0] to be passed for this case.
+ return aead.encrypt(restriction, /* associatedData= */ new byte[0]);
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder for {@link ClientSideCredentialAccessBoundaryFactory}.
+ *
+ *
Use this builder to create instances of {@code ClientSideCredentialAccessBoundaryFactory}
+ * with the desired configuration options.
+ */
+ public static class Builder {
+ private GoogleCredentials sourceCredential;
+ private HttpTransportFactory transportFactory;
+ private String universeDomain;
+ private String tokenExchangeEndpoint;
+ private Duration minimumTokenLifetime;
+ private Duration refreshMargin;
+ private Clock clock = Clock.SYSTEM; // Default to system clock;
+
+ private Builder() {}
+
+ /**
+ * Sets the required source credential.
+ *
+ * @param sourceCredential the {@code GoogleCredentials} to set. This is a
+ * required parameter.
+ * @return this {@code Builder} object for chaining.
+ * @throws NullPointerException if {@code sourceCredential} is {@code null}.
+ */
+ @CanIgnoreReturnValue
+ public Builder setSourceCredential(GoogleCredentials sourceCredential) {
+ checkNotNull(sourceCredential, "Source credential must not be null.");
+ this.sourceCredential = sourceCredential;
+ return this;
+ }
+
+ /**
+ * Sets the minimum acceptable lifetime for a generated downscoped access token.
+ *
+ *
This parameter ensures that any generated downscoped access token has a minimum validity
+ * period. If the time remaining before the underlying credentials expire is less than this
+ * value, the factory will perform a blocking refresh, meaning that it will wait until the
+ * credentials are refreshed before generating a new downscoped token. This guarantees that the
+ * generated token will be valid for at least {@code minimumTokenLifetime}. A reasonable value
+ * should be chosen based on the expected duration of operations using the downscoped token. If
+ * not set, the default value is defined by {@link #DEFAULT_MINIMUM_TOKEN_LIFETIME}.
+ *
+ * @param minimumTokenLifetime The minimum acceptable lifetime for a generated downscoped access
+ * token. Must be greater than zero.
+ * @return This {@code Builder} object.
+ * @throws IllegalArgumentException if {@code minimumTokenLifetime} is negative or zero.
+ */
+ @CanIgnoreReturnValue
+ public Builder setMinimumTokenLifetime(Duration minimumTokenLifetime) {
+ checkNotNull(minimumTokenLifetime, "Minimum token lifetime must not be null.");
+ if (minimumTokenLifetime.isNegative() || minimumTokenLifetime.isZero()) {
+ throw new IllegalArgumentException("Minimum token lifetime must be greater than zero.");
+ }
+ this.minimumTokenLifetime = minimumTokenLifetime;
+ return this;
+ }
+
+ /**
+ * Sets the refresh margin for the underlying credentials.
+ *
+ *
This duration specifies how far in advance of the credentials' expiration time an
+ * asynchronous refresh should be initiated. This refresh happens in the background, without
+ * blocking the main thread. If not provided, it will default to the value defined by {@link
+ * #DEFAULT_REFRESH_MARGIN}.
+ *
+ *
Note: The {@code refreshMargin} must be at least one minute longer than the {@code
+ * minimumTokenLifetime}.
+ *
+ * @param refreshMargin The refresh margin. Must be greater than zero.
+ * @return This {@code Builder} object.
+ * @throws IllegalArgumentException if {@code refreshMargin} is negative or zero.
+ */
+ @CanIgnoreReturnValue
+ public Builder setRefreshMargin(Duration refreshMargin) {
+ checkNotNull(refreshMargin, "Refresh margin must not be null.");
+ if (refreshMargin.isNegative() || refreshMargin.isZero()) {
+ throw new IllegalArgumentException("Refresh margin must be greater than zero.");
+ }
+ this.refreshMargin = refreshMargin;
+ return this;
+ }
+
+ /**
+ * Sets the HTTP transport factory.
+ *
+ * @param transportFactory the {@code HttpTransportFactory} to set
+ * @return this {@code Builder} object
+ */
+ @CanIgnoreReturnValue
+ public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
+ this.transportFactory = transportFactory;
+ return this;
+ }
+
+ /**
+ * Sets the optional universe domain.
+ *
+ * @param universeDomain the universe domain to set
+ * @return this {@code Builder} object
+ */
+ @CanIgnoreReturnValue
+ public Builder setUniverseDomain(String universeDomain) {
+ this.universeDomain = universeDomain;
+ return this;
+ }
+
+ /**
+ * Set the clock for checking token expiry. Used for testing.
+ *
+ * @param clock the clock to use. Defaults to the system clock
+ * @return the builder
+ */
+ public Builder setClock(Clock clock) {
+ this.clock = clock;
+ return this;
+ }
+
+ /**
+ * Creates a new {@code ClientSideCredentialAccessBoundaryFactory} instance based on the current
+ * builder configuration.
+ *
+ * @return A new {@code ClientSideCredentialAccessBoundaryFactory} instance.
+ * @throws IllegalStateException if the builder is not properly configured (e.g., if the source
+ * credential is not set).
+ * @throws IllegalArgumentException if the refresh margin is not at least one minute longer than
+ * the minimum token lifetime.
+ */
+ public ClientSideCredentialAccessBoundaryFactory build() {
+ checkNotNull(sourceCredential, "Source credential must not be null.");
+
+ // Use the default HTTP transport factory if none was provided.
+ if (transportFactory == null) {
+ this.transportFactory =
+ getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+ }
+
+ // Default to GDU when not supplied.
+ if (Strings.isNullOrEmpty(universeDomain)) {
+ this.universeDomain = Credentials.GOOGLE_DEFAULT_UNIVERSE;
+ }
+
+ // Ensure source credential's universe domain matches.
+ try {
+ if (!universeDomain.equals(sourceCredential.getUniverseDomain())) {
+ throw new IllegalArgumentException(
+ "The client side access boundary credential's universe domain must be the same as the source "
+ + "credential.");
+ }
+ } catch (IOException e) {
+ // Throwing an IOException would be a breaking change, so wrap it here.
+ throw new IllegalStateException(
+ "Error occurred when attempting to retrieve source credential universe domain.", e);
+ }
+
+ // Use default values for refreshMargin if not provided.
+ if (refreshMargin == null) {
+ this.refreshMargin = DEFAULT_REFRESH_MARGIN;
+ }
+
+ // Use default values for minimumTokenLifetime if not provided.
+ if (minimumTokenLifetime == null) {
+ this.minimumTokenLifetime = DEFAULT_MINIMUM_TOKEN_LIFETIME;
+ }
+
+ // Check if refreshMargin is at least one minute longer than minimumTokenLifetime.
+ Duration minRefreshMargin = minimumTokenLifetime.plusMinutes(1);
+ if (refreshMargin.compareTo(minRefreshMargin) < 0) {
+ throw new IllegalArgumentException(
+ "Refresh margin must be at least one minute longer than the minimum token lifetime.");
+ }
+
+ this.tokenExchangeEndpoint = String.format(TOKEN_EXCHANGE_URL_FORMAT, universeDomain);
+ return new ClientSideCredentialAccessBoundaryFactory(this);
+ }
+ }
+}
diff --git a/google-auth-library-java/cab-token-generator/java/com/google/auth/credentialaccessboundary/protobuf/ClientSideAccessBoundary.java b/google-auth-library-java/cab-token-generator/java/com/google/auth/credentialaccessboundary/protobuf/ClientSideAccessBoundary.java
new file mode 100644
index 000000000000..0492021ea1e5
--- /dev/null
+++ b/google-auth-library-java/cab-token-generator/java/com/google/auth/credentialaccessboundary/protobuf/ClientSideAccessBoundary.java
@@ -0,0 +1,1103 @@
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// NO CHECKED-IN PROTOBUF GENCODE
+// source: cloud/identity/unifiedauth/proto/client_side_access_boundary.proto
+// Protobuf Java Version: 4.33.2
+
+package com.google.auth.credentialaccessboundary.protobuf;
+
+/**
+ *
+ *
+ *
+ * An access boundary defines the upper bound of what a principal may access. It
+ * includes a list of client-side access boundary rules that each defines the
+ * resource that may be allowed as well as permissions that may be used on those
+ * resources.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * An access boundary defines the upper bound of what a principal may access. It
+ * includes a list of client-side access boundary rules that each defines the
+ * resource that may be allowed as well as permissions that may be used on those
+ * resources.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * A list of client-side access boundary rules which defines the upper bound
+ * of the permission a principal may carry. If multiple rules are specified,
+ * the effective access boundary is the union of all the access boundary rules
+ * attached.
+ *
+ * An access boundary rule that defines an upper bound of IAM
+ * permissions on a single resource. This proto has a compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto). It is used to
+ * format the access boundary restriction in the Client-Side CAB access token.
+ *
+ * The full resource name of a Google Cloud resource entity.
+ * The format definition is at
+ * https://cloud.google.com/apis/design/resource_names.
+ *
+ * Example value: `//cloudresourcemanager.googleapis.com/projects/my-project`.
+ *
+ * The full resource name of a Google Cloud resource entity.
+ * The format definition is at
+ * https://cloud.google.com/apis/design/resource_names.
+ *
+ * Example value: `//cloudresourcemanager.googleapis.com/projects/my-project`.
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ *
+ * repeated string available_permissions = 2;
+ *
+ * @return A list containing the availablePermissions.
+ */
+ public com.google.protobuf.ProtocolStringList getAvailablePermissionsList() {
+ return availablePermissions_;
+ }
+
+ /**
+ *
+ *
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ *
+ * repeated string available_permissions = 2;
+ *
+ * @return The count of availablePermissions.
+ */
+ public int getAvailablePermissionsCount() {
+ return availablePermissions_.size();
+ }
+
+ /**
+ *
+ *
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ *
+ * repeated string available_permissions = 2;
+ *
+ * @param index The index of the element to return.
+ * @return The availablePermissions at the given index.
+ */
+ public java.lang.String getAvailablePermissions(int index) {
+ return availablePermissions_.get(index);
+ }
+
+ /**
+ *
+ *
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ *
+ * repeated string available_permissions = 2;
+ *
+ * @param index The index of the value to return.
+ * @return The bytes of the availablePermissions at the given index.
+ */
+ public com.google.protobuf.ByteString getAvailablePermissionsBytes(int index) {
+ return availablePermissions_.getByteString(index);
+ }
+
+ public static final int COMPILED_AVAILABILITY_CONDITION_FIELD_NUMBER = 4;
+ private dev.cel.expr.Expr compiledAvailabilityCondition_;
+
+ /**
+ *
+ *
+ *
+ * The compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto) with limited
+ * function support.
+ *
+ * The compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto) with limited
+ * function support.
+ *
+ * The compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto) with limited
+ * function support.
+ *
+ * An access boundary rule that defines an upper bound of IAM
+ * permissions on a single resource. This proto has a compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto). It is used to
+ * format the access boundary restriction in the Client-Side CAB access token.
+ *
+ * The full resource name of a Google Cloud resource entity.
+ * The format definition is at
+ * https://cloud.google.com/apis/design/resource_names.
+ *
+ * Example value: `//cloudresourcemanager.googleapis.com/projects/my-project`.
+ *
+ * The full resource name of a Google Cloud resource entity.
+ * The format definition is at
+ * https://cloud.google.com/apis/design/resource_names.
+ *
+ * Example value: `//cloudresourcemanager.googleapis.com/projects/my-project`.
+ *
+ * The full resource name of a Google Cloud resource entity.
+ * The format definition is at
+ * https://cloud.google.com/apis/design/resource_names.
+ *
+ * Example value: `//cloudresourcemanager.googleapis.com/projects/my-project`.
+ *
+ *
+ * string available_resource = 1 [features = { ... }
+ *
+ * @param value The availableResource to set.
+ * @return This builder for chaining.
+ */
+ public Builder setAvailableResource(java.lang.String value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ availableResource_ = value;
+ bitField0_ |= 0x00000001;
+ onChanged();
+ return this;
+ }
+
+ /**
+ *
+ *
+ *
+ * The full resource name of a Google Cloud resource entity.
+ * The format definition is at
+ * https://cloud.google.com/apis/design/resource_names.
+ *
+ * Example value: `//cloudresourcemanager.googleapis.com/projects/my-project`.
+ *
+ * The full resource name of a Google Cloud resource entity.
+ * The format definition is at
+ * https://cloud.google.com/apis/design/resource_names.
+ *
+ * Example value: `//cloudresourcemanager.googleapis.com/projects/my-project`.
+ *
+ *
+ * string available_resource = 1 [features = { ... }
+ *
+ * @param value The bytes for availableResource to set.
+ * @return This builder for chaining.
+ */
+ public Builder setAvailableResourceBytes(com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ checkByteStringIsUtf8(value);
+ availableResource_ = value;
+ bitField0_ |= 0x00000001;
+ onChanged();
+ return this;
+ }
+
+ private com.google.protobuf.LazyStringArrayList availablePermissions_ =
+ com.google.protobuf.LazyStringArrayList.emptyList();
+
+ private void ensureAvailablePermissionsIsMutable() {
+ if (!availablePermissions_.isModifiable()) {
+ availablePermissions_ = new com.google.protobuf.LazyStringArrayList(availablePermissions_);
+ }
+ bitField0_ |= 0x00000002;
+ }
+
+ /**
+ *
+ *
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ *
+ * repeated string available_permissions = 2;
+ *
+ * @return The count of availablePermissions.
+ */
+ public int getAvailablePermissionsCount() {
+ return availablePermissions_.size();
+ }
+
+ /**
+ *
+ *
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ *
+ * repeated string available_permissions = 2;
+ *
+ * @param index The index of the element to return.
+ * @return The availablePermissions at the given index.
+ */
+ public java.lang.String getAvailablePermissions(int index) {
+ return availablePermissions_.get(index);
+ }
+
+ /**
+ *
+ *
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ *
+ * repeated string available_permissions = 2;
+ *
+ * @param index The index of the value to return.
+ * @return The bytes of the availablePermissions at the given index.
+ */
+ public com.google.protobuf.ByteString getAvailablePermissionsBytes(int index) {
+ return availablePermissions_.getByteString(index);
+ }
+
+ /**
+ *
+ *
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ *
+ * repeated string available_permissions = 2;
+ *
+ * @param index The index to set the value at.
+ * @param value The availablePermissions to set.
+ * @return This builder for chaining.
+ */
+ public Builder setAvailablePermissions(int index, java.lang.String value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ ensureAvailablePermissionsIsMutable();
+ availablePermissions_.set(index, value);
+ bitField0_ |= 0x00000002;
+ onChanged();
+ return this;
+ }
+
+ /**
+ *
+ *
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ *
+ * repeated string available_permissions = 2;
+ *
+ * @param value The availablePermissions to add.
+ * @return This builder for chaining.
+ */
+ public Builder addAvailablePermissions(java.lang.String value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ ensureAvailablePermissionsIsMutable();
+ availablePermissions_.add(value);
+ bitField0_ |= 0x00000002;
+ onChanged();
+ return this;
+ }
+
+ /**
+ *
+ *
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ *
+ * repeated string available_permissions = 2;
+ *
+ * @param value The bytes of the availablePermissions to add.
+ * @return This builder for chaining.
+ */
+ public Builder addAvailablePermissionsBytes(com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ checkByteStringIsUtf8(value);
+ ensureAvailablePermissionsIsMutable();
+ availablePermissions_.add(value);
+ bitField0_ |= 0x00000002;
+ onChanged();
+ return this;
+ }
+
+ private dev.cel.expr.Expr compiledAvailabilityCondition_;
+ private com.google.protobuf.SingleFieldBuilder<
+ dev.cel.expr.Expr, dev.cel.expr.Expr.Builder, dev.cel.expr.ExprOrBuilder>
+ compiledAvailabilityConditionBuilder_;
+
+ /**
+ *
+ *
+ *
+ * The compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto) with limited
+ * function support.
+ *
+ * The compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto) with limited
+ * function support.
+ *
+ * The compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto) with limited
+ * function support.
+ *
+ * The compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto) with limited
+ * function support.
+ *
+ * The compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto) with limited
+ * function support.
+ *
+ * The compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto) with limited
+ * function support.
+ *
+ * The compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto) with limited
+ * function support.
+ *
+ * The compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto) with limited
+ * function support.
+ *
+ * The compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto) with limited
+ * function support.
+ *
+ * The full resource name of a Google Cloud resource entity.
+ * The format definition is at
+ * https://cloud.google.com/apis/design/resource_names.
+ *
+ * Example value: `//cloudresourcemanager.googleapis.com/projects/my-project`.
+ *
+ * The full resource name of a Google Cloud resource entity.
+ * The format definition is at
+ * https://cloud.google.com/apis/design/resource_names.
+ *
+ * Example value: `//cloudresourcemanager.googleapis.com/projects/my-project`.
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ *
+ * repeated string available_permissions = 2;
+ *
+ * @param index The index of the element to return.
+ * @return The availablePermissions at the given index.
+ */
+ java.lang.String getAvailablePermissions(int index);
+
+ /**
+ *
+ *
+ *
+ * A list of permissions that may be allowed for use on the specified
+ * resource.
+ *
+ * The only supported values in the list are IAM roles, following the format
+ * of [google.iam.v1.Binding.role][].
+ *
+ * Example value: `inRole:roles/logging.viewer` for predefined roles and
+ * `inRole:organizations/{ORGANIZATION_ID}/roles/logging.viewer` for custom
+ * roles.
+ *
+ *
+ * repeated string available_permissions = 2;
+ *
+ * @param index The index of the value to return.
+ * @return The bytes of the availablePermissions at the given index.
+ */
+ com.google.protobuf.ByteString getAvailablePermissionsBytes(int index);
+
+ /**
+ *
+ *
+ *
+ * The compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto) with limited
+ * function support.
+ *
+ * The compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto) with limited
+ * function support.
+ *
+ * The compiled version of the
+ * availability_condition in the STS API AccessBoundaryRule
+ * (google3/google/identity/sts/v1/access_boundary.proto) with limited
+ * function support.
+ *
+ *
+ * .google.api.expr.Expr compiled_availability_condition = 4;
+ */
+ dev.cel.expr.ExprOrBuilder getCompiledAvailabilityConditionOrBuilder();
+}
diff --git a/google-auth-library-java/cab-token-generator/javatests/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactoryTest.java b/google-auth-library-java/cab-token-generator/javatests/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactoryTest.java
new file mode 100644
index 000000000000..a1714a9ba92f
--- /dev/null
+++ b/google-auth-library-java/cab-token-generator/javatests/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactoryTest.java
@@ -0,0 +1,991 @@
+/*
+ * Copyright 2025, 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.credentialaccessboundary;
+
+import static com.google.auth.oauth2.OAuth2Utils.TOKEN_EXCHANGE_URL_FORMAT;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.util.Clock;
+import com.google.auth.Credentials;
+import com.google.auth.TestUtils;
+import com.google.auth.credentialaccessboundary.ClientSideCredentialAccessBoundaryFactory.IntermediateCredentials;
+import com.google.auth.credentialaccessboundary.ClientSideCredentialAccessBoundaryFactory.RefreshType;
+import com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundary;
+import com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryRule;
+import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.CredentialAccessBoundary;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.auth.oauth2.MockStsTransport;
+import com.google.auth.oauth2.MockTokenServerTransportFactory;
+import com.google.auth.oauth2.OAuth2Utils;
+import com.google.auth.oauth2.ServiceAccountCredentials;
+import com.google.common.collect.ImmutableList;
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.RegistryConfiguration;
+import com.google.crypto.tink.TinkProtoKeysetFormat;
+import dev.cel.common.CelValidationException;
+import dev.cel.expr.Expr;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link
+ * com.google.auth.credentialaccessboundary.ClientSideCredentialAccessBoundaryFactory}.
+ */
+class ClientSideCredentialAccessBoundaryFactoryTest {
+ private static final String SA_PRIVATE_KEY_PKCS8 =
+ "-----BEGIN PRIVATE KEY-----\n"
+ + "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12i"
+ + "kv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj79Xc3yBDfKP2IeyYQIFe0t0"
+ + "zkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kHvoa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw"
+ + "4Az2ZkmeuN6Fk/y9H+Lcb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB/Gr"
+ + "CtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ryJR7GLbCzxPnJm/oQJBANwOCO6"
+ + "D2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrP"
+ + "SXagBxzp8PecbaCHjzNRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAkAut"
+ + "LPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE80L8jVLSbrbrlH43H0DjU5AkEA"
+ + "gidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEiUltk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJ"
+ + "ADj3e1YhMVdjJW5jqwlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAzMDjCQ"
+ + "==\n-----END PRIVATE KEY-----\n";
+
+ private MockStsTransportFactory mockStsTransportFactory;
+ private static MockTokenServerTransportFactory mockTokenServerTransportFactory;
+
+ static class MockStsTransportFactory implements HttpTransportFactory {
+ MockStsTransport transport = new MockStsTransport();
+
+ @Override
+ public HttpTransport create() {
+ return transport;
+ }
+ }
+
+ @BeforeEach
+ void setUp() {
+ mockStsTransportFactory = new MockStsTransportFactory();
+ mockStsTransportFactory.transport.setReturnAccessBoundarySessionKey(true);
+
+ mockTokenServerTransportFactory = new MockTokenServerTransportFactory();
+ mockTokenServerTransportFactory.transport.addServiceAccount(
+ "service-account@google.com", "accessToken");
+ }
+
+ @Test
+ void fetchIntermediateCredentials() throws Exception {
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+
+ ClientSideCredentialAccessBoundaryFactory factory =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setHttpTransportFactory(mockStsTransportFactory)
+ .build();
+
+ IntermediateCredentials intermediateCredentials = factory.fetchIntermediateCredentials();
+
+ // Verify requested token type.
+ Map query =
+ TestUtils.parseQuery(mockStsTransportFactory.transport.getRequest().getContentAsString());
+ assertEquals(
+ OAuth2Utils.TOKEN_TYPE_ACCESS_BOUNDARY_INTERMEDIARY_TOKEN,
+ query.get("requested_token_type"));
+
+ // Verify intermediate token and session key.
+ assertEquals(
+ mockStsTransportFactory.transport.getAccessBoundarySessionKey(),
+ intermediateCredentials.getAccessBoundarySessionKey());
+ assertEquals(
+ mockStsTransportFactory.transport.getAccessToken(),
+ intermediateCredentials.getIntermediateAccessToken().getTokenValue());
+ }
+
+ @Test
+ void fetchIntermediateCredentials_withCustomUniverseDomain() throws IOException {
+ String universeDomain = "foobar";
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory).toBuilder()
+ .setUniverseDomain(universeDomain)
+ .build();
+
+ ClientSideCredentialAccessBoundaryFactory factory =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setUniverseDomain(universeDomain)
+ .setSourceCredential(sourceCredentials)
+ .setHttpTransportFactory(mockStsTransportFactory)
+ .build();
+
+ factory.fetchIntermediateCredentials();
+
+ // Verify domain.
+ String url = mockStsTransportFactory.transport.getRequest().getUrl();
+ assertEquals(url, String.format(TOKEN_EXCHANGE_URL_FORMAT, universeDomain));
+ }
+
+ @Test
+ void fetchIntermediateCredentials_sourceCredentialCannotRefresh_throwsIOException()
+ throws Exception {
+ // Simulate error when refreshing the source credential.
+ mockTokenServerTransportFactory.transport.setError(new IOException());
+
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+
+ ClientSideCredentialAccessBoundaryFactory factory =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setHttpTransportFactory(mockStsTransportFactory)
+ .build();
+
+ IOException thrown = assertThrows(IOException.class, factory::fetchIntermediateCredentials);
+ assertEquals("Unable to refresh the provided source credential.", thrown.getMessage());
+ }
+
+ @Test
+ void fetchIntermediateCredentials_noExpiresInReturned_copiesSourceExpiration() throws Exception {
+ // Simulate STS not returning expires_in.
+ mockStsTransportFactory.transport.setReturnExpiresIn(false);
+
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+
+ ClientSideCredentialAccessBoundaryFactory factory =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setHttpTransportFactory(mockStsTransportFactory)
+ .build();
+
+ IntermediateCredentials intermediateCredentials = factory.fetchIntermediateCredentials();
+ AccessToken intermediateAccessToken = intermediateCredentials.getIntermediateAccessToken();
+
+ assertEquals(
+ mockStsTransportFactory.transport.getAccessToken(),
+ intermediateAccessToken.getTokenValue());
+
+ // Validate that the expires_in has been copied from the source credential.
+ AccessToken sourceAccessToken = sourceCredentials.getAccessToken();
+ assertNotNull(sourceAccessToken);
+ assertEquals(
+ sourceAccessToken.getExpirationTime(), intermediateAccessToken.getExpirationTime());
+ }
+
+ @Test
+ void refreshCredentialsIfRequired_firstCallWillFetchIntermediateCredentials() throws IOException {
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+
+ ClientSideCredentialAccessBoundaryFactory factory =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setHttpTransportFactory(mockStsTransportFactory)
+ .build();
+
+ // Verify that the first call to refreshCredentialsIfRequired() triggers a fetch of intermediate
+ // credentials, resulting in one request to the STS endpoint. This happens because the
+ // intermediate credentials are initially null.
+ assertEquals(0, mockStsTransportFactory.transport.getRequestCount());
+ factory.refreshCredentialsIfRequired();
+ assertEquals(1, mockStsTransportFactory.transport.getRequestCount());
+ }
+
+ @Test
+ void refreshCredentialsIfRequired_noRefreshNeeded() throws IOException {
+ final ClientSideCredentialAccessBoundaryFactory factory =
+ getClientSideCredentialAccessBoundaryFactory(RefreshType.NONE);
+
+ // Call refreshCredentialsIfRequired() once to initialize the intermediate credentials. This
+ // should make one request to the STS endpoint.
+ factory.refreshCredentialsIfRequired();
+
+ // Verify that a subsequent call to refreshCredentialsIfRequired() does NOT trigger another
+ // refresh, as the token is still valid. The request count should remain the same.
+ assertEquals(1, mockStsTransportFactory.transport.getRequestCount());
+ factory.refreshCredentialsIfRequired();
+ assertEquals(1, mockStsTransportFactory.transport.getRequestCount());
+ }
+
+ @Test
+ void refreshCredentialsIfRequired_blockingSingleThread() throws IOException {
+ final ClientSideCredentialAccessBoundaryFactory factory =
+ getClientSideCredentialAccessBoundaryFactory(RefreshType.BLOCKING);
+
+ // Call refreshCredentialsIfRequired() once to initialize the intermediate credentials. This
+ // should make one request to the STS endpoint.
+ factory.refreshCredentialsIfRequired();
+ assertEquals(1, mockStsTransportFactory.transport.getRequestCount());
+
+ // Simulate multiple calls to refreshCredentialsIfRequired. In blocking mode, each call should
+ // trigger a new request to the STS endpoint.
+ int numRefresh = 3;
+ for (int i = 0; i < numRefresh; i++) {
+ factory.refreshCredentialsIfRequired();
+ }
+
+ // Verify that the total number of requests to the STS endpoint is the initial request plus the
+ // number of subsequent refresh calls.
+ assertEquals(1 + numRefresh, mockStsTransportFactory.transport.getRequestCount());
+ }
+
+ @Test
+ void refreshCredentialsIfRequired_asyncSingleThread() throws IOException {
+ final ClientSideCredentialAccessBoundaryFactory factory =
+ getClientSideCredentialAccessBoundaryFactory(RefreshType.ASYNC);
+
+ // Call refreshCredentialsIfRequired() once to initialize the intermediate credentials. This
+ // should make one request to the STS endpoint.
+ factory.refreshCredentialsIfRequired();
+ assertEquals(1, mockStsTransportFactory.transport.getRequestCount());
+
+ // Subsequent calls to refreshCredentialsIfRequired() in an async mode should NOT
+ // immediately call the STS endpoint. They should schedule an asynchronous refresh.
+ int numRefresh = 3;
+ for (int i = 0; i < numRefresh; i++) {
+ factory.refreshCredentialsIfRequired();
+ }
+
+ // Verify that only the initial call resulted in an immediate STS request. The async refresh
+ // is still pending.
+ assertEquals(1, mockStsTransportFactory.transport.getRequestCount());
+
+ // Introduce a small delay to allow the asynchronous refresh task to complete. This is
+ // necessary because the async task runs on a separate thread.
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ throw new IOException(e);
+ }
+
+ // After the delay, the request count should be 2 (initial fetch + one async refresh).
+ // Subsequent calls to refreshCredentialsIfRequired() in ASYNC mode re-use the in-progress
+ // refresh task, so they don't trigger additional STS requests.
+ assertEquals(2, mockStsTransportFactory.transport.getRequestCount());
+ }
+
+ @Test
+ void refreshCredentialsIfRequired_blockingMultiThread() throws IOException, InterruptedException {
+ final ClientSideCredentialAccessBoundaryFactory factory =
+ getClientSideCredentialAccessBoundaryFactory(RefreshType.BLOCKING);
+
+ // Call refreshCredentialsIfRequired() once to initialize the intermediate credentials. This
+ // should make one request to the STS endpoint.
+ factory.refreshCredentialsIfRequired();
+ assertEquals(1, mockStsTransportFactory.transport.getRequestCount());
+
+ // Simulate multiple threads concurrently calling refreshCredentialsIfRequired(). In blocking
+ // mode, only one of these calls should trigger a new request to the STS endpoint. The others
+ // should block until the first refresh completes and then use the newly acquired credentials.
+ triggerConcurrentRefresh(factory, 3);
+
+ // After all threads complete, the request count should be 2 (the initial fetch plus one
+ // blocking refresh).
+ assertEquals(2, mockStsTransportFactory.transport.getRequestCount());
+ }
+
+ @Test
+ void refreshCredentialsIfRequired_asyncMultiThread() throws IOException, InterruptedException {
+ final ClientSideCredentialAccessBoundaryFactory factory =
+ getClientSideCredentialAccessBoundaryFactory(RefreshType.ASYNC);
+
+ // Call refreshCredentialsIfRequired() once to initialize the intermediate credentials. This
+ // should make one request to the STS endpoint.
+ factory.refreshCredentialsIfRequired();
+ assertEquals(1, mockStsTransportFactory.transport.getRequestCount());
+
+ // Simulate multiple threads concurrently calling refreshCredentialsIfRequired(). In async
+ // mode, the first call should trigger a background refresh. Subsequent calls should NOT
+ // trigger additional refreshes while the background refresh is still pending.
+ triggerConcurrentRefresh(factory, 5);
+
+ // Introduce a small delay to allow the asynchronous refresh task to complete.
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ throw new IOException(e);
+ }
+
+ // After the delay, the request count should be 2, indicating that the initial fetch and a
+ // single async refresh occurred (not one per thread).
+ assertEquals(2, mockStsTransportFactory.transport.getRequestCount());
+ }
+
+ @Test
+ void refreshCredentialsIfRequired_sourceCredentialCannotRefresh_throwsIOException()
+ throws Exception {
+ // Simulate error when refreshing the source credential.
+ mockTokenServerTransportFactory.transport.setError(new IOException());
+
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+
+ ClientSideCredentialAccessBoundaryFactory factory =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setHttpTransportFactory(mockStsTransportFactory)
+ .build();
+
+ IOException exception = assertThrows(IOException.class, factory::refreshCredentialsIfRequired);
+ assertEquals("Unable to refresh the provided source credential.", exception.getMessage());
+ }
+
+ // Tests related to the builder methods.
+ @Test
+ void builder_noSourceCredential_throws() {
+ NullPointerException exception =
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
+ .build());
+ assertEquals("Source credential must not be null.", exception.getMessage());
+ }
+
+ @Test
+ void builder_minimumTokenLifetime_negative_throws() throws IOException {
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setMinimumTokenLifetime(Duration.ofMinutes(-1)));
+
+ assertEquals("Minimum token lifetime must be greater than zero.", exception.getMessage());
+ }
+
+ @Test
+ void builder_minimumTokenLifetime_zero_throws() throws IOException {
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setMinimumTokenLifetime(Duration.ZERO));
+
+ assertEquals("Minimum token lifetime must be greater than zero.", exception.getMessage());
+ }
+
+ @Test
+ void builder_refreshMargin_negative_throws() throws IOException {
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setRefreshMargin(Duration.ofMinutes(-1)));
+
+ assertEquals("Refresh margin must be greater than zero.", exception.getMessage());
+ }
+
+ @Test
+ void builder_refreshMargin_zero_throws() throws IOException {
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setRefreshMargin(Duration.ZERO));
+
+ assertEquals("Refresh margin must be greater than zero.", exception.getMessage());
+ }
+
+ @Test
+ void builder_setsCorrectDefaultValues() throws IOException {
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+ ClientSideCredentialAccessBoundaryFactory factory =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .build();
+
+ assertEquals(OAuth2Utils.HTTP_TRANSPORT_FACTORY, factory.getTransportFactory());
+ assertEquals(
+ String.format(OAuth2Utils.TOKEN_EXCHANGE_URL_FORMAT, Credentials.GOOGLE_DEFAULT_UNIVERSE),
+ factory.getTokenExchangeEndpoint());
+ }
+
+ @Test
+ void builder_universeDomainMismatch_throws() throws IOException {
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setUniverseDomain("differentUniverseDomain")
+ .build());
+ assertEquals(
+ "The client side access boundary credential's universe domain must be the same as the source credential.",
+ exception.getMessage());
+ }
+
+ @Test
+ void builder_invalidRefreshMarginAndMinimumTokenLifetime_throws() throws IOException {
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setRefreshMargin(Duration.ofMinutes(50))
+ .setMinimumTokenLifetime(Duration.ofMinutes(50))
+ .build());
+
+ assertEquals(
+ "Refresh margin must be at least one minute longer than the minimum token lifetime.",
+ exception.getMessage());
+ }
+
+ @Test
+ void builder_invalidRefreshMargin_throws() throws IOException {
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setRefreshMargin(Duration.ofMinutes(25))
+ .build());
+
+ assertEquals(
+ "Refresh margin must be at least one minute longer than the minimum token lifetime.",
+ exception.getMessage());
+ }
+
+ @Test
+ void builder_invalidMinimumTokenLifetime_throws() throws IOException {
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+
+ IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setMinimumTokenLifetime(Duration.ofMinutes(50))
+ .build());
+
+ assertEquals(
+ "Refresh margin must be at least one minute longer than the minimum token lifetime.",
+ exception.getMessage());
+ }
+
+ @Test
+ void builder_minimumTokenLifetimeNotSet_usesDefault() throws IOException {
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+
+ ClientSideCredentialAccessBoundaryFactory factory =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setRefreshMargin(Duration.ofMinutes(50))
+ .build();
+
+ assertEquals(
+ ClientSideCredentialAccessBoundaryFactory.DEFAULT_MINIMUM_TOKEN_LIFETIME,
+ factory.getMinimumTokenLifetime());
+ }
+
+ @Test
+ void builder_refreshMarginNotSet_usesDefault() throws IOException {
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+
+ ClientSideCredentialAccessBoundaryFactory factory =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setMinimumTokenLifetime(Duration.ofMinutes(20))
+ .build();
+
+ assertEquals(
+ ClientSideCredentialAccessBoundaryFactory.DEFAULT_REFRESH_MARGIN,
+ factory.getRefreshMargin());
+ }
+
+ private static GoogleCredentials getServiceAccountSourceCredentials(
+ MockTokenServerTransportFactory transportFactory) throws IOException {
+ String email = "service-account@google.com";
+
+ ServiceAccountCredentials sourceCredentials =
+ ServiceAccountCredentials.newBuilder()
+ .setClientEmail(email)
+ .setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
+ .setPrivateKeyId("privateKeyId")
+ .setProjectId("projectId")
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ return sourceCredentials.createScoped("https://www.googleapis.com/auth/cloud-platform");
+ }
+
+ private ClientSideCredentialAccessBoundaryFactory getClientSideCredentialAccessBoundaryFactory(
+ RefreshType refreshType) throws IOException {
+ GoogleCredentials sourceCredentials =
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory);
+
+ return ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .setHttpTransportFactory(mockStsTransportFactory)
+ .setClock(createMockClock(refreshType, sourceCredentials))
+ .build();
+ }
+
+ private Clock createMockClock(RefreshType refreshType, GoogleCredentials sourceCredentials) {
+ Clock mockClock = mock(Clock.class);
+ long currentTimeInMillis = Clock.SYSTEM.currentTimeMillis();
+ long mockedTimeInMillis;
+ final long refreshMarginInMillis =
+ ClientSideCredentialAccessBoundaryFactory.DEFAULT_REFRESH_MARGIN.toMillis();
+ final long minimumTokenLifetimeMillis =
+ ClientSideCredentialAccessBoundaryFactory.DEFAULT_MINIMUM_TOKEN_LIFETIME.toMillis();
+
+ // If the source credential doesn't have an access token, set the expiration time to 1 hour from
+ // the current time.
+ long expirationTimeInMillis =
+ sourceCredentials.getAccessToken() != null
+ ? sourceCredentials.getAccessToken().getExpirationTime().getTime()
+ : currentTimeInMillis + 3600000;
+
+ switch (refreshType) {
+ case NONE:
+ // Set mocked time so that the token is fresh and no refresh is needed (before the refresh
+ // margin).
+ mockedTimeInMillis = expirationTimeInMillis - refreshMarginInMillis - 60000;
+ break;
+ case ASYNC:
+ // Set mocked time so that the token is nearing expiry and an async refresh is triggered
+ // (within the refresh margin).
+ mockedTimeInMillis = expirationTimeInMillis - refreshMarginInMillis + 60000;
+ break;
+ case BLOCKING:
+ // Set mocked time so that the token requires immediate refresh (just after the minimum
+ // token lifetime).
+ mockedTimeInMillis = expirationTimeInMillis - minimumTokenLifetimeMillis + 60000;
+ break;
+ default:
+ throw new IllegalArgumentException("Unexpected RefreshType: " + refreshType);
+ }
+
+ when(mockClock.currentTimeMillis()).thenReturn(mockedTimeInMillis);
+ return mockClock;
+ }
+
+ private static void triggerConcurrentRefresh(
+ ClientSideCredentialAccessBoundaryFactory factory, int numThreads)
+ throws InterruptedException {
+ Thread[] threads = new Thread[numThreads];
+ CountDownLatch latch = new CountDownLatch(numThreads);
+ long timeoutMillis = 5000; // 5 seconds
+
+ // Create and start threads to concurrently call refreshCredentialsIfRequired().
+ for (int i = 0; i < numThreads; i++) {
+ threads[i] =
+ new Thread(
+ () -> {
+ try {
+ latch.countDown();
+ latch.await();
+ factory.refreshCredentialsIfRequired();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ threads[i].start();
+ }
+
+ // Wait for each thread to complete, with a timeout.
+ for (Thread thread : threads) {
+ thread.join(timeoutMillis);
+ if (thread.isAlive()) {
+ thread.interrupt();
+ throw new AssertionError(
+ "Thread running refreshCredentialsIfRequired timed out after "
+ + timeoutMillis
+ + " milliseconds.");
+ }
+ }
+ }
+
+ private static class CabToken {
+ String intermediateToken;
+ String encryptedRestriction;
+
+ CabToken(String intermediateToken, String encryptedRestriction) {
+ this.intermediateToken = intermediateToken;
+ this.encryptedRestriction = encryptedRestriction;
+ }
+ }
+
+ private static CabToken parseCabToken(AccessToken token) {
+ String[] parts = token.getTokenValue().split("\\.");
+ assertEquals(2, parts.length);
+
+ return new CabToken(parts[0], parts[1]);
+ }
+
+ private static ClientSideAccessBoundary decryptRestriction(String restriction, String sessionKey)
+ throws Exception {
+ byte[] rawKey = Base64.getDecoder().decode(sessionKey);
+
+ KeysetHandle keysetHandle =
+ TinkProtoKeysetFormat.parseKeyset(rawKey, InsecureSecretKeyAccess.get());
+
+ Aead aead = keysetHandle.getPrimitive(RegistryConfiguration.get(), Aead.class);
+ byte[] rawRestrictions =
+ aead.decrypt(Base64.getUrlDecoder().decode(restriction), /* associatedData= */ new byte[0]);
+
+ return ClientSideAccessBoundary.parseFrom(rawRestrictions);
+ }
+
+ @Test
+ void generateToken_withAvailablityCondition_success() throws Exception {
+ MockStsTransportFactory transportFactory = new MockStsTransportFactory();
+ transportFactory.transport.setReturnAccessBoundarySessionKey(true);
+
+ ClientSideCredentialAccessBoundaryFactory.Builder builder =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder();
+
+ ClientSideCredentialAccessBoundaryFactory factory =
+ builder
+ .setSourceCredential(
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory))
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ CredentialAccessBoundary.Builder cabBuilder = CredentialAccessBoundary.newBuilder();
+ CredentialAccessBoundary accessBoundary =
+ cabBuilder
+ .addRule(
+ CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
+ .setAvailableResource("resource")
+ .setAvailablePermissions(ImmutableList.of("role1", "role2"))
+ .setAvailabilityCondition(
+ CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition
+ .newBuilder()
+ .setExpression("a == b")
+ .build())
+ .build())
+ .build();
+
+ AccessToken token = factory.generateToken(accessBoundary);
+
+ CabToken cabToken = parseCabToken(token);
+ assertEquals("accessToken", cabToken.intermediateToken);
+
+ // Base64 encoding output by default has `=` padding at the end if the input length
+ // is not a multiple of 3. Here we verify the use of `withoutPadding` that removes
+ // this padding.
+ assertFalse(cabToken.encryptedRestriction.contains(String.valueOf("=")));
+
+ // Checks the encrypted restriction is the correct proto format of the CredentialAccessBoundary.
+ ClientSideAccessBoundary clientSideAccessBoundary =
+ decryptRestriction(
+ cabToken.encryptedRestriction,
+ transportFactory.transport.getAccessBoundarySessionKey());
+ assertEquals(1, clientSideAccessBoundary.getAccessBoundaryRulesCount());
+
+ ClientSideAccessBoundaryRule rule = clientSideAccessBoundary.getAccessBoundaryRules(0);
+
+ // Available resource and available permission should be the exact same as in original format.
+ assertEquals("resource", rule.getAvailableResource());
+ assertEquals(ImmutableList.of("role1", "role2"), rule.getAvailablePermissionsList());
+
+ // Availability condition should be in the correct compiled proto format.
+ Expr expr = rule.getCompiledAvailabilityCondition();
+ assertEquals("_==_", expr.getCallExpr().getFunction());
+ assertEquals("a", expr.getCallExpr().getArgs(0).getIdentExpr().getName());
+ assertEquals("b", expr.getCallExpr().getArgs(1).getIdentExpr().getName());
+ }
+
+ @Test
+ void generateToken_withoutAvailabilityCondition_success() throws Exception {
+ MockStsTransportFactory transportFactory = new MockStsTransportFactory();
+ transportFactory.transport.setReturnAccessBoundarySessionKey(true);
+
+ ClientSideCredentialAccessBoundaryFactory.Builder builder =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder();
+
+ ClientSideCredentialAccessBoundaryFactory factory =
+ builder
+ .setSourceCredential(
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory))
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ CredentialAccessBoundary.Builder cabBuilder = CredentialAccessBoundary.newBuilder();
+ CredentialAccessBoundary accessBoundary =
+ cabBuilder
+ .addRule(
+ CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
+ .setAvailableResource("resource")
+ .setAvailablePermissions(ImmutableList.of("role"))
+ .build())
+ .build();
+
+ AccessToken token = factory.generateToken(accessBoundary);
+
+ CabToken cabToken = parseCabToken(token);
+ assertEquals("accessToken", cabToken.intermediateToken);
+
+ // Base64 encoding output by default has `=` padding at the end if the input length
+ // is not a multiple of 3. Here we verify the use of `withoutPadding` that removes
+ // this padding.
+ assertFalse(cabToken.encryptedRestriction.contains(String.valueOf("=")));
+
+ // Checks the encrypted restriction is the correct proto format of the CredentialAccessBoundary.
+ ClientSideAccessBoundary clientSideAccessBoundary =
+ decryptRestriction(
+ cabToken.encryptedRestriction,
+ transportFactory.transport.getAccessBoundarySessionKey());
+ assertEquals(1, clientSideAccessBoundary.getAccessBoundaryRulesCount());
+
+ ClientSideAccessBoundaryRule rule = clientSideAccessBoundary.getAccessBoundaryRules(0);
+
+ // Available resource and available permission should be the exact same as in original format.
+ assertEquals("resource", rule.getAvailableResource());
+ assertEquals(ImmutableList.of("role"), rule.getAvailablePermissionsList());
+
+ // Availability condition should be empty since it's not provided.
+ assertFalse(rule.hasCompiledAvailabilityCondition());
+ }
+
+ @Test
+ void generateToken_withMultipleRules_success() throws Exception {
+ MockStsTransportFactory transportFactory = new MockStsTransportFactory();
+ transportFactory.transport.setReturnAccessBoundarySessionKey(true);
+
+ ClientSideCredentialAccessBoundaryFactory.Builder builder =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder();
+
+ ClientSideCredentialAccessBoundaryFactory factory =
+ builder
+ .setSourceCredential(
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory))
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ CredentialAccessBoundary.Builder cabBuilder = CredentialAccessBoundary.newBuilder();
+ CredentialAccessBoundary accessBoundary =
+ cabBuilder
+ .addRule(
+ CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
+ .setAvailableResource("resource1")
+ .setAvailablePermissions(ImmutableList.of("role1-1", "role1-2"))
+ .setAvailabilityCondition(
+ CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition
+ .newBuilder()
+ .setExpression("a == b")
+ .build())
+ .build())
+ .addRule(
+ CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
+ .setAvailableResource("resource")
+ .setAvailablePermissions(ImmutableList.of("role2"))
+ .build())
+ .build();
+
+ AccessToken token = factory.generateToken(accessBoundary);
+
+ CabToken cabToken = parseCabToken(token);
+ assertEquals("accessToken", cabToken.intermediateToken);
+
+ // Checks the encrypted restriction is the correct proto format of the CredentialAccessBoundary.
+ ClientSideAccessBoundary clientSideAccessBoundary =
+ decryptRestriction(
+ cabToken.encryptedRestriction,
+ transportFactory.transport.getAccessBoundarySessionKey());
+ assertEquals(2, clientSideAccessBoundary.getAccessBoundaryRulesCount());
+
+ // Checks the first rule.
+ ClientSideAccessBoundaryRule rule1 = clientSideAccessBoundary.getAccessBoundaryRules(0);
+ assertEquals("resource1", rule1.getAvailableResource());
+ assertEquals(ImmutableList.of("role1-1", "role1-2"), rule1.getAvailablePermissionsList());
+
+ Expr expr = rule1.getCompiledAvailabilityCondition();
+ assertEquals("_==_", expr.getCallExpr().getFunction());
+ assertEquals("a", expr.getCallExpr().getArgs(0).getIdentExpr().getName());
+ assertEquals("b", expr.getCallExpr().getArgs(1).getIdentExpr().getName());
+
+ // Checks the second rule.
+ ClientSideAccessBoundaryRule rule2 = clientSideAccessBoundary.getAccessBoundaryRules(1);
+ assertEquals("resource", rule2.getAvailableResource());
+ assertEquals(ImmutableList.of("role2"), rule2.getAvailablePermissionsList());
+ assertFalse(rule2.hasCompiledAvailabilityCondition());
+ }
+
+ @Test
+ void generateToken_withInvalidAvailabilityCondition_failure() throws Exception {
+ MockStsTransportFactory transportFactory = new MockStsTransportFactory();
+ transportFactory.transport.setReturnAccessBoundarySessionKey(true);
+
+ ClientSideCredentialAccessBoundaryFactory.Builder builder =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder();
+
+ ClientSideCredentialAccessBoundaryFactory factory =
+ builder
+ .setSourceCredential(
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory))
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ CredentialAccessBoundary.Builder cabBuilder = CredentialAccessBoundary.newBuilder();
+ CredentialAccessBoundary accessBoundary =
+ cabBuilder
+ .addRule(
+ CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
+ .setAvailableResource(
+ "//storage.googleapis.com/projects/" + "_/buckets/example-bucket")
+ .setAvailablePermissions(ImmutableList.of("inRole:roles/storage.objectViewer"))
+ .setAvailabilityCondition(
+ CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition
+ .newBuilder()
+ .setExpression(
+ "resource.name.startsWith('projects/_/"
+ + "buckets/example-bucket/objects/"
+ + "customer-a'") // No closing bracket
+ .build())
+ .build())
+ .build();
+
+ assertThrows(CelValidationException.class, () -> factory.generateToken(accessBoundary));
+ }
+
+ @Test
+ void generateToken_withSessionKeyNotBase64Encoded_failure() throws Exception {
+ MockStsTransportFactory transportFactory = new MockStsTransportFactory();
+ transportFactory.transport.setReturnAccessBoundarySessionKey(true);
+ transportFactory.transport.setAccessBoundarySessionKey("invalid_key");
+
+ ClientSideCredentialAccessBoundaryFactory.Builder builder =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder();
+
+ ClientSideCredentialAccessBoundaryFactory factory =
+ builder
+ .setSourceCredential(
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory))
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ CredentialAccessBoundary.Builder cabBuilder = CredentialAccessBoundary.newBuilder();
+ CredentialAccessBoundary accessBoundary =
+ cabBuilder
+ .addRule(
+ CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
+ .setAvailableResource(
+ "//storage.googleapis.com/projects/" + "_/buckets/example-bucket")
+ .setAvailablePermissions(ImmutableList.of("inRole:roles/storage.objectViewer"))
+ .setAvailabilityCondition(
+ CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition
+ .newBuilder()
+ .setExpression(
+ "resource.name.startsWith('projects/_/"
+ + "buckets/example-bucket/objects/customer-a')")
+ .build())
+ .build())
+ .build();
+
+ assertThrows(IllegalStateException.class, () -> factory.generateToken(accessBoundary));
+ }
+
+ @Test
+ void generateToken_withMalformSessionKey_failure() throws Exception {
+ MockStsTransportFactory transportFactory = new MockStsTransportFactory();
+ transportFactory.transport.setReturnAccessBoundarySessionKey(true);
+ transportFactory.transport.setAccessBoundarySessionKey("aW52YWxpZF9rZXk=");
+
+ ClientSideCredentialAccessBoundaryFactory.Builder builder =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder();
+
+ ClientSideCredentialAccessBoundaryFactory factory =
+ builder
+ .setSourceCredential(
+ getServiceAccountSourceCredentials(mockTokenServerTransportFactory))
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ CredentialAccessBoundary.Builder cabBuilder = CredentialAccessBoundary.newBuilder();
+ CredentialAccessBoundary accessBoundary =
+ cabBuilder
+ .addRule(
+ CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
+ .setAvailableResource(
+ "//storage.googleapis.com/projects/" + "_/buckets/example-bucket")
+ .setAvailablePermissions(ImmutableList.of("inRole:roles/storage.objectViewer"))
+ .setAvailabilityCondition(
+ CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition
+ .newBuilder()
+ .setExpression(
+ "resource.name.startsWith('projects/_/"
+ + "buckets/example-bucket/objects/customer-a')")
+ .build())
+ .build())
+ .build();
+
+ assertThrows(GeneralSecurityException.class, () -> factory.generateToken(accessBoundary));
+ }
+}
diff --git a/google-auth-library-java/cab-token-generator/javatests/com/google/auth/credentialaccessboundary/ITClientSideCredentialAccessBoundaryTest.java b/google-auth-library-java/cab-token-generator/javatests/com/google/auth/credentialaccessboundary/ITClientSideCredentialAccessBoundaryTest.java
new file mode 100644
index 000000000000..cab0183f17f9
--- /dev/null
+++ b/google-auth-library-java/cab-token-generator/javatests/com/google/auth/credentialaccessboundary/ITClientSideCredentialAccessBoundaryTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2025, 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.credentialaccessboundary;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpResponseException;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.JsonObjectParser;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.auth.Credentials;
+import com.google.auth.http.HttpCredentialsAdapter;
+import com.google.auth.oauth2.CredentialAccessBoundary;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.auth.oauth2.OAuth2CredentialsWithRefresh;
+import com.google.auth.oauth2.ServiceAccountCredentials;
+import dev.cel.common.CelValidationException;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Integration tests for {@link ClientSideCredentialAccessBoundaryFactory}. *
+ *
+ *
The only requirements for this test suite to run is to set the environment variable
+ * GOOGLE_APPLICATION_CREDENTIALS to point to the same service account configured in the setup
+ * script (downscoping-with-cab-setup.sh).
+ */
+final class ITClientSideCredentialAccessBoundaryTest {
+
+ // Output copied from the setup script (downscoping-with-cab-setup.sh).
+ private static final String GCS_BUCKET_NAME = "cab-int-bucket-cbi3qrv5";
+ private static final String GCS_OBJECT_NAME_WITH_PERMISSION = "cab-first-cbi3qrv5.txt";
+ private static final String GCS_OBJECT_NAME_WITHOUT_PERMISSION = "cab-second-cbi3qrv5.txt";
+
+ // This Credential Access Boundary enables the objectViewer permission to the specified object in
+ // the specified bucket.
+ private static final CredentialAccessBoundary CREDENTIAL_ACCESS_BOUNDARY =
+ CredentialAccessBoundary.newBuilder()
+ .addRule(
+ CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
+ .setAvailableResource(
+ String.format(
+ "//storage.googleapis.com/projects/_/buckets/%s", GCS_BUCKET_NAME))
+ .addAvailablePermission("inRole:roles/storage.objectViewer")
+ .setAvailabilityCondition(
+ CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition.newBuilder()
+ .setExpression(
+ String.format(
+ "resource.name.startsWith('projects/_/buckets/%s/objects/%s')",
+ GCS_BUCKET_NAME, GCS_OBJECT_NAME_WITH_PERMISSION))
+ .build())
+ .build())
+ .build();
+
+ /**
+ * A downscoped credential is obtained using ClientSideCredentialAccessBoundaryFactory with
+ * permissions to access an object in the GCS bucket configured. We should only have access to
+ * retrieve this object.
+ *
+ *
We confirm this by: 1. Validating that we can successfully retrieve this object with the
+ * downscoped token. 2. Validating that we do not have permission to retrieve a different object
+ * in the same bucket.
+ */
+ @Test
+ void clientSideCredentialAccessBoundary_serviceAccountSource() throws IOException {
+ OAuth2CredentialsWithRefresh.OAuth2RefreshHandler refreshHandler =
+ () -> {
+ ServiceAccountCredentials sourceCredentials =
+ (ServiceAccountCredentials)
+ GoogleCredentials.getApplicationDefault()
+ .createScoped("https://www.googleapis.com/auth/cloud-platform");
+
+ ClientSideCredentialAccessBoundaryFactory factory =
+ ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ .setSourceCredential(sourceCredentials)
+ .build();
+
+ try {
+ return factory.generateToken(CREDENTIAL_ACCESS_BOUNDARY);
+ } catch (CelValidationException | GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+ };
+
+ OAuth2CredentialsWithRefresh credentials =
+ OAuth2CredentialsWithRefresh.newBuilder().setRefreshHandler(refreshHandler).build();
+
+ // Attempt to retrieve the object that the downscoped token has access to.
+ retrieveObjectFromGcs(credentials, GCS_OBJECT_NAME_WITH_PERMISSION);
+
+ // Attempt to retrieve the object that the downscoped token does not have access to. This should
+ // fail.
+ HttpResponseException exception =
+ assertThrows(
+ HttpResponseException.class,
+ () -> retrieveObjectFromGcs(credentials, GCS_OBJECT_NAME_WITHOUT_PERMISSION));
+ assertEquals(403, exception.getStatusCode());
+ }
+
+ private void retrieveObjectFromGcs(Credentials credentials, String objectName)
+ throws IOException {
+ String url =
+ String.format(
+ "https://storage.googleapis.com/storage/v1/b/%s/o/%s", GCS_BUCKET_NAME, objectName);
+
+ HttpCredentialsAdapter credentialsAdapter = new HttpCredentialsAdapter(credentials);
+ HttpRequestFactory requestFactory =
+ new NetHttpTransport().createRequestFactory(credentialsAdapter);
+ HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url));
+
+ JsonObjectParser parser = new JsonObjectParser(GsonFactory.getDefaultInstance());
+ request.setParser(parser);
+
+ HttpResponse response = request.execute();
+ assertTrue(response.isSuccessStatusCode());
+ }
+}
diff --git a/google-auth-library-java/cab-token-generator/pom.xml b/google-auth-library-java/cab-token-generator/pom.xml
new file mode 100644
index 000000000000..2970c2494094
--- /dev/null
+++ b/google-auth-library-java/cab-token-generator/pom.xml
@@ -0,0 +1,93 @@
+
+
+ 4.0.0
+
+ com.google.auth
+ google-auth-library-parent
+ 1.43.1-SNAPSHOT
+
+
+ google-auth-library-cab-token-generator
+ Google Auth Library for Java - Cab Token Generator
+
+
+ java
+ javatests
+
+
+
+
+ com.google.auth
+ google-auth-library-oauth2-http
+
+
+ com.google.auth
+ google-auth-library-credentials
+
+
+ com.google.http-client
+ google-http-client
+
+
+ com.google.errorprone
+ error_prone_annotations
+
+
+ com.google.guava
+ guava
+
+
+ com.google.protobuf
+ protobuf-java
+
+
+ dev.cel
+ cel
+
+
+ com.google.code.findbugs
+ jsr305
+
+
+ com.google.crypto.tink
+ tink
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ com.google.auth
+ google-auth-library-oauth2-http
+ test
+ test-jar
+ testlib
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ com.google.http-client
+ google-http-client-gson
+ test
+
+
+
+
\ No newline at end of file
diff --git a/google-auth-library-java/credentials/java/com/google/auth/ApiKeyCredentials.java b/google-auth-library-java/credentials/java/com/google/auth/ApiKeyCredentials.java
new file mode 100644
index 000000000000..8c454d0a9f66
--- /dev/null
+++ b/google-auth-library-java/credentials/java/com/google/auth/ApiKeyCredentials.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2024, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Credentials class for calling Google APIs using an API key.
+ *
+ *
Uses an API key directly in the request metadata to provide authorization.
+ *
+ *
Note: ApiKeyCredentials extends from base {@link Credentials} class rather than
+ * GoogleCredentials/OAuth2Credentials, as it does not provide an access token and is not considered
+ * an OAuth2 credential.
+ *
+ *
+ * Credentials credentials = ApiKeyCredentials.create("your api key");
+ *
+ */
+public class ApiKeyCredentials extends Credentials {
+ static final String API_KEY_HEADER_KEY = "x-goog-api-key";
+ private final String apiKey;
+
+ ApiKeyCredentials(String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public static ApiKeyCredentials create(String apiKey) {
+ if (apiKey == null || apiKey.isEmpty()) {
+ throw new IllegalArgumentException("API key cannot be null or blank");
+ }
+ return new ApiKeyCredentials(apiKey);
+ }
+
+ @Override
+ public String getAuthenticationType() {
+ return "API-Key";
+ }
+
+ @Override
+ public Map> getRequestMetadata(URI uri) throws IOException {
+ return Collections.singletonMap(API_KEY_HEADER_KEY, Collections.singletonList(apiKey));
+ }
+
+ @Override
+ public boolean hasRequestMetadata() {
+ return true;
+ }
+
+ @Override
+ public boolean hasRequestMetadataOnly() {
+ return true;
+ }
+
+ /** There is no concept of refreshing an API tokens, this method is a no-op. */
+ @Override
+ public void refresh() throws IOException {}
+}
diff --git a/google-auth-library-java/credentials/java/com/google/auth/CredentialTypeForMetrics.java b/google-auth-library-java/credentials/java/com/google/auth/CredentialTypeForMetrics.java
new file mode 100644
index 000000000000..50c90365c322
--- /dev/null
+++ b/google-auth-library-java/credentials/java/com/google/auth/CredentialTypeForMetrics.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 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;
+
+/**
+ * Defines the different types of credentials that can be used for metrics.
+ *
+ *
Each credential type is associated with a label that is used for reporting purposes. Add new
+ * enum constant only when corresponding configs established.
+ *
+ *
Credentials with type {@code CredentialTypeForMetrics.DO_NOT_SEND} is default value for
+ * credential implementations that do not set type specifically. It is not expected to send metrics.
+ *
+ *
+ *
+ * @see #getLabel()
+ */
+public enum CredentialTypeForMetrics {
+ USER_CREDENTIALS("u"),
+ SERVICE_ACCOUNT_CREDENTIALS_AT("sa"),
+ SERVICE_ACCOUNT_CREDENTIALS_JWT("jwt"),
+ VM_CREDENTIALS("mds"),
+ IMPERSONATED_CREDENTIALS("imp"),
+ DO_NOT_SEND("dns");
+
+ private final String label;
+
+ private CredentialTypeForMetrics(String label) {
+ this.label = label;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+}
diff --git a/google-auth-library-java/credentials/java/com/google/auth/Credentials.java b/google-auth-library-java/credentials/java/com/google/auth/Credentials.java
new file mode 100644
index 000000000000..b1579db612a9
--- /dev/null
+++ b/google-auth-library-java/credentials/java/com/google/auth/Credentials.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2015, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/** Represents an abstract authorized identity instance. */
+public abstract class Credentials implements Serializable {
+
+ private static final long serialVersionUID = 808575179767517313L;
+
+ public static final String GOOGLE_DEFAULT_UNIVERSE = "googleapis.com";
+
+ /**
+ * A constant string name describing the authentication technology.
+ *
+ *
E.g. “OAuth2”, “SSL”. For use by the transport layer to determine whether it supports the
+ * type of authentication in the case where {@link Credentials#hasRequestMetadataOnly} is false.
+ * Also serves as a debugging helper.
+ *
+ * @return The type of authentication used.
+ */
+ public abstract String getAuthenticationType();
+
+ /**
+ * Gets the universe domain for the credential in a blocking manner, refreshing tokens if
+ * required.
+ *
+ * @return a universe domain value in the format some-domain.xyz. By default, returns the Google
+ * universe domain googleapis.com.
+ * @throws IOException extending classes might have to do remote calls to determine the universe
+ * domain. The exception must implement {@link Retryable} and {@code isRetryable()} will
+ * return true if the operation may be retried.
+ */
+ public String getUniverseDomain() throws IOException {
+ return GOOGLE_DEFAULT_UNIVERSE;
+ }
+
+ /**
+ * Gets the credential type used for internal metrics header.
+ *
+ *
The default is {@code CredentialTypeForMetrics.DO_NOT_SEND}. For a credential that is
+ * established to track for metrics, this default should be overridden.
+ *
+ * @return a enum value for credential type
+ */
+ public CredentialTypeForMetrics getMetricsCredentialType() {
+ return CredentialTypeForMetrics.DO_NOT_SEND;
+ }
+
+ /**
+ * Get the current request metadata, refreshing tokens if required.
+ *
+ *
This should be called by the transport layer on each request, and the data should be
+ * populated in headers or other context. The operation can block and fail to complete and may do
+ * things such as refreshing access tokens.
+ *
+ *
The convention for handling binary data is for the key in the returned map to end with
+ * {@code "-bin"} and for the corresponding values to be base64 encoded.
+ *
+ * @return The request metadata used for populating headers or other context.
+ * @throws IOException if there was an error getting up-to-date access.
+ */
+ public Map> getRequestMetadata() throws IOException {
+ return getRequestMetadata(null);
+ }
+
+ /**
+ * Get the current request metadata without blocking.
+ *
+ *
This should be called by the transport layer on each request, and the data should be
+ * populated in headers or other context. The implementation can either call the callback inline
+ * or asynchronously. Either way it should never block in this method. The
+ * executor is provided for tasks that may block.
+ *
+ *
The default implementation will just call {@link #getRequestMetadata(URI)} then the callback
+ * from the given executor.
+ *
+ *
The convention for handling binary data is for the key in the returned map to end with
+ * {@code "-bin"} and for the corresponding values to be base64 encoded.
+ *
+ * @param uri URI of the entry point for the request.
+ * @param executor Executor to perform the request.
+ * @param callback Callback to execute when the request is finished.
+ */
+ public void getRequestMetadata(
+ final URI uri, Executor executor, final RequestMetadataCallback callback) {
+ executor.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ blockingGetToCallback(uri, callback);
+ }
+ });
+ }
+
+ /**
+ * Call {@link #getRequestMetadata(URI)} and pass the result or error to the callback.
+ *
+ * @param uri URI of the entry point for the request.
+ * @param callback Callback handler to execute when the metadata completes.
+ */
+ protected final void blockingGetToCallback(URI uri, RequestMetadataCallback callback) {
+ Map> result;
+ try {
+ result = getRequestMetadata(uri);
+ } catch (Throwable e) {
+ callback.onFailure(e);
+ return;
+ }
+ callback.onSuccess(result);
+ }
+
+ /**
+ * Get the current request metadata in a blocking manner, refreshing tokens if required.
+ *
+ *
This should be called by the transport layer on each request, and the data should be
+ * populated in headers or other context. The operation can block and fail to complete and may do
+ * things such as refreshing access tokens.
+ *
+ *
The convention for handling binary data is for the key in the returned map to end with
+ * {@code "-bin"} and for the corresponding values to be base64 encoded.
+ *
+ * @param uri URI of the entry point for the request.
+ * @return The request metadata used for populating headers or other context.
+ * @throws IOException if there was an error getting up-to-date access. The exception should
+ * implement {@link Retryable} and {@code isRetryable()} will return true if the operation may
+ * be retried.
+ */
+ public abstract Map> getRequestMetadata(URI uri) throws IOException;
+
+ /**
+ * Whether the credentials have metadata entries that should be added to each request.
+ *
+ *
This should be called by the transport layer to see if {@link
+ * Credentials#getRequestMetadata} should be used for each request.
+ *
+ * @return Whether or not the transport layer should call {@link Credentials#getRequestMetadata}
+ */
+ public abstract boolean hasRequestMetadata();
+
+ /**
+ * Indicates whether or not the Auth mechanism works purely by including request metadata.
+ *
+ *
This is meant for the transport layer. If this is true a transport does not need to take
+ * actions other than including the request metadata. If this is false, a transport must
+ * specifically know about the authentication technology to support it, and should fail to accept
+ * the credentials otherwise.
+ *
+ * @return Whether or not the Auth mechanism works purely by including request metadata.
+ */
+ public abstract boolean hasRequestMetadataOnly();
+
+ /**
+ * Refresh the authorization data, discarding any cached state.
+ *
+ *
For use by the transport to allow retry after getting an error indicating there may be
+ * invalid tokens or other cached state.
+ *
+ * @throws IOException if there was an error getting up-to-date access.
+ */
+ public abstract void refresh() throws IOException;
+}
diff --git a/google-auth-library-java/credentials/java/com/google/auth/RequestMetadataCallback.java b/google-auth-library-java/credentials/java/com/google/auth/RequestMetadataCallback.java
new file mode 100644
index 000000000000..68a20c4ac156
--- /dev/null
+++ b/google-auth-library-java/credentials/java/com/google/auth/RequestMetadataCallback.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2015, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The callback that receives the result of the asynchronous {@link
+ * Credentials#getRequestMetadata(java.net.URI, java.util.concurrent.Executor,
+ * RequestMetadataCallback)}. Exactly one method should be called.
+ */
+public interface RequestMetadataCallback {
+ /**
+ * Called when metadata is successfully produced.
+ *
+ * @param metadata Metadata returned for the request.
+ */
+ void onSuccess(Map> metadata);
+
+ /**
+ * Called when metadata generation failed.
+ *
+ * @param exception The thrown exception which caused the request metadata fetch to fail.
+ */
+ void onFailure(Throwable exception);
+}
diff --git a/google-auth-library-java/credentials/java/com/google/auth/Retryable.java b/google-auth-library-java/credentials/java/com/google/auth/Retryable.java
new file mode 100644
index 000000000000..a25410ebf219
--- /dev/null
+++ b/google-auth-library-java/credentials/java/com/google/auth/Retryable.java
@@ -0,0 +1,49 @@
+/*
+ * 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;
+
+// an interface to identify retryable errors
+public interface Retryable {
+ /**
+ * A flag indicating whether the error is retryable
+ *
+ * @return true if related error is retryable, false otherwise
+ */
+ boolean isRetryable();
+
+ /**
+ * Gets a number of performed retries for related HttpRequest
+ *
+ * @return a number of performed retries
+ */
+ int getRetryCount();
+}
diff --git a/google-auth-library-java/credentials/java/com/google/auth/ServiceAccountSigner.java b/google-auth-library-java/credentials/java/com/google/auth/ServiceAccountSigner.java
new file mode 100644
index 000000000000..840d447a55ef
--- /dev/null
+++ b/google-auth-library-java/credentials/java/com/google/auth/ServiceAccountSigner.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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;
+
+import java.util.Objects;
+
+/**
+ * Interface for a service account signer. A signer for a service account is capable of signing
+ * bytes using the private key associated with its service account.
+ */
+public interface ServiceAccountSigner {
+
+ class SigningException extends RuntimeException {
+
+ private static final long serialVersionUID = -6503954300538947223L;
+
+ public SigningException(String message, Exception cause) {
+ super(message, cause);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof SigningException)) {
+ return false;
+ }
+ SigningException other = (SigningException) obj;
+ return Objects.equals(getCause(), other.getCause())
+ && Objects.equals(getMessage(), other.getMessage());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getMessage(), getCause());
+ }
+ }
+
+ /**
+ * Returns the service account associated with the signer.
+ *
+ * @return The service account associated with the signer.
+ */
+ String getAccount();
+
+ /**
+ * Signs the provided bytes using the private key associated with the service account.
+ *
+ * @param toSign bytes to sign
+ * @return signed bytes
+ * @throws SigningException if the attempt to sign the provided bytes failed
+ */
+ byte[] sign(byte[] toSign);
+}
diff --git a/google-auth-library-java/credentials/javatests/com/google/auth/ApiKeyCredentialsTest.java b/google-auth-library-java/credentials/javatests/com/google/auth/ApiKeyCredentialsTest.java
new file mode 100644
index 000000000000..1de45daa9768
--- /dev/null
+++ b/google-auth-library-java/credentials/javatests/com/google/auth/ApiKeyCredentialsTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+/** Test case for {@link ApiKeyCredentials}. */
+class ApiKeyCredentialsTest {
+
+ private static final String TEST_API_KEY = "testApiKey";
+
+ @Test
+ void testGetAuthenticationType() {
+ ApiKeyCredentials credentials = ApiKeyCredentials.create(TEST_API_KEY);
+ assertEquals("API-Key", credentials.getAuthenticationType());
+ }
+
+ @Test
+ void testGetRequestMetadata() throws IOException, URISyntaxException {
+ ApiKeyCredentials credentials = ApiKeyCredentials.create(TEST_API_KEY);
+ Map> metadata = credentials.getRequestMetadata(new URI("http://test.com"));
+ assertEquals(1, metadata.size());
+ assertTrue(metadata.containsKey(ApiKeyCredentials.API_KEY_HEADER_KEY));
+ assertEquals(1, metadata.get(ApiKeyCredentials.API_KEY_HEADER_KEY).size());
+ assertEquals(TEST_API_KEY, metadata.get(ApiKeyCredentials.API_KEY_HEADER_KEY).get(0));
+ }
+
+ @Test
+ void testHasRequestMetadata() {
+ ApiKeyCredentials credentials = ApiKeyCredentials.create(TEST_API_KEY);
+ assertTrue(credentials.hasRequestMetadata());
+ }
+
+ @Test
+ void testHasRequestMetadataOnly() {
+ ApiKeyCredentials credentials = ApiKeyCredentials.create(TEST_API_KEY);
+ assertTrue(credentials.hasRequestMetadataOnly());
+ }
+
+ @Test
+ void testNullApiKey_ThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> ApiKeyCredentials.create(null));
+ }
+
+ @Test
+ void testBlankApiKey_ThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> ApiKeyCredentials.create(""));
+ }
+}
diff --git a/google-auth-library-java/credentials/javatests/com/google/auth/SigningExceptionTest.java b/google-auth-library-java/credentials/javatests/com/google/auth/SigningExceptionTest.java
new file mode 100644
index 000000000000..14293b082fc1
--- /dev/null
+++ b/google-auth-library-java/credentials/javatests/com/google/auth/SigningExceptionTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2016, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.auth.ServiceAccountSigner.SigningException;
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+
+class SigningExceptionTest {
+
+ private static final String EXPECTED_MESSAGE = "message";
+ private static final RuntimeException EXPECTED_CAUSE = new RuntimeException();
+
+ @Test
+ void constructor() {
+ SigningException signingException = new SigningException(EXPECTED_MESSAGE, EXPECTED_CAUSE);
+ assertEquals(EXPECTED_MESSAGE, signingException.getMessage());
+ assertSame(EXPECTED_CAUSE, signingException.getCause());
+ }
+
+ @Test
+ void equals_true() throws IOException {
+ SigningException signingException = new SigningException(EXPECTED_MESSAGE, EXPECTED_CAUSE);
+ SigningException otherSigningException = new SigningException(EXPECTED_MESSAGE, EXPECTED_CAUSE);
+ assertTrue(signingException.equals(otherSigningException));
+ assertTrue(otherSigningException.equals(signingException));
+ }
+
+ @Test
+ void equals_false_message() throws IOException {
+ SigningException signingException = new SigningException(EXPECTED_MESSAGE, EXPECTED_CAUSE);
+ SigningException otherSigningException = new SigningException("otherMessage", EXPECTED_CAUSE);
+ assertFalse(signingException.equals(otherSigningException));
+ assertFalse(otherSigningException.equals(signingException));
+ }
+
+ @Test
+ void equals_false_cause() throws IOException {
+ SigningException signingException = new SigningException(EXPECTED_MESSAGE, EXPECTED_CAUSE);
+ SigningException otherSigningException =
+ new SigningException("otherMessage", new RuntimeException());
+ assertFalse(signingException.equals(otherSigningException));
+ assertFalse(otherSigningException.equals(signingException));
+ }
+
+ @Test
+ void hashCode_equals() throws IOException {
+ SigningException signingException = new SigningException(EXPECTED_MESSAGE, EXPECTED_CAUSE);
+ SigningException otherSigningException = new SigningException(EXPECTED_MESSAGE, EXPECTED_CAUSE);
+ assertEquals(signingException.hashCode(), otherSigningException.hashCode());
+ }
+}
diff --git a/google-auth-library-java/credentials/pom.xml b/google-auth-library-java/credentials/pom.xml
new file mode 100644
index 000000000000..37778244f3bd
--- /dev/null
+++ b/google-auth-library-java/credentials/pom.xml
@@ -0,0 +1,60 @@
+
+
+ 4.0.0
+
+ com.google.auth
+ google-auth-library-parent
+ 1.43.1-SNAPSHOT
+ ../pom.xml
+
+
+ google-auth-library-credentials
+ Google Auth Library for Java - Credentials
+
+
+
+ ossrh
+ https://google.oss.sonatype.org/content/repositories/snapshots
+
+
+
+
+ java
+ javatests
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ com.google.auth
+
+
+
+
+
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+
diff --git a/google-auth-library-java/oauth2_http/EnableAutoValue.txt b/google-auth-library-java/oauth2_http/EnableAutoValue.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/google-auth-library-java/oauth2_http/clirr-ignored-differences.xml b/google-auth-library-java/oauth2_http/clirr-ignored-differences.xml
new file mode 100644
index 000000000000..00370fc99756
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/clirr-ignored-differences.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ 6001
+ com/google/auth/oauth2/ExternalAccountCredentials$Builder
+ quotaProjectId
+
+
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/http/AuthHttpConstants.java b/google-auth-library-java/oauth2_http/java/com/google/auth/http/AuthHttpConstants.java
new file mode 100644
index 000000000000..16da64bdc15d
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/http/AuthHttpConstants.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2015, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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.http;
+
+/** Constants used for auth in http */
+public class AuthHttpConstants {
+ /** HTTP "Bearer" authentication scheme */
+ public static final String BEARER = "Bearer";
+
+ /** HTTP "Authentication" header */
+ public static final String AUTHORIZATION = "Authorization";
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/http/HttpCredentialsAdapter.java b/google-auth-library-java/oauth2_http/java/com/google/auth/http/HttpCredentialsAdapter.java
new file mode 100644
index 000000000000..90a6c86f105e
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/http/HttpCredentialsAdapter.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2015, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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.http;
+
+import com.google.api.client.http.HttpHeaders;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestInitializer;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpStatusCodes;
+import com.google.api.client.http.HttpUnsuccessfulResponseHandler;
+import com.google.api.client.util.Preconditions;
+import com.google.auth.Credentials;
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+/** A wrapper for using Credentials with the Google API Client Libraries for Java with Http. */
+public class HttpCredentialsAdapter
+ implements HttpRequestInitializer, HttpUnsuccessfulResponseHandler {
+
+ private static final Logger LOGGER = Logger.getLogger(HttpCredentialsAdapter.class.getName());
+
+ /**
+ * In case an abnormal HTTP response is received with {@code WWW-Authenticate} header, and its
+ * value contains this error pattern, we will try to refresh the token.
+ */
+ private static final Pattern INVALID_TOKEN_ERROR =
+ Pattern.compile("\\s*error\\s*=\\s*\"?invalid_token\"?");
+
+ private final Credentials credentials;
+
+ /**
+ * @param credentials Credentials instance to adapt for HTTP
+ */
+ public HttpCredentialsAdapter(Credentials credentials) {
+ Preconditions.checkNotNull(credentials);
+ this.credentials = credentials;
+ }
+
+ /** A getter for the credentials instance being used */
+ public Credentials getCredentials() {
+ return credentials;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ *
Initialize the HTTP request prior to execution.
+ *
+ * @param request HTTP request
+ */
+ @Override
+ public void initialize(HttpRequest request) throws IOException {
+ request.setUnsuccessfulResponseHandler(this);
+
+ if (!credentials.hasRequestMetadata()) {
+ return;
+ }
+ HttpHeaders requestHeaders = request.getHeaders();
+ URI uri = null;
+ if (request.getUrl() != null) {
+ uri = request.getUrl().toURI();
+ }
+ Map> credentialHeaders = credentials.getRequestMetadata(uri);
+ if (credentialHeaders == null) {
+ return;
+ }
+ for (Map.Entry> entry : credentialHeaders.entrySet()) {
+ String headerName = entry.getKey();
+ List requestValues = new ArrayList<>();
+ requestValues.addAll(entry.getValue());
+ requestHeaders.put(headerName, requestValues);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ *
Checks if {@code WWW-Authenticate} exists and contains a "Bearer" value (see rfc6750 section 3.1 for more
+ * details). If so, it refreshes the token in case the error code contains {@code invalid_token}.
+ * If there is no "Bearer" in {@code WWW-Authenticate} and the status code is {@link
+ * HttpStatusCodes#STATUS_CODE_UNAUTHORIZED} it refreshes the token. If the token refresh throws
+ * an I/O exception, this implementation will log the exception and return {@code false}.
+ */
+ @Override
+ public boolean handleResponse(HttpRequest request, HttpResponse response, boolean supportsRetry) {
+ boolean refreshToken = false;
+ boolean bearer = false;
+
+ List authenticateList = response.getHeaders().getAuthenticateAsList();
+
+ // if authenticate list is not null we will check if one of the entries contains "Bearer"
+ if (authenticateList != null) {
+ for (String authenticate : authenticateList) {
+ if (authenticate.startsWith(InternalAuthHttpConstants.BEARER_PREFIX)) {
+ // mark that we found a "Bearer" value, and check if there is a invalid_token error
+ bearer = true;
+ refreshToken = INVALID_TOKEN_ERROR.matcher(authenticate).find();
+ break;
+ }
+ }
+ }
+
+ // if "Bearer" wasn't found, we will refresh the token, if we got 401
+ if (!bearer) {
+ refreshToken = response.getStatusCode() == HttpStatusCodes.STATUS_CODE_UNAUTHORIZED;
+ }
+
+ if (refreshToken) {
+ try {
+ credentials.refresh();
+ initialize(request);
+ return true;
+ } catch (IOException exception) {
+ LOGGER.log(Level.SEVERE, "unable to refresh token", exception);
+ }
+ }
+ return false;
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/http/HttpTransportFactory.java b/google-auth-library-java/oauth2_http/java/com/google/auth/http/HttpTransportFactory.java
new file mode 100644
index 000000000000..ae1e7e20ad5d
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/http/HttpTransportFactory.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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.http;
+
+import com.google.api.client.http.HttpTransport;
+
+/**
+ * A base interface for all {@link HttpTransport} factories.
+ *
+ *
Implementation must provide a public no-arg constructor. Loading of a factory implementation
+ * is done via {@link java.util.ServiceLoader}.
+ */
+public interface HttpTransportFactory {
+
+ /**
+ * Creates a {@code HttpTransport} instance.
+ *
+ * @return The HttpTransport instance.
+ */
+ HttpTransport create();
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/http/InternalAuthHttpConstants.java b/google-auth-library-java/oauth2_http/java/com/google/auth/http/InternalAuthHttpConstants.java
new file mode 100644
index 000000000000..192dadad5abc
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/http/InternalAuthHttpConstants.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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.http;
+
+/** Internal constants used for auth in http */
+class InternalAuthHttpConstants {
+ static final String BEARER_PREFIX = AuthHttpConstants.BEARER + " ";
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/CertificateSourceUnavailableException.java b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/CertificateSourceUnavailableException.java
new file mode 100644
index 000000000000..22a96ed224ad
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/CertificateSourceUnavailableException.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2025 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.mtls;
+
+import java.io.IOException;
+
+/**
+ * This exception is thrown by certificate providers in the Google auth library when the certificate
+ * source is unavailable. This means that the transport layer should move on to the next certificate
+ * source provider type.
+ */
+public class CertificateSourceUnavailableException extends IOException {
+
+ /**
+ * Constructor with a message and throwable cause.
+ *
+ * @param message The detail message (which is saved for later retrieval by the {@link
+ * #getMessage()} method)
+ * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method).
+ * (A null value is permitted, and indicates that the cause is nonexistent or unknown.)
+ */
+ public CertificateSourceUnavailableException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructor with a throwable cause.
+ *
+ * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method).
+ * (A null value is permitted, and indicates that the cause is nonexistent or unknown.)
+ */
+ public CertificateSourceUnavailableException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Constructor with a message.
+ *
+ * @param message The detail message (which is saved for later retrieval by the {@link
+ * #getMessage()} method)
+ */
+ public CertificateSourceUnavailableException(String message) {
+ super(message);
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/ContextAwareMetadataJson.java b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/ContextAwareMetadataJson.java
new file mode 100644
index 000000000000..11583c4d00d1
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/ContextAwareMetadataJson.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2025 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.mtls;
+
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.util.Key;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+
+/**
+ * Data class representing context_aware_metadata.json file. This is meant for internal Google Cloud
+ * usage and behavior may be changed without warning.
+ *
+ *
Note: This implementation is duplicated from the existing ContextAwareMetadataJson found in
+ * the Gax library. The Gax library version of ContextAwareMetadataJson will be marked as deprecated
+ * in the future.
+ */
+public class ContextAwareMetadataJson extends GenericJson {
+ /** Cert provider command */
+ @Key("cert_provider_command")
+ private List commands;
+
+ /** Returns the cert provider command. */
+ public final ImmutableList getCommands() {
+ return ImmutableList.copyOf(commands);
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/DefaultMtlsProviderFactory.java b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/DefaultMtlsProviderFactory.java
new file mode 100644
index 000000000000..b57accd4c224
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/DefaultMtlsProviderFactory.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2025 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.mtls;
+
+import java.io.IOException;
+
+public class DefaultMtlsProviderFactory {
+
+ /**
+ * Creates an instance of {@link MtlsProvider}. It first attempts to create an {@link
+ * com.google.auth.mtls.X509Provider}. If the certificate source is unavailable, it falls back to
+ * creating a {@link SecureConnectProvider}. If the secure connect provider also fails, it throws
+ * a {@link com.google.auth.mtls.CertificateSourceUnavailableException}.
+ *
+ *
This is only meant to be used internally by Google Cloud libraries, and the public facing
+ * methods may be changed without notice, and have no guarantee of backwards compatibility.
+ *
+ * @return an instance of {@link MtlsProvider}.
+ * @throws com.google.auth.mtls.CertificateSourceUnavailableException if neither provider can be
+ * created.
+ * @throws IOException if an I/O error occurs during provider creation.
+ */
+ public static MtlsProvider create() throws IOException {
+ // Note: The caller should handle CertificateSourceUnavailableException gracefully, since
+ // it is an expected error case. All other IOExceptions are unexpected and should be surfaced
+ // up the call stack.
+ MtlsProvider mtlsProvider = new X509Provider();
+ if (mtlsProvider.isAvailable()) {
+ return mtlsProvider;
+ }
+ mtlsProvider = new SecureConnectProvider();
+ if (mtlsProvider.isAvailable()) {
+ return mtlsProvider;
+ }
+ throw new CertificateSourceUnavailableException(
+ "No Certificate Source is available on this device.");
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java
new file mode 100644
index 000000000000..fe4c142098ca
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2025, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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.mtls;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.auth.http.HttpTransportFactory;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.Objects;
+
+/**
+ * An HttpTransportFactory that creates {@link NetHttpTransport} instances configured for mTLS
+ * (mutual TLS) using a specific {@link KeyStore} containing the client's certificate and private
+ * key.
+ *
+ *
Warning: This class is considered internal and is not intended for direct use by
+ * library consumers. Its API and behavior may change without notice.
+ */
+public class MtlsHttpTransportFactory implements HttpTransportFactory {
+ private final KeyStore mtlsKeyStore;
+
+ /**
+ * Constructs a factory for mTLS transports.
+ *
+ * @param mtlsKeyStore The {@link KeyStore} containing the client's X509 certificate and private
+ * key. This {@link KeyStore} is used for client authentication during the TLS handshake. Must
+ * not be null.
+ */
+ public MtlsHttpTransportFactory(KeyStore mtlsKeyStore) {
+ this.mtlsKeyStore = Objects.requireNonNull(mtlsKeyStore, "mtlsKeyStore cannot be null");
+ }
+
+ @Override
+ public NetHttpTransport create() {
+ try {
+ // Build the mTLS transport using the provided KeyStore.
+ return new NetHttpTransport.Builder().trustCertificates(null, mtlsKeyStore, "").build();
+ } catch (GeneralSecurityException e) {
+ // Wrap the checked exception in a RuntimeException because the HttpTransportFactory
+ // interface's create() method doesn't allow throwing checked exceptions.
+ throw new RuntimeException("Failed to initialize mTLS transport.", e);
+ }
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsProvider.java b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsProvider.java
new file mode 100644
index 000000000000..edc412552d06
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsProvider.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2025 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.mtls;
+
+import java.io.IOException;
+import java.security.KeyStore;
+
+/**
+ * MtlsProvider is used by the Gax library for configuring mutual TLS in the HTTP and GRPC transport
+ * layer. The source of the client certificate is up to the implementation.
+ *
+ *
Note: This interface will replace the identically named "MtlsProvider" implementation in the
+ * Gax library. The Gax library version of MtlsProvider will be marked as deprecated. See
+ * https://github.com/googleapis/google-auth-library-java/issues/1758
+ */
+public interface MtlsProvider {
+ /**
+ * Returns a mutual TLS key store.
+ *
+ * @return KeyStore for configuring mTLS.
+ * @throws CertificateSourceUnavailableException if the certificate source is unavailable (ex.
+ * missing configuration file).
+ * @throws IOException if a general I/O error occurs while creating the KeyStore
+ */
+ KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException;
+
+ /**
+ * Returns true if the underlying mTLS provider is available.
+ *
+ * @throws IOException if a general I/O error occurs while determining availability.
+ */
+ boolean isAvailable() throws IOException;
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/SecureConnectProvider.java b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/SecureConnectProvider.java
new file mode 100644
index 000000000000..9d30d6800afe
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/SecureConnectProvider.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2025 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.mtls;
+
+import com.google.api.client.json.JsonParser;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.api.client.util.SecurityUtils;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class implements {@link MtlsProvider} for the Google Auth library transport layer via {@link
+ * ContextAwareMetadataJson}. This is only meant to be used internally by Google Cloud libraries,
+ * and the public facing methods may be changed without notice, and have no guarantee of backwards
+ * compatibility.
+ *
+ *
Note: This implementation is derived from the existing "MtlsProvider" found in the Gax
+ * library, with two notable differences: 1) All logic associated with parsing environment variables
+ * related to "mTLS usage" are omitted - a separate helper class will be introduced in the Gax
+ * library to serve this purpose. 2) getKeyStore throws {@link
+ * com.google.auth.mtls.CertificateSourceUnavailableException} instead of returning "null" if this
+ * cert source is not available on the device.
+ *
+ *
Additionally, this implementation will replace the existing "MtlsProvider" in the Gax library.
+ * The Gax library version of MtlsProvider will be marked as deprecated.
+ */
+public class SecureConnectProvider implements MtlsProvider {
+ interface ProcessProvider {
+ public Process createProcess(InputStream metadata) throws IOException;
+ }
+
+ static class DefaultProcessProvider implements ProcessProvider {
+ @Override
+ public Process createProcess(InputStream metadata) throws IOException {
+ if (metadata == null) {
+ throw new IOException("Error creating Process: metadata is null");
+ }
+ List command = extractCertificateProviderCommand(metadata);
+ return new ProcessBuilder(command).start();
+ }
+ }
+
+ private static final String DEFAULT_CONTEXT_AWARE_METADATA_PATH =
+ System.getProperty("user.home") + "/.secureConnect/context_aware_metadata.json";
+
+ private String metadataPath;
+ private ProcessProvider processProvider;
+
+ @VisibleForTesting
+ SecureConnectProvider(ProcessProvider processProvider, String metadataPath) {
+ this.processProvider = processProvider;
+ this.metadataPath = metadataPath;
+ }
+
+ public SecureConnectProvider() {
+ this(new DefaultProcessProvider(), DEFAULT_CONTEXT_AWARE_METADATA_PATH);
+ }
+
+ /**
+ * Returns a mutual TLS key store backed by the certificate provided by the SecureConnect tool.
+ *
+ * @return a KeyStore containing the certificate provided by the SecureConnect tool.
+ * @throws CertificateSourceUnavailableException if the certificate source is unavailable (ex.
+ * missing configuration file).
+ * @throws IOException if a general I/O error occurs while creating the KeyStore.
+ */
+ @Override
+ public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException {
+ try (InputStream stream = new FileInputStream(metadataPath)) {
+ return getKeyStore(stream, processProvider);
+ } catch (InterruptedException e) {
+ throw new IOException("SecureConnect: Interrupted executing certificate provider command", e);
+ } catch (GeneralSecurityException e) {
+ throw new CertificateSourceUnavailableException(
+ "SecureConnect encountered GeneralSecurityException:", e);
+ } catch (FileNotFoundException exception) {
+ // If the metadata file doesn't exist, then there is no key store, so we will throw sentinel
+ // error
+ throw new CertificateSourceUnavailableException("SecureConnect metadata does not exist.");
+ }
+ }
+
+ /**
+ * Returns true if the SecureConnect mTLS provider is available.
+ *
+ * @throws IOException if a general I/O error occurs while determining availability.
+ */
+ @Override
+ public boolean isAvailable() throws IOException {
+ try {
+ this.getKeyStore();
+ } catch (CertificateSourceUnavailableException e) {
+ return false;
+ }
+ return true;
+ }
+
+ @VisibleForTesting
+ static KeyStore getKeyStore(InputStream metadata, ProcessProvider processProvider)
+ throws IOException, InterruptedException, GeneralSecurityException {
+ Process process = processProvider.createProcess(metadata);
+
+ // Run the command and timeout after 1000 milliseconds.
+ // The cert provider command usually finishes instantly (if it doesn't hang),
+ // so 1000 milliseconds is plenty of time.
+ int exitCode = runCertificateProviderCommand(process, 1000);
+ if (exitCode != 0) {
+ throw new IOException(
+ "SecureConnect: Cert provider command failed with exit code: " + exitCode);
+ }
+
+ // Create mTLS key store with the input certificates from shell command.
+ return SecurityUtils.createMtlsKeyStore(process.getInputStream());
+ }
+
+ @VisibleForTesting
+ static ImmutableList extractCertificateProviderCommand(InputStream contextAwareMetadata)
+ throws IOException {
+ JsonParser parser = new GsonFactory().createJsonParser(contextAwareMetadata);
+ ContextAwareMetadataJson json = parser.parse(ContextAwareMetadataJson.class);
+ return json.getCommands();
+ }
+
+ @VisibleForTesting
+ static int runCertificateProviderCommand(Process commandProcess, long timeoutMilliseconds)
+ throws IOException, InterruptedException {
+ boolean terminated = commandProcess.waitFor(timeoutMilliseconds, TimeUnit.MILLISECONDS);
+ if (!terminated) {
+ commandProcess.destroy();
+ throw new IOException("SecureConnect: Cert provider command timed out");
+ }
+ return commandProcess.exitValue();
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/WorkloadCertificateConfiguration.java b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/WorkloadCertificateConfiguration.java
new file mode 100644
index 000000000000..db439eea584f
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/WorkloadCertificateConfiguration.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2025 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.mtls;
+
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.JsonObjectParser;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+class WorkloadCertificateConfiguration {
+
+ private String certPath;
+ private String privateKeyPath;
+
+ private static JsonFactory jsonFactory = GsonFactory.getDefaultInstance();
+ private static JsonObjectParser parser = new JsonObjectParser(jsonFactory);
+
+ WorkloadCertificateConfiguration(String certPath, String privateKeyPath) {
+ this.certPath = certPath;
+ this.privateKeyPath = privateKeyPath;
+ }
+
+ String getCertPath() {
+ return certPath;
+ }
+
+ String getPrivateKeyPath() {
+ return privateKeyPath;
+ }
+
+ static WorkloadCertificateConfiguration fromCertificateConfigurationStream(
+ InputStream certConfigStream) throws IOException {
+ Preconditions.checkNotNull(certConfigStream);
+
+ GenericJson fileContents =
+ parser.parseAndClose(certConfigStream, StandardCharsets.UTF_8, GenericJson.class);
+
+ Map certConfigs = (Map) fileContents.get("cert_configs");
+ if (certConfigs == null) {
+ throw new IllegalArgumentException(
+ "The cert_configs object must be provided in the certificate configuration file.");
+ }
+
+ Map workloadConfig = (Map) certConfigs.get("workload");
+ if (workloadConfig == null) {
+ // Throw a CertificateSourceUnavailableException because there is no workload cert source.
+ // This tells the transport layer that it should check for another certificate source type.
+ throw new CertificateSourceUnavailableException(
+ "A workload certificate configuration must be provided in the cert_configs object.");
+ }
+
+ String certPath = (String) workloadConfig.get("cert_path");
+ if (Strings.isNullOrEmpty(certPath)) {
+ throw new IllegalArgumentException(
+ "The cert_path field must be provided in the workload certificate configuration.");
+ }
+
+ String privateKeyPath = (String) workloadConfig.get("key_path");
+ if (Strings.isNullOrEmpty(privateKeyPath)) {
+ throw new IllegalArgumentException(
+ "The key_path field must be provided in the workload certificate configuration.");
+ }
+
+ return new WorkloadCertificateConfiguration(certPath, privateKeyPath);
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java
new file mode 100644
index 000000000000..7ff490f0f147
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/mtls/X509Provider.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2025 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.mtls;
+
+import com.google.api.client.util.SecurityUtils;
+import com.google.common.base.Strings;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.SequenceInputStream;
+import java.security.KeyStore;
+import java.util.Locale;
+
+/**
+ * This class implements {@link MtlsProvider} for the Google Auth library transport layer via {@link
+ * WorkloadCertificateConfiguration}. This is only meant to be used internally by Google Cloud
+ * libraries, and the public facing methods may be changed without notice, and have no guarantee of
+ * backwards compatibility.
+ */
+public class X509Provider implements MtlsProvider {
+ static final String CERTIFICATE_CONFIGURATION_ENV_VARIABLE = "GOOGLE_API_CERTIFICATE_CONFIG";
+ static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json";
+ static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud";
+
+ private final String certConfigPathOverride;
+
+ /**
+ * Creates an X509 provider with an override path for the certificate configuration, bypassing the
+ * normal checks for the well known certificate configuration file path and environment variable.
+ * This is meant for internal Google Cloud usage and behavior may be changed without warning.
+ *
+ * @param certConfigPathOverride the path to read the certificate configuration from.
+ */
+ public X509Provider(String certConfigPathOverride) {
+ this.certConfigPathOverride = certConfigPathOverride;
+ }
+
+ /**
+ * Creates a new X.509 provider that will check the environment variable path and the well known
+ * Gcloud certificate configuration location. This is meant for internal Google Cloud usage and
+ * behavior may be changed without warning.
+ */
+ public X509Provider() {
+ this(null);
+ }
+
+ /**
+ * Returns the path to the client certificate file specified by the loaded workload certificate
+ * configuration.
+ *
+ *
If the configuration has not been loaded yet (e.g., if {@link #getKeyStore()} has not been
+ * called), this method will attempt to load it first by searching the override path, environment
+ * variable, and well-known locations.
+ *
+ * @return The path to the certificate file.
+ * @throws IOException if the certificate configuration cannot be found or loaded, or if the
+ * configuration file does not specify a certificate path.
+ * @throws CertificateSourceUnavailableException if the configuration file is not found.
+ */
+ public String getCertificatePath() throws IOException {
+ String certPath = getWorkloadCertificateConfiguration().getCertPath();
+ if (Strings.isNullOrEmpty(certPath)) {
+ // Ensure the loaded configuration actually contains the required path.
+ throw new CertificateSourceUnavailableException(
+ "Certificate configuration loaded successfully, but does not contain a 'certificate_file' path.");
+ }
+ return certPath;
+ }
+
+ /**
+ * Finds the certificate configuration file, then builds a Keystore using the X.509 certificate
+ * and private key pointed to by the configuration. This will check the following locations in
+ * order.
+ *
+ *
+ *
The certificate config override path, if set.
+ *
The path pointed to by the "GOOGLE_API_CERTIFICATE_CONFIG" environment variable
+ *
The well known gcloud location for the certificate configuration file.
+ *
+ *
+ * @return a KeyStore containing the X.509 certificate specified by the certificate configuration.
+ * @throws CertificateSourceUnavailableException if the certificate source is unavailable (ex.
+ * missing configuration file)
+ * @throws IOException if a general I/O error occurs while creating the KeyStore
+ */
+ @Override
+ public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException {
+ WorkloadCertificateConfiguration workloadCertConfig = getWorkloadCertificateConfiguration();
+
+ // Read the certificate and private key file paths into streams.
+ try (InputStream certStream = createInputStream(new File(workloadCertConfig.getCertPath()));
+ InputStream privateKeyStream =
+ createInputStream(new File(workloadCertConfig.getPrivateKeyPath()));
+ SequenceInputStream certAndPrivateKeyStream =
+ new SequenceInputStream(certStream, privateKeyStream)) {
+
+ // Build a key store using the combined stream.
+ return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
+ } catch (CertificateSourceUnavailableException e) {
+ // Throw the CertificateSourceUnavailableException without wrapping.
+ throw e;
+ } catch (Exception e) {
+ // Wrap all other exception types to an IOException.
+ throw new IOException("X509Provider: Unexpected IOException:", e);
+ }
+ }
+
+ /**
+ * Returns true if the X509 mTLS provider is available.
+ *
+ * @throws IOException if a general I/O error occurs while determining availability.
+ */
+ @Override
+ public boolean isAvailable() throws IOException {
+ try {
+ this.getKeyStore();
+ } catch (CertificateSourceUnavailableException e) {
+ return false;
+ }
+ return true;
+ }
+
+ private WorkloadCertificateConfiguration getWorkloadCertificateConfiguration()
+ throws IOException {
+ File certConfig;
+ if (this.certConfigPathOverride != null) {
+ certConfig = new File(certConfigPathOverride);
+ } else {
+ String envCredentialsPath = getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
+ if (!Strings.isNullOrEmpty(envCredentialsPath)) {
+ certConfig = new File(envCredentialsPath);
+ } else {
+ certConfig = getWellKnownCertificateConfigFile();
+ }
+ }
+ InputStream certConfigStream = null;
+ try {
+ if (!isFile(certConfig)) {
+ // Path will be put in the message from the catch block below
+ throw new CertificateSourceUnavailableException("File does not exist.");
+ }
+ certConfigStream = createInputStream(certConfig);
+ return WorkloadCertificateConfiguration.fromCertificateConfigurationStream(certConfigStream);
+ } finally {
+ if (certConfigStream != null) {
+ certConfigStream.close();
+ }
+ }
+ }
+
+ /*
+ * Start of methods to allow overriding in the test code to isolate from the environment.
+ */
+ boolean isFile(File file) {
+ return file.isFile();
+ }
+
+ InputStream createInputStream(File file) throws FileNotFoundException {
+ return new FileInputStream(file);
+ }
+
+ String getEnv(String name) {
+ return System.getenv(name);
+ }
+
+ String getOsName() {
+ return getProperty("os.name", "").toLowerCase(Locale.US);
+ }
+
+ String getProperty(String property, String def) {
+ return System.getProperty(property, def);
+ }
+
+ /*
+ * End of methods to allow overriding in the test code to isolate from the environment.
+ */
+
+ private File getWellKnownCertificateConfigFile() {
+ File cloudConfigPath;
+ String envPath = getEnv("CLOUDSDK_CONFIG");
+ if (envPath != null) {
+ cloudConfigPath = new File(envPath);
+ } else if (getOsName().indexOf("windows") >= 0) {
+ File appDataPath = new File(getEnv("APPDATA"));
+ cloudConfigPath = new File(appDataPath, CLOUDSDK_CONFIG_DIRECTORY);
+ } else {
+ File configPath = new File(getProperty("user.home", ""), ".config");
+ cloudConfigPath = new File(configPath, CLOUDSDK_CONFIG_DIRECTORY);
+ }
+ return new File(cloudConfigPath, WELL_KNOWN_CERTIFICATE_CONFIG_FILE);
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AccessToken.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AccessToken.java
new file mode 100644
index 000000000000..40032ca47afc
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AccessToken.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2015, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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.common.base.MoreObjects;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+
+/** Represents a temporary OAuth2 access token and its expiration information. */
+public class AccessToken implements Serializable {
+
+ private static final long serialVersionUID = -8514239465808977353L;
+
+ private final String tokenValue;
+ private final Long expirationTimeMillis;
+ private final List scopes;
+
+ /**
+ * @param tokenValue String representation of the access token.
+ * @param expirationTime Time when access token will expire.
+ */
+ public AccessToken(String tokenValue, Date expirationTime) {
+ this.tokenValue = tokenValue;
+ this.expirationTimeMillis = (expirationTime == null) ? null : expirationTime.getTime();
+ this.scopes = new ArrayList<>();
+ }
+
+ private AccessToken(Builder builder) {
+ this.tokenValue = builder.getTokenValue();
+ Date expirationTime = builder.getExpirationTime();
+ this.expirationTimeMillis = (expirationTime == null) ? null : expirationTime.getTime();
+ this.scopes = builder.getScopes();
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ /**
+ * Scopes from the access token response. Not all credentials provide scopes in response and as
+ * per https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 it is optional in the response.
+ *
+ * @return List of scopes
+ */
+ public List getScopes() {
+ return scopes;
+ }
+
+ /**
+ * String representation of the access token.
+ *
+ * @return The raw access token string value.
+ */
+ public String getTokenValue() {
+ return tokenValue;
+ }
+
+ /**
+ * Time when access token will expire.
+ *
+ * @return The expiration time as a {@link Date}.
+ */
+ public Date getExpirationTime() {
+ if (expirationTimeMillis == null) {
+ return null;
+ }
+ return new Date(expirationTimeMillis);
+ }
+
+ Long getExpirationTimeMillis() {
+ return expirationTimeMillis;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(tokenValue, expirationTimeMillis, scopes);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("tokenValue", tokenValue)
+ .add("expirationTimeMillis", expirationTimeMillis)
+ .add("scopes", scopes)
+ .toString();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof AccessToken)) {
+ return false;
+ }
+ AccessToken other = (AccessToken) obj;
+ return Objects.equals(this.tokenValue, other.tokenValue)
+ && Objects.equals(this.expirationTimeMillis, other.expirationTimeMillis)
+ && Objects.equals(this.scopes, other.scopes);
+ }
+
+ public static class Builder {
+ private String tokenValue;
+ private Date expirationTime;
+ private List scopes = new ArrayList<>();
+
+ protected Builder() {}
+
+ protected Builder(AccessToken accessToken) {
+ this.tokenValue = accessToken.getTokenValue();
+ this.expirationTime = accessToken.getExpirationTime();
+ this.scopes = accessToken.getScopes();
+ }
+
+ public String getTokenValue() {
+ return this.tokenValue;
+ }
+
+ public List getScopes() {
+ return this.scopes;
+ }
+
+ public Date getExpirationTime() {
+ return this.expirationTime;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setTokenValue(String tokenValue) {
+ this.tokenValue = tokenValue;
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setScopes(String scopes) {
+ if (scopes != null && scopes.trim().length() > 0) {
+ this.scopes = Arrays.asList(scopes.split(" "));
+ }
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setScopes(List scopes) {
+ if (scopes == null) {
+ this.scopes = new ArrayList<>();
+ } else {
+ this.scopes = scopes;
+ }
+
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setExpirationTime(Date expirationTime) {
+ this.expirationTime = expirationTime;
+ return this;
+ }
+
+ public AccessToken build() {
+ return new AccessToken(this);
+ }
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ActingParty.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ActingParty.java
new file mode 100644
index 000000000000..ad1d452fc028
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ActingParty.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2021 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;
+
+/**
+ * The acting party as defined in OAuth 2.0 Token
+ * Exchange.
+ */
+final class ActingParty {
+ private final String actorToken;
+ private final String actorTokenType;
+
+ ActingParty(String actorToken, String actorTokenType) {
+ this.actorToken = checkNotNull(actorToken);
+ this.actorTokenType = checkNotNull(actorTokenType);
+ }
+
+ String getActorToken() {
+ return actorToken;
+ }
+
+ String getActorTokenType() {
+ return actorTokenType;
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AppEngineCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AppEngineCredentials.java
new file mode 100644
index 000000000000..2ab1db8570fc
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AppEngineCredentials.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2016, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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.ServiceAccountSigner;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * OAuth2 credentials representing the built-in service account for Google App Engine.
+ *
+ *
Instances of this class use reflection to access AppIdentityService in AppEngine SDK.
+ */
+class AppEngineCredentials extends GoogleCredentials implements ServiceAccountSigner {
+
+ private static final long serialVersionUID = -493219027336622194L;
+
+ static final String APP_IDENTITY_SERVICE_FACTORY_CLASS =
+ "com.google.appengine.api.appidentity.AppIdentityServiceFactory";
+ static final String APP_IDENTITY_SERVICE_CLASS =
+ "com.google.appengine.api.appidentity.AppIdentityService";
+ static final String GET_ACCESS_TOKEN_RESULT_CLASS =
+ "com.google.appengine.api.appidentity.AppIdentityService$GetAccessTokenResult";
+ static final String SIGNING_RESULT_CLASS =
+ "com.google.appengine.api.appidentity.AppIdentityService$SigningResult";
+ private static final String GET_APP_IDENTITY_SERVICE_METHOD = "getAppIdentityService";
+ private static final String GET_ACCESS_TOKEN_RESULT_METHOD = "getAccessToken";
+ private static final String GET_ACCESS_TOKEN_METHOD = "getAccessToken";
+ private static final String GET_EXPIRATION_TIME_METHOD = "getExpirationTime";
+ private static final String GET_SERVICE_ACCOUNT_NAME_METHOD = "getServiceAccountName";
+ private static final String SIGN_FOR_APP_METHOD = "signForApp";
+ private static final String GET_SIGNATURE_METHOD = "getSignature";
+
+ private final Collection scopes;
+ private final boolean scopesRequired;
+
+ private transient Object appIdentityService;
+ private transient Method getAccessToken;
+ private transient Method getAccessTokenResult;
+ private transient Method getExpirationTime;
+ private transient Method signForApp;
+ private transient Method getSignature;
+ private transient String account;
+
+ AppEngineCredentials(Collection scopes, Collection defaultScopes)
+ throws IOException {
+ // Use defaultScopes only when scopes don't exist.
+ if (scopes == null || scopes.isEmpty()) {
+ this.scopes =
+ defaultScopes == null ? ImmutableList.of() : ImmutableList.copyOf(defaultScopes);
+ } else {
+ this.scopes = ImmutableList.copyOf(scopes);
+ }
+ this.scopesRequired = this.scopes.isEmpty();
+ init();
+ }
+
+ AppEngineCredentials(
+ Collection scopes, Collection defaultScopes, AppEngineCredentials unscoped) {
+ this.appIdentityService = unscoped.appIdentityService;
+ this.getAccessToken = unscoped.getAccessToken;
+ this.getAccessTokenResult = unscoped.getAccessTokenResult;
+ this.getExpirationTime = unscoped.getExpirationTime;
+ // Use defaultScopes only when scopes don't exist.
+ if (scopes == null || scopes.isEmpty()) {
+ this.scopes =
+ defaultScopes == null ? ImmutableSet.of() : ImmutableList.copyOf(defaultScopes);
+ } else {
+ this.scopes = ImmutableList.copyOf(scopes);
+ }
+ this.scopesRequired = this.scopes.isEmpty();
+ }
+
+ private void init() throws IOException {
+ try {
+ Class> factoryClass = forName(APP_IDENTITY_SERVICE_FACTORY_CLASS);
+ Method method = factoryClass.getMethod(GET_APP_IDENTITY_SERVICE_METHOD);
+ this.appIdentityService = method.invoke(null);
+ Class> serviceClass = forName(APP_IDENTITY_SERVICE_CLASS);
+ Class> tokenResultClass = forName(GET_ACCESS_TOKEN_RESULT_CLASS);
+ this.getAccessTokenResult =
+ serviceClass.getMethod(GET_ACCESS_TOKEN_RESULT_METHOD, Iterable.class);
+ this.getAccessToken = tokenResultClass.getMethod(GET_ACCESS_TOKEN_METHOD);
+ this.getExpirationTime = tokenResultClass.getMethod(GET_EXPIRATION_TIME_METHOD);
+ this.account =
+ (String)
+ serviceClass.getMethod(GET_SERVICE_ACCOUNT_NAME_METHOD).invoke(appIdentityService);
+ this.signForApp = serviceClass.getMethod(SIGN_FOR_APP_METHOD, byte[].class);
+ Class> signingResultClass = forName(SIGNING_RESULT_CLASS);
+ this.getSignature = signingResultClass.getMethod(GET_SIGNATURE_METHOD);
+ this.name = GoogleCredentialsInfo.APP_ENGINE_CREDENTIALS.getCredentialName();
+ } catch (ClassNotFoundException
+ | NoSuchMethodException
+ | IllegalAccessException
+ | InvocationTargetException ex) {
+ throw new IOException(
+ "Application Default Credentials failed to create the Google App Engine service account"
+ + " credentials. Check that the App Engine SDK is deployed.",
+ ex);
+ }
+ }
+
+ /** Refresh the access token by getting it from the App Identity service. */
+ @Override
+ public AccessToken refreshAccessToken() throws IOException {
+ if (createScopedRequired()) {
+ throw new IOException("AppEngineCredentials requires createScoped call before use.");
+ }
+ try {
+ Object accessTokenResult = getAccessTokenResult.invoke(appIdentityService, scopes);
+ String accessToken = (String) getAccessToken.invoke(accessTokenResult);
+ Date expirationTime = (Date) getExpirationTime.invoke(accessTokenResult);
+ return new AccessToken(accessToken, expirationTime);
+ } catch (Exception e) {
+ throw new IOException("Could not get the access token.", e);
+ }
+ }
+
+ @Override
+ public boolean createScopedRequired() {
+ return scopesRequired;
+ }
+
+ @Override
+ public GoogleCredentials createScoped(Collection scopes) {
+ return new AppEngineCredentials(scopes, null, this);
+ }
+
+ @Override
+ public GoogleCredentials createScoped(
+ Collection scopes, Collection defaultScopes) {
+ return new AppEngineCredentials(scopes, defaultScopes, this);
+ }
+
+ @Override
+ public String getAccount() {
+ return account;
+ }
+
+ @Override
+ public byte[] sign(byte[] toSign) {
+ try {
+ Object signingResult = signForApp.invoke(appIdentityService, toSign);
+ return (byte[]) getSignature.invoke(signingResult);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
+ throw new SigningException("Failed to sign the provided bytes", ex);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(scopes, scopesRequired);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("scopes", scopes)
+ .add("scopesRequired", scopesRequired)
+ .toString();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof AppEngineCredentials)) {
+ return false;
+ }
+ AppEngineCredentials other = (AppEngineCredentials) obj;
+ return this.scopesRequired == other.scopesRequired && Objects.equals(this.scopes, other.scopes);
+ }
+
+ private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
+ input.defaultReadObject();
+ init();
+ }
+
+ /*
+ * Start of methods to allow overriding in the test code to isolate from the environment.
+ */
+
+ Class> forName(String className) throws ClassNotFoundException {
+ return Class.forName(className);
+ }
+
+ /*
+ * End of methods to allow overriding in the test code to isolate from the environment.
+ */
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsCredentialSource.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsCredentialSource.java
new file mode 100644
index 000000000000..a48b7d51d5a7
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsCredentialSource.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2023 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.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** The AWS credential source. Stores data required to retrieve the AWS credential. */
+public class AwsCredentialSource extends ExternalAccountCredentials.CredentialSource {
+
+ static final String IMDSV2_SESSION_TOKEN_URL_FIELD_NAME = "imdsv2_session_token_url";
+ static final long serialVersionUID = -4180558200808134436L;
+
+ final String regionUrl;
+ final String url;
+ final String regionalCredentialVerificationUrl;
+ final String imdsv2SessionTokenUrl;
+
+ /**
+ * The source of the AWS credential. The credential source map must contain the
+ * `regional_cred_verification_url` field.
+ *
+ *
The `regional_cred_verification_url` is the regional GetCallerIdentity action URL, used to
+ * determine the account ID and its roles.
+ *
+ *
The `environment_id` is the environment identifier, in the format “aws${version}”. This
+ * indicates whether breaking changes were introduced to the underlying AWS implementation.
+ *
+ *
The `region_url` identifies the targeted region. Optional.
+ *
+ *
The `url` locates the metadata server used to retrieve the AWS credentials. Optional.
+ */
+ public AwsCredentialSource(Map credentialSourceMap) {
+ super(credentialSourceMap);
+ if (!credentialSourceMap.containsKey("regional_cred_verification_url")) {
+ throw new IllegalArgumentException(
+ "A regional_cred_verification_url representing the"
+ + " GetCallerIdentity action URL must be specified.");
+ }
+
+ String environmentId = (String) credentialSourceMap.get("environment_id");
+
+ // Environment version is prefixed by "aws". e.g. "aws1".
+ Matcher matcher = Pattern.compile("(aws)([\\d]+)").matcher(environmentId);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("Invalid AWS environment ID.");
+ }
+
+ int environmentVersion = Integer.parseInt(matcher.group(2));
+ if (environmentVersion != 1) {
+ throw new IllegalArgumentException(
+ String.format(
+ "AWS version %s is not supported in the current build.", environmentVersion));
+ }
+
+ this.regionUrl = (String) credentialSourceMap.get("region_url");
+ this.url = (String) credentialSourceMap.get("url");
+ this.regionalCredentialVerificationUrl =
+ (String) credentialSourceMap.get("regional_cred_verification_url");
+
+ if (credentialSourceMap.containsKey(IMDSV2_SESSION_TOKEN_URL_FIELD_NAME)) {
+ this.imdsv2SessionTokenUrl =
+ (String) credentialSourceMap.get(IMDSV2_SESSION_TOKEN_URL_FIELD_NAME);
+ } else {
+ this.imdsv2SessionTokenUrl = null;
+ }
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java
new file mode 100644
index 000000000000..3085765317b7
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2021 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.auth.http.HttpTransportFactory;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * Credentials representing an AWS third-party identity for calling Google APIs. AWS security
+ * credentials are either sourced by calling EC2 metadata endpoints, environment variables, or a
+ * user provided supplier method.
+ *
+ *
By default, attempts to exchange the external credential for a GCP access token.
+ */
+public class AwsCredentials extends ExternalAccountCredentials {
+
+ static final String DEFAULT_REGIONAL_CREDENTIAL_VERIFICATION_URL =
+ "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15";
+
+ static final String AWS_METRICS_HEADER_VALUE = "aws";
+
+ private static final long serialVersionUID = -3670131891574618105L;
+
+ private final AwsSecurityCredentialsSupplier awsSecurityCredentialsSupplier;
+ private final ExternalAccountSupplierContext supplierContext;
+ // Regional credential verification url override. This needs to be its own value so we can
+ // correctly pass it to a builder.
+ @Nullable private final String regionalCredentialVerificationUrlOverride;
+ @Nullable private final String regionalCredentialVerificationUrl;
+ private final String metricsHeaderValue;
+
+ /** Internal constructor. See {@link AwsCredentials.Builder}. */
+ AwsCredentials(Builder builder) {
+ super(builder);
+ this.supplierContext =
+ ExternalAccountSupplierContext.newBuilder()
+ .setAudience(this.getAudience())
+ .setSubjectTokenType(this.getSubjectTokenType())
+ .build();
+
+ // Check that one and only one of supplier or credential source are provided.
+ if (builder.awsSecurityCredentialsSupplier != null && builder.credentialSource != null) {
+ throw new IllegalArgumentException(
+ "AwsCredentials cannot have both an awsSecurityCredentialsSupplier and a credentialSource.");
+ }
+ if (builder.awsSecurityCredentialsSupplier == null && builder.credentialSource == null) {
+ throw new IllegalArgumentException(
+ "An awsSecurityCredentialsSupplier or a credentialSource must be provided.");
+ }
+
+ AwsCredentialSource credentialSource = (AwsCredentialSource) builder.credentialSource;
+ // Set regional credential verification url override if provided.
+ this.regionalCredentialVerificationUrlOverride =
+ builder.regionalCredentialVerificationUrlOverride;
+
+ // Set regional credential verification url depending on inputs.
+ if (this.regionalCredentialVerificationUrlOverride != null) {
+ this.regionalCredentialVerificationUrl = this.regionalCredentialVerificationUrlOverride;
+ } else if (credentialSource != null) {
+ this.regionalCredentialVerificationUrl = credentialSource.regionalCredentialVerificationUrl;
+ } else {
+ this.regionalCredentialVerificationUrl = DEFAULT_REGIONAL_CREDENTIAL_VERIFICATION_URL;
+ }
+
+ // If user has provided a security credential supplier, use that to retrieve the AWS security
+ // credentials.
+ if (builder.awsSecurityCredentialsSupplier != null) {
+ this.awsSecurityCredentialsSupplier = builder.awsSecurityCredentialsSupplier;
+ this.metricsHeaderValue = PROGRAMMATIC_METRICS_HEADER_VALUE;
+ } else {
+ this.awsSecurityCredentialsSupplier =
+ new InternalAwsSecurityCredentialsSupplier(
+ credentialSource, this.getEnvironmentProvider(), this.transportFactory);
+ this.metricsHeaderValue = AWS_METRICS_HEADER_VALUE;
+ }
+ }
+
+ @Override
+ public AccessToken refreshAccessToken() throws IOException {
+ StsTokenExchangeRequest.Builder stsTokenExchangeRequest =
+ StsTokenExchangeRequest.newBuilder(retrieveSubjectToken(), getSubjectTokenType())
+ .setAudience(getAudience());
+
+ // Add scopes, if possible.
+ Collection scopes = getScopes();
+ if (scopes != null && !scopes.isEmpty()) {
+ stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes));
+ }
+
+ return exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest.build());
+ }
+
+ @Override
+ public String retrieveSubjectToken() throws IOException {
+
+ // The targeted region is required to generate the signed request. The regional
+ // endpoint must also be used.
+ String region = awsSecurityCredentialsSupplier.getRegion(supplierContext);
+
+ AwsSecurityCredentials credentials =
+ awsSecurityCredentialsSupplier.getCredentials(supplierContext);
+
+ // Generate the signed request to the AWS STS GetCallerIdentity API.
+ Map headers = new HashMap<>();
+ headers.put("x-goog-cloud-target-resource", getAudience());
+
+ AwsRequestSigner signer =
+ AwsRequestSigner.newBuilder(
+ credentials,
+ "POST",
+ this.regionalCredentialVerificationUrl.replace("{region}", region),
+ region)
+ .setAdditionalHeaders(headers)
+ .build();
+
+ AwsRequestSignature awsRequestSignature = signer.sign();
+ return buildSubjectToken(awsRequestSignature);
+ }
+
+ /** Clones the AwsCredentials with the specified scopes. */
+ @Override
+ public GoogleCredentials createScoped(Collection newScopes) {
+ return new AwsCredentials((AwsCredentials.Builder) newBuilder(this).setScopes(newScopes));
+ }
+
+ @Override
+ String getCredentialSourceType() {
+ return this.metricsHeaderValue;
+ }
+
+ private String buildSubjectToken(AwsRequestSignature signature)
+ throws UnsupportedEncodingException {
+ Map canonicalHeaders = signature.getCanonicalHeaders();
+ List headerList = new ArrayList<>();
+ for (String headerName : canonicalHeaders.keySet()) {
+ headerList.add(formatTokenHeaderForSts(headerName, canonicalHeaders.get(headerName)));
+ }
+
+ headerList.add(formatTokenHeaderForSts("Authorization", signature.getAuthorizationHeader()));
+
+ // The canonical resource name of the workload identity pool provider.
+ headerList.add(formatTokenHeaderForSts("x-goog-cloud-target-resource", getAudience()));
+
+ GenericJson token = new GenericJson();
+ token.setFactory(OAuth2Utils.JSON_FACTORY);
+
+ token.put("headers", headerList);
+ token.put("method", signature.getHttpMethod());
+ token.put(
+ "url", this.regionalCredentialVerificationUrl.replace("{region}", signature.getRegion()));
+ return URLEncoder.encode(token.toString(), "UTF-8");
+ }
+
+ @VisibleForTesting
+ String getRegionalCredentialVerificationUrl() {
+ return this.regionalCredentialVerificationUrl;
+ }
+
+ @VisibleForTesting
+ String getEnv(String name) {
+ return System.getenv(name);
+ }
+
+ @VisibleForTesting
+ AwsSecurityCredentialsSupplier getAwsSecurityCredentialsSupplier() {
+ return this.awsSecurityCredentialsSupplier;
+ }
+
+ @Nullable
+ public String getRegionalCredentialVerificationUrlOverride() {
+ return this.regionalCredentialVerificationUrlOverride;
+ }
+
+ private static GenericJson formatTokenHeaderForSts(String key, String value) {
+ // The GCP STS endpoint expects the headers to be formatted as:
+ // [
+ // {key: 'x-amz-date', value: '...'},
+ // {key: 'Authorization', value: '...'},
+ // ...
+ // ]
+ GenericJson header = new GenericJson();
+ header.setFactory(OAuth2Utils.JSON_FACTORY);
+ header.put("key", key);
+ header.put("value", value);
+ return header;
+ }
+
+ public static AwsCredentials.Builder newBuilder() {
+ return new AwsCredentials.Builder();
+ }
+
+ public static AwsCredentials.Builder newBuilder(AwsCredentials awsCredentials) {
+ return new AwsCredentials.Builder(awsCredentials);
+ }
+
+ @Override
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ public static class Builder extends ExternalAccountCredentials.Builder {
+
+ private AwsSecurityCredentialsSupplier awsSecurityCredentialsSupplier;
+
+ private String regionalCredentialVerificationUrlOverride;
+
+ Builder() {}
+
+ Builder(AwsCredentials credentials) {
+ super(credentials);
+ if (this.credentialSource == null) {
+ this.awsSecurityCredentialsSupplier = credentials.awsSecurityCredentialsSupplier;
+ }
+ this.regionalCredentialVerificationUrlOverride =
+ credentials.regionalCredentialVerificationUrlOverride;
+ }
+
+ /**
+ * Sets the AWS security credentials supplier. The supplier should return a valid {@code
+ * AwsSecurityCredentials} object and a valid AWS region.
+ *
+ * @param awsSecurityCredentialsSupplier the supplier to use.
+ * @return this {@code Builder} object
+ */
+ @CanIgnoreReturnValue
+ public Builder setAwsSecurityCredentialsSupplier(
+ AwsSecurityCredentialsSupplier awsSecurityCredentialsSupplier) {
+ this.awsSecurityCredentialsSupplier = awsSecurityCredentialsSupplier;
+ return this;
+ }
+
+ /**
+ * Sets the AWS regional credential verification URL. If set, will override any credential
+ * verification URL provided in the credential source. If not set, the credential verification
+ * URL will default to
+ * `https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15`
+ *
+ * @param regionalCredentialVerificationUrlOverride the AWS credential verification url to set.
+ * @return this {@code Builder} object
+ */
+ @CanIgnoreReturnValue
+ public Builder setRegionalCredentialVerificationUrlOverride(
+ String regionalCredentialVerificationUrlOverride) {
+ this.regionalCredentialVerificationUrlOverride = regionalCredentialVerificationUrlOverride;
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
+ super.setHttpTransportFactory(transportFactory);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setAudience(String audience) {
+ super.setAudience(audience);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setSubjectTokenType(String subjectTokenType) {
+ super.setSubjectTokenType(subjectTokenType);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setSubjectTokenType(SubjectTokenTypes subjectTokenType) {
+ super.setSubjectTokenType(subjectTokenType);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setTokenUrl(String tokenUrl) {
+ super.setTokenUrl(tokenUrl);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setCredentialSource(AwsCredentialSource credentialSource) {
+ super.setCredentialSource(credentialSource);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setServiceAccountImpersonationUrl(String serviceAccountImpersonationUrl) {
+ super.setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setTokenInfoUrl(String tokenInfoUrl) {
+ super.setTokenInfoUrl(tokenInfoUrl);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setQuotaProjectId(String quotaProjectId) {
+ super.setQuotaProjectId(quotaProjectId);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setClientId(String clientId) {
+ super.setClientId(clientId);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setClientSecret(String clientSecret) {
+ super.setClientSecret(clientSecret);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setScopes(Collection scopes) {
+ super.setScopes(scopes);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) {
+ super.setWorkforcePoolUserProject(workforcePoolUserProject);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setServiceAccountImpersonationOptions(Map optionsMap) {
+ super.setServiceAccountImpersonationOptions(optionsMap);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setUniverseDomain(String universeDomain) {
+ super.setUniverseDomain(universeDomain);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) {
+ super.setEnvironmentProvider(environmentProvider);
+ return this;
+ }
+
+ @Override
+ public AwsCredentials build() {
+ return new AwsCredentials(this);
+ }
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsDates.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsDates.java
new file mode 100644
index 000000000000..abf81add9aad
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsDates.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2021 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;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+/** Formats dates required for AWS Signature V4 request signing. */
+final class AwsDates {
+ private static final String X_AMZ_DATE_FORMAT = "yyyyMMdd'T'HHmmss'Z'";
+ private static final String HTTP_DATE_FORMAT = "E, dd MMM yyyy HH:mm:ss z";
+
+ private final String xAmzDate;
+ private final String originalDate;
+
+ private AwsDates(String amzDate) {
+ this.xAmzDate = checkNotNull(amzDate);
+ this.originalDate = amzDate;
+ }
+
+ private AwsDates(String xAmzDate, String originalDate) {
+ this.xAmzDate = checkNotNull(xAmzDate);
+ this.originalDate = checkNotNull(originalDate);
+ }
+
+ /**
+ * Returns the original date. This can either be the x-amz-date or a specified date in the format
+ * of E, dd MMM yyyy HH:mm:ss z.
+ */
+ String getOriginalDate() {
+ return originalDate;
+ }
+
+ /** Returns the x-amz-date in yyyyMMdd'T'HHmmss'Z' format. */
+ String getXAmzDate() {
+ return xAmzDate;
+ }
+
+ /** Returns the x-amz-date in YYYYMMDD format. */
+ String getFormattedDate() {
+ return xAmzDate.substring(0, 8);
+ }
+
+ static AwsDates fromXAmzDate(String xAmzDate) throws ParseException {
+ // Validate format
+ new SimpleDateFormat(AwsDates.X_AMZ_DATE_FORMAT).parse(xAmzDate);
+ return new AwsDates(xAmzDate);
+ }
+
+ static AwsDates fromDateHeader(String date) throws ParseException {
+ DateFormat dateFormat = new SimpleDateFormat(X_AMZ_DATE_FORMAT);
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+ Date inputDate = new SimpleDateFormat(HTTP_DATE_FORMAT).parse(date);
+ String xAmzDate = dateFormat.format(inputDate);
+ return new AwsDates(xAmzDate, date);
+ }
+
+ static AwsDates generateXAmzDate() {
+ DateFormat dateFormat = new SimpleDateFormat(X_AMZ_DATE_FORMAT);
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ String xAmzDate = dateFormat.format(new Date(System.currentTimeMillis()));
+ return new AwsDates(xAmzDate);
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java
new file mode 100644
index 000000000000..99ec90cf439a
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsRequestSignature.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2021 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.errorprone.annotations.CanIgnoreReturnValue;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Stores the AWS API request signature based on the AWS Signature Version 4 signing process, and
+ * the parameters used in the signing process.
+ */
+class AwsRequestSignature {
+
+ private AwsSecurityCredentials awsSecurityCredentials;
+ private Map canonicalHeaders;
+
+ private String signature;
+ private String credentialScope;
+ private String url;
+ private String httpMethod;
+ private String date;
+ private String region;
+ private String authorizationHeader;
+
+ private AwsRequestSignature(
+ AwsSecurityCredentials awsSecurityCredentials,
+ Map canonicalHeaders,
+ String signature,
+ String credentialScope,
+ String url,
+ String httpMethod,
+ String date,
+ String region,
+ String authorizationHeader) {
+ this.awsSecurityCredentials = awsSecurityCredentials;
+ this.canonicalHeaders = canonicalHeaders;
+ this.signature = signature;
+ this.credentialScope = credentialScope;
+ this.url = url;
+ this.httpMethod = httpMethod;
+ this.date = date;
+ this.region = region;
+ this.authorizationHeader = authorizationHeader;
+ }
+
+ /** Returns the request signature based on the AWS Signature Version 4 signing process. */
+ String getSignature() {
+ return signature;
+ }
+
+ /** Returns the credential scope. e.g. 20150830/us-east-1/iam/aws4_request */
+ String getCredentialScope() {
+ return credentialScope;
+ }
+
+ /** Returns the AWS security credentials. */
+ AwsSecurityCredentials getSecurityCredentials() {
+ return awsSecurityCredentials;
+ }
+
+ /** Returns the request URL. */
+ String getUrl() {
+ return url;
+ }
+
+ /** Returns the HTTP request method. */
+ String getHttpMethod() {
+ return httpMethod;
+ }
+
+ /** Returns the HTTP request canonical headers. */
+ Map getCanonicalHeaders() {
+ return new HashMap<>(canonicalHeaders);
+ }
+
+ /** Returns the request date. */
+ String getDate() {
+ return date;
+ }
+
+ /** Returns the targeted region. */
+ String getRegion() {
+ return region;
+ }
+
+ /** Returns the authorization header. */
+ String getAuthorizationHeader() {
+ return authorizationHeader;
+ }
+
+ static class Builder {
+
+ private AwsSecurityCredentials awsSecurityCredentials;
+ private Map canonicalHeaders;
+
+ private String signature;
+ private String credentialScope;
+ private String url;
+ private String httpMethod;
+ private String date;
+ private String region;
+ private String authorizationHeader;
+
+ @CanIgnoreReturnValue
+ Builder setSignature(String signature) {
+ this.signature = signature;
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setCredentialScope(String credentialScope) {
+ this.credentialScope = credentialScope;
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setSecurityCredentials(AwsSecurityCredentials awsSecurityCredentials) {
+ this.awsSecurityCredentials = awsSecurityCredentials;
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setUrl(String url) {
+ this.url = url;
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setHttpMethod(String httpMethod) {
+ this.httpMethod = httpMethod;
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setCanonicalHeaders(Map canonicalHeaders) {
+ this.canonicalHeaders = new HashMap<>(canonicalHeaders);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setDate(String date) {
+ this.date = date;
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setRegion(String region) {
+ this.region = region;
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setAuthorizationHeader(String authorizationHeader) {
+ this.authorizationHeader = authorizationHeader;
+ return this;
+ }
+
+ AwsRequestSignature build() {
+ return new AwsRequestSignature(
+ awsSecurityCredentials,
+ canonicalHeaders,
+ signature,
+ credentialScope,
+ url,
+ httpMethod,
+ date,
+ region,
+ authorizationHeader);
+ }
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java
new file mode 100644
index 000000000000..275c15105d37
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsRequestSigner.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright 2021 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;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.auth.ServiceAccountSigner.SigningException;
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.io.BaseEncoding;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.net.URI;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import javax.annotation.Nullable;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Internal utility that signs AWS API requests based on the AWS Signature Version 4 signing
+ * process.
+ *
+ * @see AWS
+ * Signature V4
+ */
+class AwsRequestSigner {
+
+ // AWS Signature Version 4 signing algorithm identifier.
+ private static final String HASHING_ALGORITHM = "AWS4-HMAC-SHA256";
+
+ // The termination string for the AWS credential scope value as defined in
+ // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
+ private static final String AWS_REQUEST_TYPE = "aws4_request";
+
+ private final AwsSecurityCredentials awsSecurityCredentials;
+ private final Map additionalHeaders;
+ private final String httpMethod;
+ private final String region;
+ private final String requestPayload;
+ private final URI uri;
+ private final AwsDates dates;
+
+ /**
+ * Internal constructor.
+ *
+ * @param awsSecurityCredentials AWS security credentials
+ * @param httpMethod the HTTP request method
+ * @param url the request URL
+ * @param region the targeted region
+ * @param requestPayload the request payload
+ * @param additionalHeaders a map of additional HTTP headers to be included with the signed
+ * request
+ */
+ private AwsRequestSigner(
+ AwsSecurityCredentials awsSecurityCredentials,
+ String httpMethod,
+ String url,
+ String region,
+ @Nullable String requestPayload,
+ @Nullable Map additionalHeaders,
+ @Nullable AwsDates awsDates) {
+ this.awsSecurityCredentials = checkNotNull(awsSecurityCredentials);
+ this.httpMethod = checkNotNull(httpMethod);
+ this.uri = URI.create(url).normalize();
+ this.region = checkNotNull(region);
+ this.requestPayload = requestPayload == null ? "" : requestPayload;
+ this.additionalHeaders =
+ (additionalHeaders != null)
+ ? new HashMap<>(additionalHeaders)
+ : new HashMap();
+ this.dates = awsDates == null ? AwsDates.generateXAmzDate() : awsDates;
+ }
+
+ /**
+ * Signs the specified AWS API request.
+ *
+ * @return the {@link AwsRequestSignature}
+ */
+ AwsRequestSignature sign() {
+ // Retrieve the service name. For example: iam.amazonaws.com host => iam service.
+ String serviceName = Splitter.on(".").split(uri.getHost()).iterator().next();
+
+ Map canonicalHeaders = getCanonicalHeaders(dates.getOriginalDate());
+ // Headers must be sorted.
+ List sortedHeaderNames = new ArrayList<>();
+ for (String headerName : canonicalHeaders.keySet()) {
+ sortedHeaderNames.add(headerName.toLowerCase(Locale.US));
+ }
+ Collections.sort(sortedHeaderNames);
+
+ String canonicalRequestHash = createCanonicalRequestHash(canonicalHeaders, sortedHeaderNames);
+ String credentialScope =
+ dates.getFormattedDate() + "/" + region + "/" + serviceName + "/" + AWS_REQUEST_TYPE;
+ String stringToSign =
+ createStringToSign(canonicalRequestHash, dates.getXAmzDate(), credentialScope);
+ String signature =
+ calculateAwsV4Signature(
+ serviceName,
+ awsSecurityCredentials.getSecretAccessKey(),
+ dates.getFormattedDate(),
+ region,
+ stringToSign);
+
+ String authorizationHeader =
+ generateAuthorizationHeader(
+ sortedHeaderNames, awsSecurityCredentials.getAccessKeyId(), credentialScope, signature);
+
+ return new AwsRequestSignature.Builder()
+ .setSignature(signature)
+ .setCanonicalHeaders(canonicalHeaders)
+ .setHttpMethod(httpMethod)
+ .setSecurityCredentials(awsSecurityCredentials)
+ .setCredentialScope(credentialScope)
+ .setUrl(uri.toString())
+ .setDate(dates.getOriginalDate())
+ .setRegion(region)
+ .setAuthorizationHeader(authorizationHeader)
+ .build();
+ }
+
+ /** Task 1: Create a canonical request for Signature Version 4. */
+ private String createCanonicalRequestHash(
+ Map headers, List sortedHeaderNames) {
+ // Append the HTTP request method.
+ StringBuilder canonicalRequest = new StringBuilder(httpMethod).append("\n");
+
+ // Append the path.
+ String urlPath = uri.getRawPath().isEmpty() ? "/" : uri.getRawPath();
+ canonicalRequest.append(urlPath).append("\n");
+
+ // Append the canonical query string.
+ String actionQueryString = uri.getRawQuery() != null ? uri.getRawQuery() : "";
+ canonicalRequest.append(actionQueryString).append("\n");
+
+ // Append the canonical headers.
+ StringBuilder canonicalHeaders = new StringBuilder();
+ for (String headerName : sortedHeaderNames) {
+ canonicalHeaders.append(headerName).append(":").append(headers.get(headerName)).append("\n");
+ }
+ canonicalRequest.append(canonicalHeaders).append("\n");
+
+ // Append the signed headers.
+ canonicalRequest.append(Joiner.on(';').join(sortedHeaderNames)).append("\n");
+
+ // Append the hashed request payload.
+ canonicalRequest.append(getHexEncodedSha256Hash(requestPayload.getBytes(UTF_8)));
+
+ // Return the hashed canonical request.
+ return getHexEncodedSha256Hash(canonicalRequest.toString().getBytes(UTF_8));
+ }
+
+ /** Task 2: Create a string to sign for Signature Version 4. */
+ private String createStringToSign(
+ String canonicalRequestHash, String xAmzDate, String credentialScope) {
+ return HASHING_ALGORITHM
+ + "\n"
+ + xAmzDate
+ + "\n"
+ + credentialScope
+ + "\n"
+ + canonicalRequestHash;
+ }
+
+ /**
+ * Task 3: Calculate the signature for AWS Signature Version 4.
+ *
+ * @param date the date used in the hashing process in YYYYMMDD format
+ */
+ private String calculateAwsV4Signature(
+ String serviceName, String secret, String date, String region, String stringToSign) {
+ byte[] kDate = sign(("AWS4" + secret).getBytes(UTF_8), date.getBytes(UTF_8));
+ byte[] kRegion = sign(kDate, region.getBytes(UTF_8));
+ byte[] kService = sign(kRegion, serviceName.getBytes(UTF_8));
+ byte[] kSigning = sign(kService, AWS_REQUEST_TYPE.getBytes(UTF_8));
+ return BaseEncoding.base16().lowerCase().encode(sign(kSigning, stringToSign.getBytes(UTF_8)));
+ }
+
+ /** Task 4: Format the signature to be added to the HTTP request. */
+ private String generateAuthorizationHeader(
+ List sortedHeaderNames,
+ String accessKeyId,
+ String credentialScope,
+ String signature) {
+ return String.format(
+ "%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
+ HASHING_ALGORITHM,
+ accessKeyId,
+ credentialScope,
+ Joiner.on(';').join(sortedHeaderNames),
+ signature);
+ }
+
+ private Map getCanonicalHeaders(String defaultDate) {
+ Map headers = new HashMap<>();
+ headers.put("host", uri.getHost());
+
+ // Only add the date if it hasn't been specified through the "date" header.
+ if (!additionalHeaders.containsKey("date")) {
+ headers.put("x-amz-date", defaultDate);
+ }
+
+ if (awsSecurityCredentials.getSessionToken() != null
+ && !awsSecurityCredentials.getSessionToken().isEmpty()) {
+ headers.put("x-amz-security-token", awsSecurityCredentials.getSessionToken());
+ }
+
+ // Add all additional headers.
+ for (String key : additionalHeaders.keySet()) {
+ // Header keys need to be lowercase.
+ headers.put(key.toLowerCase(Locale.US), additionalHeaders.get(key));
+ }
+ return headers;
+ }
+
+ private static byte[] sign(byte[] key, byte[] value) {
+ try {
+ String algorithm = "HmacSHA256";
+ Mac mac = Mac.getInstance(algorithm);
+ mac.init(new SecretKeySpec(key, algorithm));
+ return mac.doFinal(value);
+ } catch (NoSuchAlgorithmException e) {
+ // Will not occur as HmacSHA256 is supported. We may allow other algorithms in the future.
+ throw new RuntimeException("HmacSHA256 must be supported by the JVM.", e);
+ } catch (InvalidKeyException e) {
+ throw new SigningException("Invalid key used when calculating the AWS V4 Signature", e);
+ }
+ }
+
+ private static String getHexEncodedSha256Hash(byte[] bytes) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ return BaseEncoding.base16().lowerCase().encode(digest.digest(bytes));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("Failed to compute SHA-256 hash.", e);
+ }
+ }
+
+ static Builder newBuilder(
+ AwsSecurityCredentials awsSecurityCredentials, String httpMethod, String url, String region) {
+ return new Builder(awsSecurityCredentials, httpMethod, url, region);
+ }
+
+ static class Builder {
+
+ private final AwsSecurityCredentials awsSecurityCredentials;
+ private final String httpMethod;
+ private final String url;
+ private final String region;
+
+ @Nullable private String requestPayload;
+ @Nullable private Map additionalHeaders;
+ @Nullable private AwsDates dates;
+
+ private Builder(
+ AwsSecurityCredentials awsSecurityCredentials,
+ String httpMethod,
+ String url,
+ String region) {
+ this.awsSecurityCredentials = awsSecurityCredentials;
+ this.httpMethod = httpMethod;
+ this.url = url;
+ this.region = region;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setRequestPayload(String requestPayload) {
+ this.requestPayload = requestPayload;
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setAdditionalHeaders(Map additionalHeaders) {
+ if (additionalHeaders.containsKey("date") && additionalHeaders.containsKey("x-amz-date")) {
+ throw new IllegalArgumentException("One of {date, x-amz-date} can be specified, not both.");
+ }
+ try {
+ if (additionalHeaders.containsKey("date")) {
+ this.dates = AwsDates.fromDateHeader(additionalHeaders.get("date"));
+ }
+ if (additionalHeaders.containsKey("x-amz-date")) {
+ this.dates = AwsDates.fromXAmzDate(additionalHeaders.get("x-amz-date"));
+ }
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("The provided date header value is invalid.", e);
+ }
+
+ this.additionalHeaders = additionalHeaders;
+ return this;
+ }
+
+ AwsRequestSigner build() {
+ return new AwsRequestSigner(
+ awsSecurityCredentials,
+ httpMethod,
+ url,
+ region,
+ requestPayload,
+ additionalHeaders,
+ dates);
+ }
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java
new file mode 100644
index 000000000000..7101dda3e5bf
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentials.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2021 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 javax.annotation.Nullable;
+
+/**
+ * Defines AWS security credentials. These are either retrieved from the AWS security_credentials
+ * endpoint or AWS environment variables.
+ */
+public class AwsSecurityCredentials {
+
+ private final String accessKeyId;
+ private final String secretAccessKey;
+
+ @Nullable private final String sessionToken;
+
+ /**
+ * Constructor for AWSSecurityCredentials.
+ *
+ * @param accessKeyId the AWS access Key Id.
+ * @param secretAccessKey the AWS secret access key.
+ * @param sessionToken the AWS session token. Optional.
+ */
+ public AwsSecurityCredentials(
+ String accessKeyId, String secretAccessKey, @Nullable String sessionToken) {
+ this.accessKeyId = accessKeyId;
+ this.secretAccessKey = secretAccessKey;
+ this.sessionToken = sessionToken;
+ }
+
+ /**
+ * Gets the AWS access key id.
+ *
+ * @return the AWS access key id.
+ */
+ public String getAccessKeyId() {
+ return accessKeyId;
+ }
+
+ /**
+ * Gets the AWS secret access key.
+ *
+ * @return the AWS secret access key.
+ */
+ public String getSecretAccessKey() {
+ return secretAccessKey;
+ }
+
+ /**
+ * Gets the AWS session token.
+ *
+ * @return the AWS session token.
+ */
+ @Nullable
+ public String getSessionToken() {
+ return sessionToken;
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentialsSupplier.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentialsSupplier.java
new file mode 100644
index 000000000000..f7f21992348b
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsSecurityCredentialsSupplier.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2024 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.io.Serializable;
+
+/**
+ * Supplier for retrieving AWS Security credentials for {@link AwsCredentials} to exchange for GCP
+ * access tokens.
+ */
+public interface AwsSecurityCredentialsSupplier extends Serializable {
+
+ /**
+ * Gets the AWS region to use.
+ *
+ * @param context relevant context from the calling credential.
+ * @return the AWS region that should be used for the credential.
+ * @throws IOException
+ */
+ String getRegion(ExternalAccountSupplierContext context) throws IOException;
+
+ /**
+ * Gets AWS security credentials.
+ *
+ * @param context relevant context from the calling credential.
+ * @return valid AWS security credentials that can be exchanged for a GCP access token.
+ * @throws IOException
+ */
+ AwsSecurityCredentials getCredentials(ExternalAccountSupplierContext context) throws IOException;
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplier.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplier.java
new file mode 100644
index 000000000000..49a8ec18d279
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplier.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright 2025 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;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Paths;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Provider for retrieving the subject tokens for {@link IdentityPoolCredentials} by reading an
+ * X.509 certificate from the filesystem. The certificate file (e.g., PEM or DER encoded) is read,
+ * the leaf certificate is base64-encoded (DER format), wrapped in a JSON array, and used as the
+ * subject token for STS exchange.
+ */
+public class CertificateIdentityPoolSubjectTokenSupplier
+ implements IdentityPoolSubjectTokenSupplier {
+
+ private final IdentityPoolCredentialSource credentialSource;
+
+ private static final Pattern PEM_CERT_PATTERN =
+ Pattern.compile("-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----", Pattern.DOTALL);
+
+ CertificateIdentityPoolSubjectTokenSupplier(IdentityPoolCredentialSource credentialSource) {
+ this.credentialSource = checkNotNull(credentialSource, "credentialSource cannot be null");
+ // This check ensures that the credential source was intended for certificate usage.
+ // IdentityPoolCredentials logic should guarantee credentialLocation is set in this case.
+ checkNotNull(
+ credentialSource.getCertificateConfig(),
+ "credentialSource.certificateConfig cannot be null when creating"
+ + " CertificateIdentityPoolSubjectTokenSupplier");
+ }
+
+ private static String loadAndEncodeLeafCertificate(String path) throws IOException {
+ try {
+ byte[] leafCertBytes = Files.readAllBytes(Paths.get(path));
+ X509Certificate leafCert = parseCertificate(leafCertBytes);
+ return encodeCert(leafCert);
+ } catch (NoSuchFileException e) {
+ throw new IOException(String.format("Leaf certificate file not found: %s", path), e);
+ } catch (CertificateException e) {
+ throw new IOException(
+ String.format("Failed to parse leaf certificate from file: %s", path), e);
+ } catch (IOException e) {
+ // This catches any other general I/O errors during leaf certificate file reading (e.g.,
+ // permissions).
+ throw new IOException(String.format("Failed to read leaf certificate file: %s", path), e);
+ }
+ }
+
+ @VisibleForTesting
+ static X509Certificate parseCertificate(byte[] certData) throws CertificateException {
+ if (certData == null || certData.length == 0) {
+ throw new IllegalArgumentException(
+ "Invalid certificate data: Certificate file is empty or null.");
+ }
+
+ try {
+ CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+ InputStream certificateStream = new ByteArrayInputStream(certData);
+ return (X509Certificate) certificateFactory.generateCertificate(certificateStream);
+ } catch (CertificateException e) {
+ // Catch the original exception to add context about the operation being performed.
+ // This helps pinpoint the failure point during debugging.
+ throw new CertificateException("Failed to parse X.509 certificate data.", e);
+ }
+ }
+
+ private static String encodeCert(X509Certificate certificate)
+ throws CertificateEncodingException {
+ return Base64.getEncoder().encodeToString(certificate.getEncoded());
+ }
+
+ /**
+ * Retrieves the X509 subject token. This method loads the leaf certificate specified by the
+ * {@code credentialSource.credentialLocation}. If a trust chain path is configured in the {@code
+ * credentialSource.certificateConfig}, it also loads and includes the trust chain certificates.
+ * The subject token is constructed as a JSON array containing the base64-encoded (DER format)
+ * leaf certificate, followed by the base64-encoded (DER format) certificates in the trust chain.
+ * This JSON array serves as the subject token for mTLS authentication.
+ *
+ * @param context The external account supplier context. This parameter is currently not used in
+ * this implementation.
+ * @return The JSON string representation of the base64-encoded certificate chain (leaf
+ * certificate followed by the trust chain, if present).
+ * @throws IOException If an I/O error occurs while reading the certificate file(s).
+ */
+ @Override
+ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOException {
+ String leafCertPath = credentialSource.getCredentialLocation();
+ String trustChainPath = null;
+ if (credentialSource.getCertificateConfig() != null) {
+ trustChainPath = credentialSource.getCertificateConfig().getTrustChainPath();
+ }
+
+ // Load and encode the leaf certificate.
+ String encodedLeafCert = loadAndEncodeLeafCertificate(leafCertPath);
+
+ // Initialize the certificate chain for the subject token. The Security Token Service (STS)
+ // requires that the leaf certificate (the one used for authenticating this workload) must be
+ // the first certificate in this chain.
+ List certChain = new ArrayList<>();
+ certChain.add(encodedLeafCert);
+
+ // Handle trust chain loading and processing.
+ try {
+ // Read the trust chain.
+ List trustChainCerts = readTrustChain(trustChainPath);
+
+ // Process the trust chain certificates read from the file.
+ if (!trustChainCerts.isEmpty()) {
+ populateCertChainFromTrustChain(certChain, trustChainCerts, encodedLeafCert);
+ }
+ } catch (IllegalArgumentException e) {
+ // This catches the specific error for misconfigured trust chain (e.g., leaf in wrong place).
+ throw new IOException("Trust chain misconfiguration: " + e.getMessage(), e);
+ } catch (NoSuchFileException e) {
+ throw new IOException(String.format("Trust chain file not found: %s", trustChainPath), e);
+ } catch (CertificateException e) {
+ throw new IOException(
+ String.format("Failed to parse certificate(s) from trust chain file: %s", trustChainPath),
+ e);
+ } catch (IOException e) {
+ // This catches any other general I/O errors during trust chain file reading (e.g.,
+ // permissions).
+ throw new IOException(
+ String.format("Failed to read trust chain file: %s", trustChainPath), e);
+ }
+
+ return OAuth2Utils.JSON_FACTORY.toString(certChain);
+ }
+
+ /**
+ * Extends {@code certChainToPopulate} with encoded certificates from {@code trustChainCerts},
+ * applying validation rules for the leaf certificate's presence and order within the trust chain.
+ *
+ * @param certChainToPopulate The list of encoded certificate strings to populate.
+ * @param trustChainCerts The list of X509Certificates from the trust chain file (non-empty).
+ * @param encodedLeafCert The Base64-encoded leaf certificate.
+ * @throws CertificateEncodingException If an error occurs during certificate encoding.
+ * @throws IllegalArgumentException If the leaf certificate is found in an invalid position in the
+ * trust chain.
+ */
+ private void populateCertChainFromTrustChain(
+ List certChainToPopulate,
+ List trustChainCerts,
+ String encodedLeafCert)
+ throws CertificateEncodingException, IllegalArgumentException {
+
+ // Get the first certificate from the user-provided trust chain file.
+ X509Certificate firstTrustCert = trustChainCerts.get(0);
+ String encodedFirstTrustCert = encodeCert(firstTrustCert);
+
+ // If the first certificate in the user-provided trust chain file is *not* the leaf
+ // certificate (which has already been added as the first element to `certChainToPopulate`),
+ // then add this certificate. This handles cases where the user's trust chain file
+ // starts with an intermediate certificate. If the first certificate in the trust chain file
+ // *is* the leaf certificate, this means the user has explicitly included the leaf in their
+ // trust chain file. In this case, we skip adding it again to prevent duplication, as the
+ // leaf is already at the beginning of `certChainToPopulate`.
+ if (!encodedFirstTrustCert.equals(encodedLeafCert)) {
+ certChainToPopulate.add(encodedFirstTrustCert);
+ }
+
+ // Iterate over the remaining certificates in the trust chain.
+ for (int i = 1; i < trustChainCerts.size(); i++) {
+ X509Certificate currentCert = trustChainCerts.get(i);
+ String encodedCurrentCert = encodeCert(currentCert);
+
+ // Throw an error if the current certificate (from the user-provided trust chain file,
+ // at an index beyond the first) is the same as the leaf certificate.
+ // This enforces that if the leaf certificate is included in the trust chain file by the
+ // user, it must be the very first certificate in that file. It should not appear
+ // elsewhere in the chain.
+ if (encodedCurrentCert.equals(encodedLeafCert)) {
+ throw new IllegalArgumentException(
+ "The leaf certificate should only appear at the beginning of the trust chain file, or be omitted entirely.");
+ }
+
+ // Add the current certificate to the chain.
+ certChainToPopulate.add(encodedCurrentCert);
+ }
+ }
+
+ /**
+ * Reads a file containing PEM-encoded X509 certificates and returns a list of parsed
+ * certificates. It splits the file content based on PEM headers and parses each certificate.
+ * Returns an empty list if the trust chain path is empty.
+ *
+ * @param trustChainPath The path to the trust chain file.
+ * @return A list of parsed X509 certificates.
+ * @throws IOException If an error occurs while reading the file.
+ * @throws CertificateException If an error occurs while parsing a certificate.
+ */
+ @VisibleForTesting
+ static List readTrustChain(String trustChainPath)
+ throws IOException, CertificateException {
+ List certificateTrustChain = new ArrayList<>();
+
+ // If no trust chain path is provided, return an empty list.
+ if (Strings.isNullOrEmpty(trustChainPath)) {
+ return certificateTrustChain;
+ }
+
+ // initialize certificate factory to retrieve x509 certificates.
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+
+ // Read the trust chain file.
+ byte[] trustChainData;
+ trustChainData = Files.readAllBytes(Paths.get(trustChainPath));
+
+ // Split the file content into PEM certificate blocks.
+ String content = new String(trustChainData, StandardCharsets.UTF_8);
+
+ Matcher matcher = PEM_CERT_PATTERN.matcher(content);
+
+ while (matcher.find()) {
+ String pemCertBlock = matcher.group(0);
+ try (InputStream certStream =
+ new ByteArrayInputStream(pemCertBlock.getBytes(StandardCharsets.UTF_8))) {
+ // Parse the certificate data.
+ Certificate cert = cf.generateCertificate(certStream);
+
+ // Append the certificate to the trust chain.
+ if (cert instanceof X509Certificate) {
+ certificateTrustChain.add((X509Certificate) cert);
+ } else {
+ throw new CertificateException(
+ "Found non-X.509 certificate in trust chain file: " + trustChainPath);
+ }
+ } catch (CertificateException e) {
+ // If parsing an individual PEM block fails, re-throw with more context.
+ throw new CertificateException(
+ "Error loading PEM certificates from the trust chain file: "
+ + trustChainPath
+ + " - "
+ + e.getMessage(),
+ e);
+ }
+ }
+
+ if (trustChainData.length > 0 && certificateTrustChain.isEmpty()) {
+ throw new CertificateException(
+ "Trust chain file was not empty but no PEM certificates were found: " + trustChainPath);
+ }
+
+ return certificateTrustChain;
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ClientId.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ClientId.java
new file mode 100644
index 000000000000..4f1de34c3e5e
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ClientId.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2015, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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.JsonObjectParser;
+import com.google.api.client.util.Preconditions;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+/**
+ * An OAuth2 user authorization Client ID and associated information.
+ *
+ *
Corresponds to the information in the json file downloadable for a Client ID.
+ */
+public class ClientId {
+
+ private static final String FIELD_TYPE_INSTALLED = "installed";
+ private static final String FIELD_TYPE_WEB = "web";
+ private static final String FIELD_CLIENT_ID = "client_id";
+ private static final String FIELD_CLIENT_SECRET = "client_secret";
+ private static final String JSON_PARSE_ERROR = "Error parsing Client ID JSON: ";
+
+ private final String clientId;
+ private final String clientSecret;
+
+ /**
+ * Constructs a client ID from an explicit ID and secret.
+ *
+ *
Note: Direct use of this factory method in application code is not recommended to avoid
+ * having secrets or values that need to be updated in source code.
+ *
+ * @param clientId Text identifier of the Client ID.
+ * @param clientSecret Secret to associated with the Client ID.
+ * @return The ClientId instance.
+ */
+ public static ClientId of(String clientId, String clientSecret) {
+ return new ClientId(clientId, clientSecret);
+ }
+
+ /**
+ * Constructs a Client ID from JSON from a downloaded file.
+ *
+ * @param json the JSON from the downloaded file
+ * @return the ClientId instance based on the JSON
+ * @throws IOException the JSON could not be parsed
+ */
+ public static ClientId fromJson(Map json) throws IOException {
+ Object rawDetail = null;
+ rawDetail = json.get(FIELD_TYPE_INSTALLED);
+ if (rawDetail == null) {
+ rawDetail = json.get(FIELD_TYPE_WEB);
+ }
+ if (rawDetail == null || !(rawDetail instanceof Map, ?>)) {
+ throw new IOException(
+ "Unable to parse Client ID JSON. Expecting top-level field '"
+ + FIELD_TYPE_WEB
+ + "' or '"
+ + FIELD_TYPE_INSTALLED
+ + "' of collection type");
+ }
+ @SuppressWarnings("unchecked")
+ Map detail = (Map) rawDetail;
+ String clientId = OAuth2Utils.validateString(detail, FIELD_CLIENT_ID, JSON_PARSE_ERROR);
+ if (clientId == null || clientId.length() == 0) {
+ throw new IOException(
+ "Unable to parse ClientId. Field '" + FIELD_CLIENT_ID + "' is required.");
+ }
+ String clientSecret =
+ OAuth2Utils.validateOptionalString(detail, FIELD_CLIENT_SECRET, JSON_PARSE_ERROR);
+ return new ClientId(clientId, clientSecret);
+ }
+
+ /**
+ * Constructs a Client ID from JSON file stored as a resource.
+ *
+ * @param relativeClass a class in the same namespace as the resource
+ * @param resourceName the name of the resource
+ * @return the constructed ClientID instance based on the JSON in the resource
+ * @throws IOException The JSON could not be loaded or parsed.
+ */
+ public static ClientId fromResource(Class> relativeClass, String resourceName)
+ throws IOException {
+ InputStream stream = relativeClass.getResourceAsStream(resourceName);
+ return fromStream(stream);
+ }
+
+ /**
+ * Constructs a Client ID from JSON file stream.
+ *
+ * @param stream the downloaded JSON file
+ * @return the constructed ClientID instance based on the JSON in the stream
+ * @throws IOException the JSON could not be read or parsed
+ */
+ public static ClientId fromStream(InputStream stream) throws IOException {
+ Preconditions.checkNotNull(stream);
+ JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
+ GenericJson parsedJson =
+ parser.parseAndClose(stream, StandardCharsets.UTF_8, GenericJson.class);
+ return fromJson(parsedJson);
+ }
+
+ /**
+ * Constructs a client ID using an explicit ID and secret
+ *
+ *
Note: Direct use of this constructor in application code is not recommended to avoid having
+ * secrets or values that need to be updated in source code.
+ *
+ * @param clientId Text identifier of the Client ID.
+ * @param clientSecret Secret to associated with the Client ID.
+ */
+ private ClientId(String clientId, String clientSecret) {
+ this.clientId = Preconditions.checkNotNull(clientId);
+ this.clientSecret = clientSecret;
+ }
+
+ /**
+ * Returns the text identifier of the Client ID.
+ *
+ * @return The text identifier of the Client ID.
+ */
+ public final String getClientId() {
+ return clientId;
+ }
+
+ /**
+ * Returns the secret associated with the Client ID.
+ *
+ * @return The secret associated with the Client ID.
+ */
+ public final String getClientSecret() {
+ return clientSecret;
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ public static class Builder {
+
+ private String clientId;
+
+ private String clientSecret;
+
+ protected Builder() {}
+
+ protected Builder(ClientId clientId) {
+ this.clientId = clientId.getClientId();
+ this.clientSecret = clientId.getClientSecret();
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setClientId(String clientId) {
+ this.clientId = clientId;
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public Builder setClientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
+ return this;
+ }
+
+ public String getClientSecret() {
+ return clientSecret;
+ }
+
+ public ClientId build() {
+ return new ClientId(clientId, clientSecret);
+ }
+ }
+}
diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/CloudShellCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/CloudShellCredentials.java
new file mode 100644
index 000000000000..7b1be7d9115f
--- /dev/null
+++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/CloudShellCredentials.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2015, Google Inc. All rights reserved.
+ *
+ * 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 Inc. 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.JsonParser;
+import com.google.common.base.MoreObjects;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/** OAuth2 credentials representing the built-in service account for Google Cloud Shell. */
+public class CloudShellCredentials extends GoogleCredentials {
+
+ private static final long serialVersionUID = -2133257318957488451L;
+ private static final int ACCESS_TOKEN_INDEX = 2;
+ private static final int READ_TIMEOUT_MS = 5000;
+
+ /**
+ * The Cloud Shell back authorization channel uses serialized Javascript Protobuffers, preceded by
+ * the message length and a new line character. However, the request message has no content, so a
+ * token request consists of an empty JsPb, and its 2 character length prefix.
+ */
+ protected static final String GET_AUTH_TOKEN_REQUEST = "2\n[]";
+
+ protected static final byte[] GET_AUTH_TOKEN_REQUEST_BYTES =
+ (GET_AUTH_TOKEN_REQUEST + "\n").getBytes(StandardCharsets.UTF_8);
+
+ private final int authPort;
+
+ public static CloudShellCredentials create(int authPort) {
+ return CloudShellCredentials.newBuilder().setAuthPort(authPort).build();
+ }
+
+ private CloudShellCredentials(Builder builder) {
+ super(builder);
+ this.authPort = builder.getAuthPort();
+ this.name = GoogleCredentialsInfo.CLOUD_SHELL_CREDENTIALS.getCredentialName();
+ }
+
+ protected int getAuthPort() {
+ return this.authPort;
+ }
+
+ @Override
+ public AccessToken refreshAccessToken() throws IOException {
+ Socket socket = new Socket("localhost", this.getAuthPort());
+ socket.setSoTimeout(READ_TIMEOUT_MS);
+ AccessToken token;
+ try {
+ OutputStream os = socket.getOutputStream();
+ os.write(GET_AUTH_TOKEN_REQUEST_BYTES);
+
+ BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ input.readLine(); // Skip over the first line
+ JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(input);
+ List