Repository navigation
[google-auth-library-java] OAuthException.createFromHttpResponseException throws ClassCastException on a Google API error body, losing the HTTP status and body #14514
Copy link
Copy link
Open
Description
Environment details
- API: auth, workload identity federation (
AwsCredentials, STS token exchange during Cloud Storage calls) - OS type and version: Linux (AWS Lambda and AWS ECS)
- Java version: 25
- Version(s):
google-auth-library-oauth2-http1.50.0 (libraries-bom 26.86.0),google-http-client2.2.0.createFromHttpResponseExceptionis unchanged in 1.53.0 and onmain(05a998f).
Steps to reproduce
- Deterministic, no credentials needed: call
OAuthException.createFromHttpResponseExceptionwith anHttpResponseExceptionwhose content is a Google API error body (snippet below). It throwsClassCastException. - Against the real STS: an
AwsCredentialswhosetoken_urlishttps://sts.googleapis.com/v1/introspectgets HTTP 400 with{"error":{"code":400,"message":"Invalid JSON payload received. ...","status":"INVALID_ARGUMENT"}}and fails with the sameClassCastException. The misconfigured URL is only a way to get that body shape on demand; in production the same exception comes fromhttps://sts.googleapis.com/v1/token.
Code example
package com.google.auth.oauth2;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpResponseException;
class OAuthExceptionNestedErrorRepro {
public static void main(String[] args) throws Exception {
HttpResponseException e =
new HttpResponseException.Builder(400, "Bad Request", new HttpHeaders())
.setContent(
"{\"error\":{\"code\":400,\"message\":\"Request contains an invalid argument.\",\"status\":\"INVALID_ARGUMENT\"}}")
.build();
OAuthException.createFromHttpResponseException(e); // java.lang.ClassCastException
}
}
Stack trace
Production, workload identity federation from AWS, token exchange triggered by a Cloud Storage request:
java.lang.ClassCastException: class com.google.api.client.util.ArrayMap cannot be cast to class java.lang.String
at com.google.auth.oauth2.OAuthException.createFromHttpResponseException(OAuthException.java:90)
at com.google.auth.oauth2.StsRequestHandler.exchangeToken(StsRequestHandler.java:129)
at com.google.auth.oauth2.ExternalAccountCredentials.exchangeExternalCredentialForAccessToken(ExternalAccountCredentials.java:567)
at com.google.auth.oauth2.AwsCredentials.refreshAccessToken(AwsCredentials.java:132)
at com.google.auth.oauth2.OAuth2Credentials.getRequestMetadata(OAuth2Credentials.java:189)
External references such as API reference guides
- ClassCastException: class
com.google.api.client.util.ArrayMapcannot be cast to classjava.lang.Stringbazelbuild/bazel#29600: the same exception fromIdentityPoolCredentials(GitLab CI, workload identity federation), closed as not reproducible. - test(oauth2): add mTLS in-process socket tests and live GCP WIF integration tests #14220: contains an equivalent guard in
createFromHttpResponseException, but targets theoauth2-bound-tokensbranch and does not mention this bug.
Any additional information below
OAuthException.java:90castserrortoString. The request-validation errors we could trigger on/v1/tokencome back in the OAuth form ({"error":"invalid_request","error_description":"..."}), but some production responses from/v1/tokencarryerroras an object (Google's API error format), which makes the cast throw. We could not capture those bodies because the library drops them.- The
ClassCastExceptionis thrown from insidecatch (HttpResponseException e)inStsRequestHandler.exchangeToken. It is aRuntimeException, so it escapes theIOExceptioncontract ofrefresh()/getRequestMetadata(), and it has no cause, so theHttpResponseExceptionholding the status and body is lost.LoggingUtilslogs the response only on success, so the body is not logged either. When this happened to us intermittently in production (a burst of failures across several independent GCP projects within minutes), we could not tell whether STS was rate limiting, unavailable, or refusing the credential. - Because the
ClassCastExceptionis not anIOException, google-cloud-storage classifies it as unretryable (Unretryable error (attempts: 1, maxAttempts: 6)), so a transient STS failure is not retried either. - The same method is also called from
ExternalAccountAuthorizedUserCredentials.java:204. - Related: a body without an
errorkey makes the constructor'scheckNotNull(errorCode)throwNullPointerException.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity