Skip to content

[google-auth-library-java] OAuthException.createFromHttpResponseException throws ClassCastException on a Google API error body, losing the HTTP status and body #14514

Description

@ozan-backupta

Environment details

  1. API: auth, workload identity federation (AwsCredentials, STS token exchange during Cloud Storage calls)
  2. OS type and version: Linux (AWS Lambda and AWS ECS)
  3. Java version: 25
  4. Version(s): google-auth-library-oauth2-http 1.50.0 (libraries-bom 26.86.0), google-http-client 2.2.0. createFromHttpResponseException is unchanged in 1.53.0 and on main (05a998f).

Steps to reproduce

  1. Deterministic, no credentials needed: call OAuthException.createFromHttpResponseException with an HttpResponseException whose content is a Google API error body (snippet below). It throws ClassCastException.
  2. Against the real STS: an AwsCredentials whose token_url is https://sts.googleapis.com/v1/introspect gets HTTP 400 with {"error":{"code":400,"message":"Invalid JSON payload received. ...","status":"INVALID_ARGUMENT"}} and fails with the same ClassCastException. The misconfigured URL is only a way to get that body shape on demand; in production the same exception comes from https://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

Any additional information below

  • OAuthException.java:90 casts error to String. The request-validation errors we could trigger on /v1/token come back in the OAuth form ({"error":"invalid_request","error_description":"..."}), but some production responses from /v1/token carry error as an object (Google's API error format), which makes the cast throw. We could not capture those bodies because the library drops them.
  • The ClassCastException is thrown from inside catch (HttpResponseException e) in StsRequestHandler.exchangeToken. It is a RuntimeException, so it escapes the IOException contract of refresh() / getRequestMetadata(), and it has no cause, so the HttpResponseException holding the status and body is lost. LoggingUtils logs 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 ClassCastException is not an IOException, 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 error key makes the constructor's checkNotNull(errorCode) throw NullPointerException.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions