Skip to content

feat(oauth2): support certificate-bound OAuth2 tokens and mTLS rotation for Workload Identity Federation - #14781

Merged
macastelaz merged 14 commits into
mainfrom
oauth2-bound-tokens
Oct 30, 2026
Merged

macastelaz merged 14 commits into
mainfrom
oauth2-bound-tokens

Conversation

@macastelaz

@macastelaz macastelaz commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Merges the oauth2-bound-tokens feature branch into main, adding end-to-end support for certificate-bound OAuth 2.0 tokens and automatic mTLS certificate rotation in Workload Identity Federation (IdentityPoolCredentials and ImpersonatedCredentials).

Included Feature Branch PRs

Key Capabilities

  • Actor Token Support for Hybrid Certificate-Bound WIF (IdentityPoolActorTokenSupplier, FileIdentityPoolSubjectTokenSupplier):

    • Supports extracting both a primary X.509 leaf certificate subject token and a secondary file-sourced actor_token (text or JSON via actor_token_field_name and actor_token_type) for STS token exchange.
    • Caches file-sourced tokens using file metadata (mtime, size, file key) and JWT exp claims with a 5-minute expiration margin to avoid redundant disk reads.
  • Per-Cycle mTLS Transport Pinning & Certificate Rotation (IdentityPoolCredentials, ImpersonatedCredentials, MtlsHttpTransportFactory):

    • Snapshots the KeyStore once per refresh cycle so leaf certificate extraction, the STS token exchange, and downstream IAM Service Account impersonation (generateAccessToken / generateIdToken) share the exact same certificate and private key snapshot.
    • Automatically recovers from 401 Unauthorized, OAuth invalid_grant, and transient TLS/KeyStore load failures during non-atomic split cert/key writes by reloading the KeyStore once and retrying when a certificate change is detected (OAuth2Utils.hasCertificateChanged).
    • Supports standalone ImpersonatedCredentials configured with MtlsProvider as well as delegated ImpersonatedCredentials chains.
  • Test Coverage:

    • Comprehensive unit tests across IdentityPoolCredentialsTest, ImpersonatedCredentialsTest, FileIdentityPoolSubjectTokenSupplierTest, MtlsHttpTransportFactoryTest, OAuth2UtilsTest, and OAuthExceptionTest.
    • In-process mTLS socket tests (MtlsPipelineLocalTest) and live GCP Workload Identity Federation integration tests (ITWorkloadIdentityFederationTest).

    Manual & Live Verification

End-to-end live GCP and real-TLS socket fault-injection testing was executed against oauth2-bound-tokens (65d592bd783):

  • Full verification report, logs, and test harness: https://paste.googleplex.com/6755399436525184
  • Scenarios verified (15 / 15 passed):
    1. Live Direct mTLS STS Exchange (sts.mtls.googleapis.com): File-based JSON subject_token + actor_token with RFC 8705 cnf.x5t#S256 certificate binding and atomic single-supplier file reads.
    2. Serialization Round-Trip (readObject) + createScoped(): Deserialization of IdentityPoolCredentials with mTLS state reconstruction across multiple live STS refresh cycles.
    3. Full 3-Hop Live mTLS Chain (STS mTLS -> IAM Impersonation mTLS -> GCS mTLS): Cert-bound workload + actor token exchange at sts.mtls.googleapis.com, service account impersonation at iamcredentials.mtls.googleapis.com, and bucket access at storage.mtls.googleapis.com (HTTP 200).
    4. Programmatic Builder (Suppliers + MtlsHttpTransportFactory) Live 3-Hop mTLS: End-to-end STS mTLS -> IAM mTLS -> GCS mTLS (HTTP 200) and verification of OAuth2Utils mTLS helpers (hasMtlsKeyStore, isKeylessMtlsTransportFactory, isDefaultOrMtlsTransportFactory).
    5. Non-mTLS Unbound Actor Token over Public Endpoints: Unbound subject_token + actor_token exchange over public sts.googleapis.com -> iamcredentials.googleapis.com -> storage.googleapis.com (HTTP 200).
    6. X.509 Certificate Workload Identity Federation: Leaf certificate extraction and trust_chain handshake against sts.mtls.googleapis.com via CertificateIdentityPoolSubjectTokenSupplier.
    7. PluggableAuthCredentials Impersonation Transport Reconciliation: Rebuilding PluggableAuthCredentials with a custom HttpTransportFactory and verifying both STS and ImpersonatedCredentials use the reconciled transport against live STS, IAM, and GCS (HTTP 200).
    8. Review Feedback Edge Cases: FileIdentityPoolSubjectTokenSupplier TEXT format subject/actor token handling and OAuthException.createFromJsonErrorResponse parsing across live STS 400 errors, nested google.rpc.Status JSON, and empty-body HTTP error fallbacks.
    9. Real-TLS Socket Fault Injection: Mid-cycle cert rotation pinning between STS and IAM, 401 Unauthorized IAM recovery (both fresh and cached STS token eviction), 400 invalid_grant STS cert rotation recovery, and 8-thread concurrent mTLS refresh.

