Summary
When an external_account (AWS) credential is Java-serialized and then deserialized, a later refresh() throws a NullPointerException because the credential's internal HttpTransportFactory (transportFactory) is declared transient and is not restored on deserialization. There is no readObject (or equivalent) that re-initializes it to the default transport.
The region-lookup / STS path then dereferences the null transport and fails with:
com.google.api.client.googleapis.json.GoogleJsonResponseException / IllegalStateException
... UNAUTHENTICATED: Failed computing credential metadata
Caused by: java.lang.NullPointerException
at ...InternalAwsSecurityCredentialsSupplier... (transportFactory is null)
Why this matters / real-world trigger
Distributed frameworks routinely serialize a Credentials object on one JVM and use it on another e.g. Apache Spark builds credentials on the driver and ships them to executors. The credential often serializes and works initially (or the token is still valid), but the first refresh after deserialization (long-running jobs, past the ~1h token lifetime) NPEs on the null transport. This surfaces as intermittent UNAUTHENTICATED failures that look unrelated to serialization.
GoogleCredentials implements Serializable, so serializing one is a reasonable expectation; the silent loss of the transport is surprising.
Minimal reproduction
import com.google.auth.oauth2.GoogleCredentials;
import java.io.*;
public class Repro {
public static void main(String[] args) throws Exception {
// external_account (AWS) config; GOOGLE_APPLICATION_CREDENTIALS points at it,
// or load via GoogleCredentials.fromStream(new FileInputStream(...)).
GoogleCredentials cred =
GoogleCredentials.getApplicationDefault()
.createScoped("https://www.googleapis.com/auth/cloud-platform");
// Round-trip through Java serialization (what Spark does driver -> executor).
byte[] bytes;
try (var bos = new ByteArrayOutputStream(); var oos = new ObjectOutputStream(bos)) {
oos.writeObject(cred);
oos.flush();
bytes = bos.toByteArray();
}
GoogleCredentials restored;
try (var ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
restored = (GoogleCredentials) ois.readObject();
}
// Fresh instance refreshes fine; the deserialized one NPEs because transportFactory == null.
restored.refresh(); // <-- NullPointerException here
}
}
Run on an AWS host (EC2, so IMDS is reachable) with an external_account AWS credential config.
Root cause
The internal AWS security-credentials supplier holds an HttpTransportFactory marked transient.
Nothing re-initializes it after deserialization, so it comes back null, and the AWS region/metadata lookup dereferences it during refresh().
Expected behavior
After deserialization, refresh() should work: the transport should be restored to the default
(OAuth2Utils.HTTP_TRANSPORT_FACTORY) via a readObject/readResolve, matching the pattern other Credentials subclasses use for their transient transport fields.
Workaround (for others hitting this)
Don't serialize a live credential across JVMs. Instead serialize the config bytes and rebuild
GoogleCredentials.fromStream(...) on the target JVM so the transport is freshly initialized. (In Spark we do this via a custom AccessTokenProvider that carries the WIF config JSON and mints tokens per-executor.)
Environment
- Library:
com.google.auth:google-auth-library-oauth2-http
- Version: 1.43.0
(as shaded inside com.google.cloud.spark:spark-3.5-bigquery:0.44.1, but the bug is in the
plain upstream library — reproducible against google-auth-library-oauth2-http:1.43.0 directly,
and the transient field is still present on main)
- Java: 17
- Credential type: Workload Identity Federation,
external_account with AWS source
(credential_source.environment_id = "aws1", region/URL from EC2 IMDS)
Summary
When an
external_account(AWS) credential is Java-serialized and then deserialized, a laterrefresh()throws aNullPointerExceptionbecause the credential's internalHttpTransportFactory(transportFactory) is declaredtransientand is not restored on deserialization. There is noreadObject(or equivalent) that re-initializes it to the default transport.The region-lookup / STS path then dereferences the null transport and fails with:
com.google.api.client.googleapis.json.GoogleJsonResponseException / IllegalStateException
... UNAUTHENTICATED: Failed computing credential metadata
Caused by: java.lang.NullPointerException
at ...InternalAwsSecurityCredentialsSupplier... (transportFactory is null)
Why this matters / real-world trigger
Distributed frameworks routinely serialize a
Credentialsobject on one JVM and use it on another e.g. Apache Spark builds credentials on the driver and ships them to executors. The credential often serializes and works initially (or the token is still valid), but the first refresh after deserialization (long-running jobs, past the ~1h token lifetime) NPEs on the null transport. This surfaces as intermittentUNAUTHENTICATEDfailures that look unrelated to serialization.GoogleCredentials implements Serializable, so serializing one is a reasonable expectation; the silent loss of the transport is surprising.Minimal reproduction
Run on an AWS host (EC2, so IMDS is reachable) with an external_account AWS credential config.
Root cause
The internal AWS security-credentials supplier holds an HttpTransportFactory marked transient.
Nothing re-initializes it after deserialization, so it comes back null, and the AWS region/metadata lookup dereferences it during refresh().
Expected behavior
After deserialization, refresh() should work: the transport should be restored to the default
(OAuth2Utils.HTTP_TRANSPORT_FACTORY) via a readObject/readResolve, matching the pattern other Credentials subclasses use for their transient transport fields.
Workaround (for others hitting this)
Don't serialize a live credential across JVMs. Instead serialize the config bytes and rebuild
GoogleCredentials.fromStream(...) on the target JVM so the transport is freshly initialized. (In Spark we do this via a custom AccessTokenProvider that carries the WIF config JSON and mints tokens per-executor.)
Environment
com.google.auth:google-auth-library-oauth2-http(as shaded inside
com.google.cloud.spark:spark-3.5-bigquery:0.44.1, but the bug is in theplain upstream library — reproducible against
google-auth-library-oauth2-http:1.43.0directly,and the
transientfield is still present onmain)external_accountwith AWS source(
credential_source.environment_id = "aws1", region/URL from EC2 IMDS)