macastelaz and others added 10 commits September 16, 2026 20:54
…#13955)

## Summary
Adds support for actor tokens in `IdentityPoolCredentials` during
certificate-bound OAuth2 STS token exchange, and implements per-cycle
mTLS certificate snapshotting and single-retry rotation.
---
## Architectural Changes & Review Feedback Resolutions
### 1. Actor Token Extraction & Atomic Reads
- Added package-private `IdentityPoolActorTokenSupplier` interface.
- Enhanced `FileIdentityPoolSubjectTokenSupplier` with `readTokens()` to
read and parse JSON credential files in a single atomic I/O operation,
returning a `TokenPair` for subject and actor tokens.
- Removed unreliable file timestamp caching; the file is read at most
once per refresh cycle.
- Added early validation in `IdentityPoolCredentialSource`:
`actorTokenFieldName` must be non-empty, distinct from
`subjectTokenFieldName`, and is supported only on JSON file sources.
### 2. Per-Cycle Certificate Pinning & 401 Rotation Retry
- `refreshAccessToken()` snapshots the `KeyStore` from `X509Provider` at
the start of each refresh cycle, creating a cycle-isolated
`MtlsHttpTransportFactory` passed to the STS exchange.
- On 401 Unauthorized from STS, `refreshAccessToken()` re-reads a fresh
`KeyStore` from `x509Provider` and retries the exchange once (preserving
original error context via `addSuppressed` if cert reload fails).
- Relaxed mTLS endpoint validation to check for `.mtls.` to support
custom universes and PSC endpoints.
### 3. Serialization & Binary Backward Compatibility
- **SUID Compatibility**: Preserved
`FileIdentityPoolSubjectTokenSupplier` class name and declared static
`serialVersionUID = 7152208690659890358L;`, matching the synthetic SUID
generated before this PR to prevent `InvalidClassException` on existing
serialized credentials.
- **Binary ABI Preservation**: Kept `public NetHttpTransport create()`
return type on `MtlsHttpTransportFactory` to preserve binary
compatibility for compiled callers via covariant return typing.
- **Post-Deserialization Usability**: Implemented custom `readObject()`
in `IdentityPoolCredentials` to reconstruct transient `x509Provider` and
`MtlsHttpTransportFactory` from the serialized `credentialSource`,
ensuring `refreshAccessToken()` and `createScoped()` remain fully
functional after deserialization.
- **KeyStore Validation**: `MtlsHttpTransportFactory.hasKeyStore()`
checks `mtlsKeyStore != null && mtlsKeyStore.size() > 0`, ensuring
uninitialized or empty KeyStores are not treated as mTLS-configured.
### 4. API Visibility
- Made `setActorTokenSupplier`, `setActorTokenType`, and
`setX509Provider` package-private on `IdentityPoolCredentials.Builder`
for internal/testing use.
---
## Test Coverage
- **1009 unit tests passing** (`oauth2_http` module).
- **Production Path Tests**: End-to-end tests parsing JSON configs via
`ExternalAccountCredentials.fromStream()` and executing
`refreshAccessToken()`.
- **Serialization Tests**: Verified backward compatibility against
pre-PR serialized byte fixtures and verified token refresh on
deserialized instances.
- **Cert Pinning & Retry Tests**: Verified single-snapshot cert
consistency and 401 cert rotation retries.
- **Google Java Format**: Verified 100% compliance with
`com.spotify.fmt:fmt-maven-plugin:2.25`.

### Manual Testing

- Verified end-to-end execution against live Google STS endpoints, token
exchange with actor tokens over mTLS, scoping, serialization round-trip
with `readObject()` restoration, and multi-cycle refreshes.
- Sample Test App: https://paste.googleplex.com/6209850244726784
- Sanitized Verification Output:
https://paste.googleplex.com/6325994750345216

## Next Steps                                                     
                                                                     
  The next phase (which will follow in a separate PR to keep reviews 
  scoped) will introduce dynamic mTLS transport rotation natively in 
  GAX and a 401 connection-draining interceptor required for         
  downstream service retry logic. 
  
Based on design:
https://docs.google.com/document/d/1NIKeJX86ETNAjoQA-lZIE_G_8mHspisBaL8gwNCO3dA/edit?resourcekey=0-z7teBZZk0WFIJEHL2ODZxw&tab=t.0

See b/542238030 for tracking
Removes a duplicate `google-cloud-storage` dependency declaration in
`java-bigquery/google-cloud-bigquery/pom.xml`. The duplicate entry
caused Maven project model validation failures during CI builds.

The other declaration is in line 34 - 37.
…lters for flatten-maven-plugin (#14598)

## Summary

Fixes `java.lang.VerifyError` in downstream tests (such as
`java-bigquery-jdbc`) on GitHub Actions runners upgraded to **Apache
Maven `3.10.0`**.

## Cause

Maven `3.10.0` (Maven Resolver `2.0.24`) enables
`aether.remoteRepositoryFilter.prefixes` and
`aether.remoteRepositoryFilter.groupId` by default. Because
`flatten-maven-plugin:1.3.0` reads dependency POMs with `repositories =
null` (`[]`), the active remote repository filter rejects cached POMs in
`~/.m2/repository`, causing `flatten-maven-plugin` to drop
`<exclusions>` (such as `protobuf-javalite` on
`io.grpc:grpc-protobuf-lite`) when generating `.flattened-pom.xml`.

## Fix

Disable the default remote repository filters in `.mvn/maven.config`:
```text
-Daether.remoteRepositoryFilter.prefixes=false
-Daether.remoteRepositoryFilter.groupId=false
```
…patibility (#14609)

Support for '#' comments in .mvn/maven.config was introduced in Apache
Maven 3.9.0 (MNG-7682). Prior versions of Maven (such as 3.8.x) and IDE
Maven parsers split the entire file on whitespace and reject '#' comment
tokens with: Unable to parse maven.config: Unrecognized maven.config
entries: [#, TODO:, ...]

Remove the comment line from .mvn/maven.config so that it parses cleanly
across both Maven 3.8.x and 3.9+.
…401 recovery (#14212)

### 🥞 Stacked PRs
- 👉 **This PR** (*feat: implement IAM impersonation mTLS transport
pinning and 401 recovery*)
- ⏳ Stacked PR #14220 (*test: add mTLS in-process socket tests and live
GCP WIF integration tests*)

---
## Summary
Propagates the cycle-pinned mTLS transport factory into
`ImpersonatedCredentials` during Workload Identity Federation with
Service Account Impersonation, ensuring the IAM `generateAccessToken`
call uses the exact same pinned client certificate as the STS token
exchange.
Also extends 401 recovery in `IdentityPoolCredentials` to catch
unauthorized responses across both STS and IAM calls, reloading fresh
certificates and re-executing the full refresh cycle.
## Test Coverage
- **1,030 unit tests passing** across `oauth2_http` (including transport
pinning lifecycle, multi-cycle rotation, 401 retry on IAM, and
suppressed error handling).
- **Google Java Format**: 100% compliant
(`com.spotify.fmt:fmt-maven-plugin:2.25`).

### Manual Testing
Sample test "app": https://paste.googleplex.com/5495919190081536
Test results: https://paste.googleplex.com/5757145073713152
See b/542238030 for tracking.
…ration tests (#14220)

## Description
This PR adds comprehensive integration tests for Certificate-Bound OAuth
2.0 (mTLS), Actor Tokens, and Service Account Impersonation transport
pinning across both hermetic in-process socket tests and live GCP
Workload Identity Federation suites.

### Summary of Changes:
1. **Hermetic In-Process Socket Suite (`MtlsPipelineLocalTest.java`)**:
- Uses JDK `HttpsServer` on `localhost` requiring client authentication
(`setNeedClientAuth(true)`).
- `testMtlsPipeline_verifiesPeerCertAndPayload`: Verifies server
extracts client peer certificate from `SSLSession` and parses token
exchange body (`grant_type`, `subject_token`, `actor_token`,
`actor_token_type`).
- `testMtlsPipeline_withImpersonation_usesSameCertForStsAndIam`:
Verifies both STS and IAM calls execute on the exact same pinned mTLS
transport and verifies the intermediate `Authorization: Bearer <token>`
header.
- `testMtlsPipeline_withImpersonation_401OnIam_retriesWithFreshCert`:
Verifies full-cycle recovery when IAM returns 401 (reloads fresh
KeyStore from disk, re-exchanges at STS with new cert, and calls IAM
with new token + cert).
- `testMtlsPipeline_concurrentRefreshes`: Multi-threaded refresh
verifying thread-isolated transport snapshots.
- `testMtlsPipeline_atomicTokenRead`: Verifies single-pass disk I/O for
subject + actor tokens.
2. **Live GCP Suite Extensions
(`ITWorkloadIdentityFederationTest.java`)**:
- `identityPoolCredentials_withCertificateBoundWorkloadAndActorToken`:
E2E verification of JSON config with certificate config and actor
tokens.
- `identityPoolCredentials_withProgrammaticMtlsAndActorToken`: E2E
verification of programmatic builder configuration.
3. **`OAuthException.java`**:
   - Safely handles null HTTP error response content without NPE.

### Verification:
- **Unit & Socket Tests**: 1,040 / 1,040 tests passing (including
hermetic socket tests).
- **Google Java Format**: 100% compliant (`mvn
com.spotify.fmt:fmt-maven-plugin:2.25:check`).
Source-Link: b/542238030
… IdentityPoolCredentials (#14430)

## Summary

Allows `actor_token` and `actor_token_type` to be used in
`IdentityPoolCredentials` with standard (non-mTLS) STS and IAM
impersonation endpoints and without requiring client certificate
(`certificate_config`) configuration.

## Context & Rationale
In #13955, client-side guardrails (`isMtlsConfigured()` and
`validateMtlsEndpoint()`) were enforced in the `IdentityPoolCredentials`
constructor because Google STS initially required actor tokens to be
paired with certificate-bound tokens over mTLS. Per the original design
discussion, this was intentionally designed as a one-way door that could
be loosened in a non-breaking manner once backend support for non-mTLS
actor token exchanges was ready.

## Changes
1. **Removed Constructor mTLS Restrictions:**
- Removed `isMtlsConfigured()` check in
`IdentityPoolCredentials(Builder)` so actor tokens can be configured
with standard `HttpTransportFactory` instances and without a
`certificate` block.
- Removed `validateMtlsEndpoint()` checks on `tokenUrl` and
`serviceAccountImpersonationUrl`, allowing standard public endpoints
(e.g., `https://sts.googleapis.com/v1/token`) to be used with actor
tokens.
- Preserved strict pairing validation between `actorTokenSupplier` and
`actorTokenType`, as well as JSON format checks for file-based actor
token extraction.
2. **401 Handling & Cert Rotation:**
- When `x509Provider == null` (non-mTLS credentials),
`refreshAccessToken()` uses standard transport without snapshotting a
`KeyStore`, and `401 Unauthorized` errors propagate immediately without
retry.
- When `x509Provider != null && transportFactory instanceof
MtlsHttpTransportFactory` (mTLS credentials), per-cycle certificate
pinning and single-retry cert reload on `401` remain unchanged.
3. **Javadoc & Test Coverage:**
- Updated class-level and builder Javadocs on `IdentityPoolCredentials`.
- Updated negative constructor tests to positive verification tests
(`*_succeeds`).
- Added
`refreshAccessToken_401WithActorTokenAndNonMtlsTransport_bubblesUpWithoutRetry`
and
`fromStream_fileCredentialSource_withoutCertificateConfig_andActorToken_withNonMtlsUrl_refreshesSuccessfully`
to verify end-to-end non-mTLS actor token exchanges.

## Verification
- All **1,035** unit tests passing in `oauth2_http` module (including
all **88** tests in `IdentityPoolCredentialsTest`).
- 100% compliant with `fmt-maven-plugin:2.25:check`.

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…ult (#14554)

### Summary

Enables the 4 mTLS + `actor_token` Workload Identity Federation
integration tests in `ITWorkloadIdentityFederationTest` to run by
default in CI without requiring `GOOGLE_API_CERTIFICATE_CONFIG` or
external Secret Manager secrets:

1. **Default mTLS Certificate Config Fallback:** Falls back to the
checked-in `testresources/mtls/certificate_config.json` (`test_cert.pem`
/ `test_key.pem`) when `GOOGLE_API_CERTIFICATE_CONFIG` is unset,
removing the `assumeTrue` skip.
2. **Signed OIDC JWTs with `cnf.x5t#S256` and `may_act`:** Targets
`oidc-jwt-1` (`OIDC_JWT_AUDIENCE`) and uses the
`GOOGLE_APPLICATION_CREDENTIALS` service account private key (whose
public key is registered in `oidc-jwt-1`'s `jwksJson`) to sign OIDC JWTs
containing RFC 8705 certificate thumbprint binding (`cnf.x5t#S256`) and
delegation (`may_act`) claims.
@macastelaz
macastelaz requested review from a team as code owners October 27, 2026 02:32

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for actor tokens in Workload Identity Federation, enabling certificate-bound workloads and delegation. It updates IdentityPoolCredentials and ImpersonatedCredentials to handle atomic subject and actor token reads, per-cycle mTLS transport pinning, and robust 401/400 retry mechanisms with certificate reloading. Additionally, MtlsHttpTransportFactory is made serializable, and extensive hermetic tests are added to validate the mTLS pipeline. Feedback on the changes suggests wrapping keyStore.getKey in a nested try-catch block within OAuth2Utils to prevent password mismatches from disabling token caching, and deferring InputStreamReader instantiation in FileIdentityPoolSubjectTokenSupplier to avoid consuming bytes before JSON parsing.

@macastelaz
macastelaz requested a review from lqiu96 October 27, 2026 13:46
*/
FileIdentityPoolSubjectTokenSupplier(IdentityPoolCredentialSource credentialSource) {
this.credentialSource = credentialSource;
this.credentialSource = checkNotNull(credentialSource, "credentialSource cannot be null");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

qq: From what I see FileIdentityPoolSubjectTokenSupplier is an existing class, is this guaranteed to not be a behavior change? Would null credentialSource worked before?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

FileIdentityPoolSubjectTokenSupplier is package-private (class FileIdentityPoolSubjectTokenSupplier) and is only instantiated inside IdentityPoolCredentials after verifying credentialSource != null. Previously, a null credentialSource would have thrown a NullPointerException on the first call to getSubjectToken() (at credentialSource.getCredentialLocation()); Preconditions.checkNotNull fails fast with the same NullPointerException at construction time.

Comment on lines +82 to +83
"Actor tokens are only supported for JSON-formatted credential files with distinct field"
+ " names.");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

qq: What does distinct field names mean in this case? This is checking for format type == text, but the error message suggests something else

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch! Updated the error message in e05fa17 to "Actor tokens are only supported for JSON-formatted credential files."

static String parseToken(InputStream inputStream, IdentityPoolCredentialSource credentialSource)
throws IOException {
if (credentialSource.credentialFormatType == CredentialFormatType.TEXT) {
private String getToken(@Nullable String targetFieldName) throws IOException {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

qq, should targetFieldName allowed to be nullable? What cases would be supported for this? Would it just be easier to assert non-null here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

targetFieldName is null when reading a TEXT-formatted subject token: getSubjectToken() passes credentialSource.subjectTokenFieldName, which is null for TEXT format (since the entire file content is read as the raw token string rather than extracting a JSON field). Added an explanatory comment above getToken in e05fa17 to clarify this.

Comment on lines +203 to +209
static boolean isDefaultOrMtlsTransportFactory(@Nullable HttpTransportFactory transportFactory) {
return transportFactory == null
|| transportFactory instanceof OAuth2Utils.DefaultHttpTransportFactory
|| transportFactory.getClass() == MtlsHttpTransportFactory.class
|| (transportFactory instanceof MtlsHttpTransportFactory
&& !((MtlsHttpTransportFactory) transportFactory).hasKeyStore());
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: I think I've seen this type of check added a few times now. I wonder if this can be put in a shared util?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in e05fa17 — moved isDefaultOrMtlsTransportFactory, hasMtlsKeyStore, and isKeylessMtlsTransportFactory into OAuth2Utils and updated IdentityPoolCredentials and ImpersonatedCredentials to use the shared helpers.

Comment on lines +124 to +127
ImpersonatedCredentials impersonated = getImpersonatedCredentials();
if (impersonated != null) {
return impersonated.refreshAccessToken();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: I don't recall the logic for this from the old PRs, can you add a comment that explains this logic and why it's needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in e05fa17 — added an inline comment in both PluggableAuthCredentials and AwsCredentials explaining why getImpersonatedCredentials() must be reconciled with the parent builder's transportFactory when service account impersonation is configured.

if (content == null || content.trim().isEmpty()) {
OAuthException oauthException =
new OAuthException(
"http_error_" + e.getStatusCode(), e.getStatusMessage(), null, e.getStatusCode());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

qq, what is this format string exception http_error_{status}_{message}? I think it may just be clearer as a string

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Those are actually two separate arguments to the OAuthException(String errorCode, @Nullable String errorDescription, @Nullable String errorUri) constructor ("http_error_" + e.getStatusCode() is errorCode, and e.getStatusMessage() is errorDescription), rather than a single concatenated string. OAuthException enforces Preconditions.checkNotNull(errorCode) and formats the message as "Error code " + errorCode + ": " + errorDescription. Added an inline comment in e05fa17 to make this clearer.

Comment on lines +112 to +135
JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(content);
GenericJson errorResponse = parser.parseAndClose(GenericJson.class);
if (errorResponse == null) {
OAuthException oauthException =
new OAuthException(
"http_error_" + e.getStatusCode(), e.getStatusMessage(), null, e.getStatusCode());
oauthException.initCause(e);
return oauthException;
}

String errorCode = null;
String errorDescription = null;
Object rawError = errorResponse.get("error");
if (rawError instanceof String) {
errorCode = (String) rawError;
} else if (rawError instanceof Map) {
Map<?, ?> errorMap = (Map<?, ?>) rawError;
if (errorMap.get("status") instanceof String) {
errorCode = (String) errorMap.get("status");
}
if (errorMap.get("message") instanceof String) {
errorDescription = (String) errorMap.get("message");
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: Can you add comments to explain what this big block of code is doing? This looks to be parsing out status/ messages from the error details? What is the format of that

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in e05fa17 — added inline comments documenting the two JSON error formats supported here (standard OAuth 2.0 RFC 6749 flat JSON {"error": "...", "error_description": "...", "error_uri": "..."} from the STS token endpoint vs. nested Google API Standard Error google.rpc.Status JSON {"error": {"code": 401, "status": "UNAUTHENTICATED", "message": "..."}} from the IAM Credentials API) and the fallback order.

Comment on lines +124 to +129
// Token and IAM endpoints can return errors in two JSON schemas:
// 1. RFC 6749 Section 5.2 OAuth 2.0 format, where "error" is a top-level String:
// {"error": "invalid_grant", "error_description": "...", "error_uri": "..."}
// 2. Google API Standard Error format (google.rpc.Status / AIP-193), returned by GFE/STS on
// mTLS or HTTP 401/403 authentication errors, where "error" is a nested JSON object:
// {"error": {"code": 401, "status": "UNAUTHENTICATED", "message": "..."}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the comment. This is for the token/ IAM endpoints, does the above check come from a different endpoint response?

This method is a bit long, can the token/ IAM endpoint logic here be moved to a helper?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The whole createFromHttpResponseException method handles error responses from the OAuth, STS, and IAM endpoints — the checks above (content == null || content.trim().isEmpty() and errorResponse == null) are just the guards for when an HTTP error response has an empty or null body before inspecting the parsed JSON fields.

Extracted the JSON field extraction into a createFromJsonErrorResponse helper in 65d592b and updated the comments to make that distinction clearer.

@lqiu96 lqiu96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is merging already reviewed code into main. Reviewed the new changes and those LGTM

@macastelaz
macastelaz enabled auto-merge (squash) October 30, 2026 21:24
@macastelaz
macastelaz merged commit dcb29b0 into main Oct 30, 2026
298 of 311 checks passed
@macastelaz
macastelaz deleted the oauth2-bound-tokens branch October 30, 2026 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants