From 06bf21a6ce9478a35907bd6681e53a0e86ffc83f Mon Sep 17 00:00:00 2001
From: Leo <39062083+lsirac@users.noreply.github.com>
Date: Fri, 20 Jan 2023 22:10:46 -0800
Subject: [PATCH] feat: adds external account authorized user credentials
(#1129)
* feat: adds external account authorized user credentials
* fix: positioning
* fix: serialVersionUID
* fix: toString() test
* fix: tests
---
...ernalAccountAuthorizedUserCredentials.java | 513 ++++++++
.../google/auth/oauth2/GoogleCredentials.java | 4 +
.../google/auth/oauth2/OAuthException.java | 21 +
.../google/auth/oauth2/StsRequestHandler.java | 12 +-
...lAccountAuthorizedUserCredentialsTest.java | 1124 +++++++++++++++++
.../auth/oauth2/GoogleCredentialsTest.java | 16 +
.../google/auth/oauth2/MockStsTransport.java | 46 +-
.../auth/oauth2/OAuthExceptionTest.java | 48 +
8 files changed, 1756 insertions(+), 28 deletions(-)
create mode 100644 oauth2_http/java/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentials.java
create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java
diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentials.java
new file mode 100644
index 000000000..3304eae75
--- /dev/null
+++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentials.java
@@ -0,0 +1,513 @@
+/*
+ * 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.oauth2;
+
+import static com.google.auth.oauth2.OAuth2Utils.JSON_FACTORY;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpHeaders;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpResponseException;
+import com.google.api.client.http.UrlEncodedContent;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonObjectParser;
+import com.google.api.client.util.GenericData;
+import com.google.api.client.util.Preconditions;
+import com.google.auth.http.HttpTransportFactory;
+import com.google.common.base.MoreObjects;
+import com.google.common.io.BaseEncoding;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.Map;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * OAuth2 credentials sourced using external identities through Workforce Identity Federation.
+ *
+ *
Obtaining the initial access and refresh token can be done through the Google Cloud CLI.
+ *
+ *
+ * Example credentials file:
+ * {
+ * "type": "external_account_authorized_user",
+ * "audience": "//iam.googleapis.com/locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID",
+ * "refresh_token": "refreshToken",
+ * "token_url": "https://sts.googleapis.com/v1/oauthtoken",
+ * "token_info_url": "https://sts.googleapis.com/v1/introspect",
+ * "client_id": "clientId",
+ * "client_secret": "clientSecret"
+ * }
+ *
+ */
+public class ExternalAccountAuthorizedUserCredentials extends GoogleCredentials {
+
+ private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
+
+ private static final long serialVersionUID = -2181779590486283287L;
+
+ static final String EXTERNAL_ACCOUNT_AUTHORIZED_USER_FILE_TYPE =
+ "external_account_authorized_user";
+
+ private final String transportFactoryClassName;
+ private final String audience;
+ private final String tokenUrl;
+ private final String tokenInfoUrl;
+ private final String revokeUrl;
+ private final String clientId;
+ private final String clientSecret;
+
+ private String refreshToken;
+
+ private transient HttpTransportFactory transportFactory;
+
+ /**
+ * Internal constructor.
+ *
+ * @param builder A builder for {@link ExternalAccountAuthorizedUserCredentials}. See {@link
+ * ExternalAccountAuthorizedUserCredentials.Builder}
+ */
+ private ExternalAccountAuthorizedUserCredentials(Builder builder) {
+ super(builder);
+ this.transportFactory =
+ MoreObjects.firstNonNull(
+ builder.transportFactory,
+ getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
+ this.transportFactoryClassName = this.transportFactory.getClass().getName();
+ this.audience = builder.audience;
+ this.refreshToken = builder.refreshToken;
+ this.tokenUrl = builder.tokenUrl;
+ this.tokenInfoUrl = builder.tokenInfoUrl;
+ this.revokeUrl = builder.revokeUrl;
+ this.clientId = builder.clientId;
+ this.clientSecret = builder.clientSecret;
+
+ Preconditions.checkState(
+ getAccessToken() != null || canRefresh(),
+ "ExternalAccountAuthorizedUserCredentials must be initialized with "
+ + "an access token or fields to enable refresh: "
+ + "('refresh_token', 'token_url', 'client_id', 'client_secret').");
+ }
+
+ /**
+ * Returns external account authorized user credentials defined by a JSON file stream.
+ *
+ * @param credentialsStream the stream with the credential definition
+ * @return the credential defined by the credentialsStream
+ * @throws IOException if the credential cannot be created from the stream
+ */
+ public static ExternalAccountAuthorizedUserCredentials fromStream(InputStream credentialsStream)
+ throws IOException {
+ checkNotNull(credentialsStream);
+ return fromStream(credentialsStream, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+ }
+
+ /**
+ * Returns external account authorized user credentials defined by a JSON file stream.
+ *
+ * @param credentialsStream the stream with the credential definition
+ * @param transportFactory the HTTP transport factory used to create the transport to get access
+ * tokens
+ * @return the credential defined by the credentialsStream
+ * @throws IOException if the credential cannot be created from the stream
+ */
+ public static ExternalAccountAuthorizedUserCredentials fromStream(
+ InputStream credentialsStream, HttpTransportFactory transportFactory) throws IOException {
+ checkNotNull(credentialsStream);
+ checkNotNull(transportFactory);
+
+ JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
+ GenericJson fileContents =
+ parser.parseAndClose(credentialsStream, StandardCharsets.UTF_8, GenericJson.class);
+ try {
+ return fromJson(fileContents, transportFactory);
+ } catch (ClassCastException | IllegalArgumentException e) {
+ throw new CredentialFormatException("Invalid input stream provided.", e);
+ }
+ }
+
+ @Override
+ public AccessToken refreshAccessToken() throws IOException {
+ if (!canRefresh()) {
+ throw new IllegalStateException(
+ "Unable to refresh ExternalAccountAuthorizedUserCredentials. All of 'refresh_token',"
+ + "'token_url', 'client_id', 'client_secret' are required to refresh.");
+ }
+
+ HttpResponse response;
+ try {
+ HttpRequest httpRequest = buildRefreshRequest();
+ response = httpRequest.execute();
+ } catch (HttpResponseException e) {
+ throw OAuthException.createFromHttpResponseException(e);
+ }
+
+ // Parse response.
+ GenericData responseData = response.parseAs(GenericData.class);
+ response.disconnect();
+
+ // Required fields.
+ String accessToken =
+ OAuth2Utils.validateString(responseData, /* key= */ "access_token", PARSE_ERROR_PREFIX);
+ int expiresInSeconds =
+ OAuth2Utils.validateInt32(responseData, /* key= */ "expires_in", PARSE_ERROR_PREFIX);
+ Date expiresAtMilliseconds = new Date(clock.currentTimeMillis() + expiresInSeconds * 1000L);
+
+ // Set the new refresh token if returned.
+ String refreshToken =
+ OAuth2Utils.validateOptionalString(
+ responseData, /* key= */ "refresh_token", PARSE_ERROR_PREFIX);
+ if (refreshToken != null && refreshToken.trim().length() > 0) {
+ this.refreshToken = refreshToken;
+ }
+
+ return AccessToken.newBuilder()
+ .setExpirationTime(expiresAtMilliseconds)
+ .setTokenValue(accessToken)
+ .build();
+ }
+
+ @Nullable
+ public String getAudience() {
+ return audience;
+ }
+
+ @Nullable
+ public String getClientId() {
+ return clientId;
+ }
+
+ @Nullable
+ public String getClientSecret() {
+ return clientSecret;
+ }
+
+ @Nullable
+ public String getRevokeUrl() {
+ return revokeUrl;
+ }
+
+ @Nullable
+ public String getTokenUrl() {
+ return tokenUrl;
+ }
+
+ @Nullable
+ public String getTokenInfoUrl() {
+ return tokenInfoUrl;
+ }
+
+ @Nullable
+ public String getRefreshToken() {
+ return refreshToken;
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ super.hashCode(),
+ clientId,
+ clientSecret,
+ refreshToken,
+ tokenUrl,
+ tokenInfoUrl,
+ revokeUrl,
+ audience,
+ transportFactoryClassName,
+ quotaProjectId);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("requestMetadata", getRequestMetadataInternal())
+ .add("temporaryAccess", getAccessToken())
+ .add("clientId", clientId)
+ .add("clientSecret", clientSecret)
+ .add("refreshToken", refreshToken)
+ .add("tokenUrl", tokenUrl)
+ .add("tokenInfoUrl", tokenInfoUrl)
+ .add("revokeUrl", revokeUrl)
+ .add("audience", audience)
+ .add("transportFactoryClassName", transportFactoryClassName)
+ .add("quotaProjectId", quotaProjectId)
+ .toString();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ExternalAccountAuthorizedUserCredentials)) {
+ return false;
+ }
+ ExternalAccountAuthorizedUserCredentials credentials =
+ (ExternalAccountAuthorizedUserCredentials) obj;
+ return super.equals(credentials)
+ && Objects.equals(this.clientId, credentials.clientId)
+ && Objects.equals(this.clientSecret, credentials.clientSecret)
+ && Objects.equals(this.refreshToken, credentials.refreshToken)
+ && Objects.equals(this.tokenUrl, credentials.tokenUrl)
+ && Objects.equals(this.tokenInfoUrl, credentials.tokenInfoUrl)
+ && Objects.equals(this.revokeUrl, credentials.revokeUrl)
+ && Objects.equals(this.audience, credentials.audience)
+ && Objects.equals(this.transportFactoryClassName, credentials.transportFactoryClassName)
+ && Objects.equals(this.quotaProjectId, credentials.quotaProjectId);
+ }
+
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ /**
+ * Returns external account authorized user credentials defined by JSON contents using the format
+ * supported by the Cloud SDK.
+ *
+ * @param json a map from the JSON representing the credentials
+ * @param transportFactory HTTP transport factory, creates the transport used to get access tokens
+ * @return the external account authorized user credentials defined by the JSON
+ */
+ static ExternalAccountAuthorizedUserCredentials fromJson(
+ Map json, HttpTransportFactory transportFactory) throws IOException {
+ String audience = (String) json.get("audience");
+ String refreshToken = (String) json.get("refresh_token");
+ String tokenUrl = (String) json.get("token_url");
+ String tokenInfoUrl = (String) json.get("token_info_url");
+ String revokeUrl = (String) json.get("revoke_url");
+ String clientId = (String) json.get("client_id");
+ String clientSecret = (String) json.get("client_secret");
+ String quotaProjectId = (String) json.get("quota_project_id");
+
+ return ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(audience)
+ .setRefreshToken(refreshToken)
+ .setTokenUrl(tokenUrl)
+ .setTokenInfoUrl(tokenInfoUrl)
+ .setRevokeUrl(revokeUrl)
+ .setClientId(clientId)
+ .setClientSecret(clientSecret)
+ .setRefreshToken(refreshToken)
+ .setHttpTransportFactory(transportFactory)
+ .setQuotaProjectId(quotaProjectId)
+ .build();
+ }
+
+ private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
+ input.defaultReadObject();
+ transportFactory = newInstance(transportFactoryClassName);
+ }
+
+ private boolean canRefresh() {
+ return refreshToken != null
+ && refreshToken.trim().length() > 0
+ && tokenUrl != null
+ && tokenUrl.trim().length() > 0
+ && clientId != null
+ && clientId.trim().length() > 0
+ && clientSecret != null
+ && clientSecret.trim().length() > 0;
+ }
+
+ private HttpRequest buildRefreshRequest() throws IOException {
+ GenericData tokenRequest = new GenericData();
+ tokenRequest.set("grant_type", "refresh_token");
+ tokenRequest.set("refresh_token", refreshToken);
+
+ HttpRequest request =
+ transportFactory
+ .create()
+ .createRequestFactory()
+ .buildPostRequest(new GenericUrl(tokenUrl), new UrlEncodedContent(tokenRequest));
+
+ request.setParser(new JsonObjectParser(JSON_FACTORY));
+
+ HttpHeaders requestHeaders = request.getHeaders();
+ requestHeaders.setAuthorization(
+ String.format(
+ "Basic %s",
+ BaseEncoding.base64()
+ .encode(
+ String.format("%s:%s", clientId, clientSecret)
+ .getBytes(StandardCharsets.UTF_8))));
+
+ return request;
+ }
+
+ /** Builder for {@link ExternalAccountAuthorizedUserCredentials}. */
+ public static class Builder extends GoogleCredentials.Builder {
+
+ private HttpTransportFactory transportFactory;
+ private String audience;
+ private String refreshToken;
+ private String tokenUrl;
+ private String tokenInfoUrl;
+ private String revokeUrl;
+ private String clientId;
+ private String clientSecret;
+
+ protected Builder() {}
+
+ protected Builder(ExternalAccountAuthorizedUserCredentials credentials) {
+ super(credentials);
+ this.transportFactory = credentials.transportFactory;
+ this.audience = credentials.audience;
+ this.refreshToken = credentials.refreshToken;
+ this.tokenUrl = credentials.tokenUrl;
+ this.tokenInfoUrl = credentials.tokenInfoUrl;
+ this.revokeUrl = credentials.revokeUrl;
+ this.clientId = credentials.clientId;
+ this.clientSecret = credentials.clientSecret;
+ }
+
+ /**
+ * Sets the HTTP transport factory.
+ *
+ * @param transportFactory the {@code HttpTransportFactory} to set
+ * @return this {@code Builder} object
+ */
+ public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
+ this.transportFactory = transportFactory;
+ return this;
+ }
+
+ /**
+ * Sets the optional audience, which is usually the fully specified resource name of the
+ * workforce pool provider.
+ *
+ * @param audience the audience to set
+ * @return this {@code Builder} object
+ */
+ public Builder setAudience(String audience) {
+ this.audience = audience;
+ return this;
+ }
+
+ /**
+ * Sets the token exchange endpoint.
+ *
+ * @param tokenUrl the token exchange url to set
+ * @return this {@code Builder} object
+ */
+ public Builder setTokenUrl(String tokenUrl) {
+ this.tokenUrl = tokenUrl;
+ return this;
+ }
+
+ /**
+ * Sets the token introspection endpoint used to retrieve account related information.
+ *
+ * @param tokenInfoUrl the token info url to set
+ * @return this {@code Builder} object
+ */
+ public Builder setTokenInfoUrl(String tokenInfoUrl) {
+ this.tokenInfoUrl = tokenInfoUrl;
+ return this;
+ }
+
+ /**
+ * Sets the token revocation endpoint.
+ *
+ * @param revokeUrl the revoke url to set
+ * @return this {@code Builder} object
+ */
+ public Builder setRevokeUrl(String revokeUrl) {
+ this.revokeUrl = revokeUrl;
+ return this;
+ }
+
+ /**
+ * Sets the OAuth 2.0 refresh token.
+ *
+ * @param refreshToken the refresh token
+ * @return this {@code Builder} object
+ */
+ public Builder setRefreshToken(String refreshToken) {
+ this.refreshToken = refreshToken;
+ return this;
+ }
+
+ /**
+ * Sets the OAuth 2.0 client ID.
+ *
+ * @param clientId the client ID
+ * @return this {@code Builder} object
+ */
+ public Builder setClientId(String clientId) {
+ this.clientId = clientId;
+ return this;
+ }
+
+ /**
+ * Sets the OAuth 2.0 client secret.
+ *
+ * @param clientSecret the client secret
+ * @return this {@code Builder} object
+ */
+ public Builder setClientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
+ return this;
+ }
+
+ /**
+ * Sets the optional project used for quota and billing purposes.
+ *
+ * @param quotaProjectId the quota and billing project id to set
+ * @return this {@code Builder} object
+ */
+ public Builder setQuotaProjectId(String quotaProjectId) {
+ super.setQuotaProjectId(quotaProjectId);
+ return this;
+ }
+
+ /**
+ * Sets the optional access token.
+ *
+ * @param accessToken the access token
+ * @return this {@code Builder} object
+ */
+ public Builder setAccessToken(AccessToken accessToken) {
+ super.setAccessToken(accessToken);
+ return this;
+ }
+
+ public ExternalAccountAuthorizedUserCredentials build() {
+ return new ExternalAccountAuthorizedUserCredentials(this);
+ }
+ }
+}
diff --git a/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java
index 9fc61c29d..d12632a32 100644
--- a/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java
@@ -181,6 +181,10 @@ public static GoogleCredentials fromStream(
if (ExternalAccountCredentials.EXTERNAL_ACCOUNT_FILE_TYPE.equals(fileType)) {
return ExternalAccountCredentials.fromJson(fileContents, transportFactory);
}
+ if (ExternalAccountAuthorizedUserCredentials.EXTERNAL_ACCOUNT_AUTHORIZED_USER_FILE_TYPE.equals(
+ fileType)) {
+ return ExternalAccountAuthorizedUserCredentials.fromJson(fileContents, transportFactory);
+ }
if ("impersonated_service_account".equals(fileType)) {
return ImpersonatedCredentials.fromJson(fileContents, transportFactory);
}
diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuthException.java b/oauth2_http/java/com/google/auth/oauth2/OAuthException.java
index a920b93d0..011456e24 100644
--- a/oauth2_http/java/com/google/auth/oauth2/OAuthException.java
+++ b/oauth2_http/java/com/google/auth/oauth2/OAuthException.java
@@ -33,6 +33,10 @@
import static com.google.common.base.Preconditions.checkNotNull;
+import com.google.api.client.http.HttpResponseException;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.json.JsonParser;
+import java.io.IOException;
import javax.annotation.Nullable;
/**
@@ -77,4 +81,21 @@ String getErrorDescription() {
String getErrorUri() {
return errorUri;
}
+
+ static OAuthException createFromHttpResponseException(HttpResponseException e)
+ throws IOException {
+ JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser((e).getContent());
+ GenericJson errorResponse = parser.parseAndClose(GenericJson.class);
+
+ String errorCode = (String) errorResponse.get("error");
+ String errorDescription = null;
+ String errorUri = null;
+ if (errorResponse.containsKey("error_description")) {
+ errorDescription = (String) errorResponse.get("error_description");
+ }
+ if (errorResponse.containsKey("error_uri")) {
+ errorUri = (String) errorResponse.get("error_uri");
+ }
+ return new OAuthException(errorCode, errorDescription, errorUri);
+ }
}
diff --git a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java
index 15e9611b1..3f93ad86e 100644
--- a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java
+++ b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java
@@ -107,17 +107,7 @@ public StsTokenExchangeResponse exchangeToken() throws IOException {
GenericData responseData = response.parseAs(GenericData.class);
return buildResponse(responseData);
} catch (HttpResponseException e) {
- GenericJson errorResponse = parseJson((e).getContent());
- String errorCode = (String) errorResponse.get("error");
- String errorDescription = null;
- String errorUri = null;
- if (errorResponse.containsKey("error_description")) {
- errorDescription = (String) errorResponse.get("error_description");
- }
- if (errorResponse.containsKey("error_uri")) {
- errorUri = (String) errorResponse.get("error_uri");
- }
- throw new OAuthException(errorCode, errorDescription, errorUri);
+ throw OAuthException.createFromHttpResponseException(e);
}
}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java
new file mode 100644
index 000000000..2fb89dfdc
--- /dev/null
+++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountAuthorizedUserCredentialsTest.java
@@ -0,0 +1,1124 @@
+/*
+ * 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.oauth2;
+
+import static com.google.auth.oauth2.ExternalAccountAuthorizedUserCredentials.EXTERNAL_ACCOUNT_AUTHORIZED_USER_FILE_TYPE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.testing.http.MockLowLevelHttpRequest;
+import com.google.api.client.util.Clock;
+import com.google.auth.TestUtils;
+import com.google.auth.http.AuthHttpConstants;
+import com.google.auth.http.HttpTransportFactory;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.BaseEncoding;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test case for {@link ExternalAccountAuthorizedUserCredentials}. */
+@RunWith(JUnit4.class)
+public class ExternalAccountAuthorizedUserCredentialsTest extends BaseSerializationTest {
+
+ private static final String AUDIENCE =
+ "//iam.googleapis.com/locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID";
+ private static final String CLIENT_SECRET = "jakuaL9YyieakhECKL2SwZcu";
+ private static final String CLIENT_ID = "ya29.1.AADtN_UtlxN3PuGAxrN2XQnZTVRvDyVWnYq4I6dws";
+ private static final String REFRESH_TOKEN = "1/Tl6awhpFjkMkSJoj1xsli0H2eL5YsMgU_NKPY2TyGWY";
+ private static final String ACCESS_TOKEN = "1/MkSJoj1xsli0AccessToken_NKPY2";
+ private static final String TOKEN_URL = "https://sts.googleapis.com/v1/oauthtoken";
+ private static final String TOKEN_INFO_URL = "https://sts.googleapis.com/v1/introspect";
+ private static final String REVOKE_URL = "https://sts.googleapis.com/v1/revoke";
+ private static final String QUOTA_PROJECT = "sample-quota-project-id";
+ private static final String BASIC_AUTH =
+ String.format(
+ "Basic %s",
+ BaseEncoding.base64()
+ .encode(
+ String.format("%s:%s", CLIENT_ID, CLIENT_SECRET)
+ .getBytes(StandardCharsets.UTF_8)));
+
+ private static final Collection SCOPES = Collections.singletonList("dummy.scope");
+ private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo");
+
+ private MockExternalAccountAuthorizedUserCredentialsTransportFactory transportFactory;
+
+ static class MockExternalAccountAuthorizedUserCredentialsTransportFactory
+ implements HttpTransportFactory {
+
+ MockStsTransport transport = new MockStsTransport();
+
+ @Override
+ public HttpTransport create() {
+ return transport;
+ }
+ }
+
+ @Before
+ public void setup() {
+ transportFactory = new MockExternalAccountAuthorizedUserCredentialsTransportFactory();
+ }
+
+ @Test
+ public void builder_allFields() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .build();
+
+ assertEquals(AUDIENCE, credentials.getAudience());
+ assertEquals(CLIENT_ID, credentials.getClientId());
+ assertEquals(CLIENT_SECRET, credentials.getClientSecret());
+ assertEquals(REFRESH_TOKEN, credentials.getRefreshToken());
+ assertEquals(TOKEN_URL, credentials.getTokenUrl());
+ assertEquals(TOKEN_INFO_URL, credentials.getTokenInfoUrl());
+ assertEquals(REVOKE_URL, credentials.getRevokeUrl());
+ assertEquals(ACCESS_TOKEN, credentials.getAccessToken().getTokenValue());
+ assertEquals(QUOTA_PROJECT, credentials.getQuotaProjectId());
+ }
+
+ @Test
+ public void builder_minimumRequiredFieldsForRefresh() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .build();
+
+ assertEquals(CLIENT_ID, credentials.getClientId());
+ assertEquals(CLIENT_SECRET, credentials.getClientSecret());
+ assertEquals(REFRESH_TOKEN, credentials.getRefreshToken());
+ assertEquals(TOKEN_URL, credentials.getTokenUrl());
+ assertNull(credentials.getAudience());
+ assertNull(credentials.getTokenInfoUrl());
+ assertNull(credentials.getRevokeUrl());
+ assertNull(credentials.getAccessToken());
+ assertNull(credentials.getQuotaProjectId());
+ }
+
+ @Test
+ public void builder_accessTokenOnly() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAccessToken(AccessToken.newBuilder().setTokenValue(ACCESS_TOKEN).build())
+ .build();
+
+ assertEquals(ACCESS_TOKEN, credentials.getAccessToken().getTokenValue());
+ assertNull(credentials.getAudience());
+ assertNull(credentials.getTokenUrl());
+ assertNull(credentials.getTokenInfoUrl());
+ assertNull(credentials.getRevokeUrl());
+ assertNull(credentials.getClientId());
+ assertNull(credentials.getClientSecret());
+ assertNull(credentials.getRefreshToken());
+ assertNull(credentials.getQuotaProjectId());
+ }
+
+ @Test
+ public void builder_credentialConstructor() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .build();
+
+ ExternalAccountAuthorizedUserCredentials otherCredentials = credentials.toBuilder().build();
+
+ assertEquals(AUDIENCE, otherCredentials.getAudience());
+ assertEquals(CLIENT_ID, otherCredentials.getClientId());
+ assertEquals(CLIENT_SECRET, otherCredentials.getClientSecret());
+ assertEquals(REFRESH_TOKEN, otherCredentials.getRefreshToken());
+ assertEquals(TOKEN_URL, otherCredentials.getTokenUrl());
+ assertEquals(TOKEN_INFO_URL, otherCredentials.getTokenInfoUrl());
+ assertEquals(REVOKE_URL, otherCredentials.getRevokeUrl());
+ assertEquals(QUOTA_PROJECT, otherCredentials.getQuotaProjectId());
+ }
+
+ @Test
+ public void builder_accessTokenWithMissingRefreshFields() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAccessToken(AccessToken.newBuilder().setTokenValue(ACCESS_TOKEN).build())
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setClientId(CLIENT_ID)
+ .build();
+
+ assertEquals(ACCESS_TOKEN, credentials.getAccessToken().getTokenValue());
+ assertEquals(REFRESH_TOKEN, credentials.getRefreshToken());
+ assertEquals(TOKEN_URL, credentials.getTokenUrl());
+ assertEquals(CLIENT_ID, credentials.getClientId());
+ assertNull(credentials.getAudience());
+ assertNull(credentials.getTokenInfoUrl());
+ assertNull(credentials.getRevokeUrl());
+ assertNull(credentials.getClientSecret());
+ assertNull(credentials.getQuotaProjectId());
+ }
+
+ @Test
+ public void builder_accessAndRefreshTokenNull_throws() {
+ try {
+ ExternalAccountAuthorizedUserCredentials.newBuilder().build();
+ fail("Should not be able to continue without exception.");
+ } catch (IllegalStateException exception) {
+ assertEquals(
+ "ExternalAccountAuthorizedUserCredentials must be initialized with "
+ + "an access token or fields to enable refresh: "
+ + "('refresh_token', 'token_url', 'client_id', 'client_secret').",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ public void builder_missingTokenUrl_throws() {
+ try {
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setRefreshToken(REFRESH_TOKEN)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .build();
+ fail("Should not be able to continue without exception.");
+ } catch (IllegalStateException exception) {
+ assertEquals(
+ "ExternalAccountAuthorizedUserCredentials must be initialized with "
+ + "an access token or fields to enable refresh: "
+ + "('refresh_token', 'token_url', 'client_id', 'client_secret').",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ public void builder_missingClientId_throws() {
+ try {
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setClientSecret(CLIENT_SECRET)
+ .build();
+ fail("Should not be able to continue without exception.");
+ } catch (IllegalStateException exception) {
+ assertEquals(
+ "ExternalAccountAuthorizedUserCredentials must be initialized with "
+ + "an access token or fields to enable refresh: "
+ + "('refresh_token', 'token_url', 'client_id', 'client_secret').",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ public void builder_missingClientSecret_throws() {
+ try {
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setClientId(CLIENT_ID)
+ .build();
+ fail("Should not be able to continue without exception.");
+ } catch (IllegalStateException exception) {
+ assertEquals(
+ "ExternalAccountAuthorizedUserCredentials must be initialized with "
+ + "an access token or fields to enable refresh: "
+ + "('refresh_token', 'token_url', 'client_id', 'client_secret').",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ public void toBuilder() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, new Date()))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .build();
+
+ ExternalAccountAuthorizedUserCredentials secondCredentials = credentials.toBuilder().build();
+
+ assertEquals(credentials, secondCredentials);
+ }
+
+ @Test
+ public void fromJson_allFields() throws IOException {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.fromJson(
+ buildJsonCredentials(), OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+
+ assertEquals(AUDIENCE, credentials.getAudience());
+ assertEquals(CLIENT_ID, credentials.getClientId());
+ assertEquals(CLIENT_SECRET, credentials.getClientSecret());
+ assertEquals(REFRESH_TOKEN, credentials.getRefreshToken());
+ assertEquals(TOKEN_URL, credentials.getTokenUrl());
+ assertEquals(TOKEN_INFO_URL, credentials.getTokenInfoUrl());
+ assertEquals(REVOKE_URL, credentials.getRevokeUrl());
+ assertEquals(QUOTA_PROJECT, credentials.getQuotaProjectId());
+ }
+
+ @Test
+ public void fromJson_minimumRequiredFieldsForRefresh() throws IOException {
+ GenericJson json = new GenericJson();
+ json.put("client_id", CLIENT_ID);
+ json.put("client_secret", CLIENT_SECRET);
+ json.put("refresh_token", REFRESH_TOKEN);
+ json.put("token_url", TOKEN_URL);
+
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+
+ assertEquals(CLIENT_ID, credentials.getClientId());
+ assertEquals(CLIENT_SECRET, credentials.getClientSecret());
+ assertEquals(REFRESH_TOKEN, credentials.getRefreshToken());
+ assertEquals(TOKEN_URL, credentials.getTokenUrl());
+ assertNull(credentials.getAudience());
+ assertNull(credentials.getTokenInfoUrl());
+ assertNull(credentials.getRevokeUrl());
+ assertNull(credentials.getAccessToken());
+ assertNull(credentials.getQuotaProjectId());
+ }
+
+ @Test
+ public void fromJson_accessTokenOnly_notSupported() throws IOException {
+ GenericJson json = new GenericJson();
+ json.put("access_token", ACCESS_TOKEN);
+
+ try {
+ ExternalAccountAuthorizedUserCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+ fail("Should not be able to continue without exception.");
+ } catch (IllegalStateException exception) {
+ assertEquals(
+ "ExternalAccountAuthorizedUserCredentials must be initialized with "
+ + "an access token or fields to enable refresh: "
+ + "('refresh_token', 'token_url', 'client_id', 'client_secret').",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ public void fromJson_missingRefreshToken_throws() throws IOException {
+ try {
+ GenericJson json = buildJsonCredentials();
+ json.remove("refresh_token");
+ ExternalAccountAuthorizedUserCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+ fail("Should not be able to continue without exception.");
+ } catch (IllegalStateException exception) {
+ assertEquals(
+ "ExternalAccountAuthorizedUserCredentials must be initialized with "
+ + "an access token or fields to enable refresh: "
+ + "('refresh_token', 'token_url', 'client_id', 'client_secret').",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ public void fromJson_missingTokenUrl_throws() throws IOException {
+ try {
+ GenericJson json = buildJsonCredentials();
+ json.remove("token_url");
+ ExternalAccountAuthorizedUserCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+ fail("Should not be able to continue without exception.");
+ } catch (IllegalStateException exception) {
+ assertEquals(
+ "ExternalAccountAuthorizedUserCredentials must be initialized with "
+ + "an access token or fields to enable refresh: "
+ + "('refresh_token', 'token_url', 'client_id', 'client_secret').",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ public void fromJson_missingClientId_throws() throws IOException {
+ try {
+ GenericJson json = buildJsonCredentials();
+ json.remove("client_id");
+ ExternalAccountAuthorizedUserCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+ fail("Should not be able to continue without exception.");
+ } catch (IllegalStateException exception) {
+ assertEquals(
+ "ExternalAccountAuthorizedUserCredentials must be initialized with "
+ + "an access token or fields to enable refresh: "
+ + "('refresh_token', 'token_url', 'client_id', 'client_secret').",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ public void fromJson_missingClientSecret_throws() throws IOException {
+ try {
+ GenericJson json = buildJsonCredentials();
+ json.remove("client_secret");
+ ExternalAccountAuthorizedUserCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+ fail("Should not be able to continue without exception.");
+ } catch (IllegalStateException exception) {
+ assertEquals(
+ "ExternalAccountAuthorizedUserCredentials must be initialized with "
+ + "an access token or fields to enable refresh: "
+ + "('refresh_token', 'token_url', 'client_id', 'client_secret').",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ public void fromStream_allFields() throws IOException {
+ GenericJson json = buildJsonCredentials();
+
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.fromStream(TestUtils.jsonToInputStream(json));
+
+ assertEquals(AUDIENCE, credentials.getAudience());
+ assertEquals(CLIENT_ID, credentials.getClientId());
+ assertEquals(CLIENT_SECRET, credentials.getClientSecret());
+ assertEquals(REFRESH_TOKEN, credentials.getRefreshToken());
+ assertEquals(TOKEN_URL, credentials.getTokenUrl());
+ assertEquals(TOKEN_INFO_URL, credentials.getTokenInfoUrl());
+ assertEquals(REVOKE_URL, credentials.getRevokeUrl());
+ assertEquals(QUOTA_PROJECT, credentials.getQuotaProjectId());
+ }
+
+ @Test
+ public void fromStream_minimumRequiredFieldsForRefresh() throws IOException {
+ GenericJson json = new GenericJson();
+ json.put("client_id", CLIENT_ID);
+ json.put("client_secret", CLIENT_SECRET);
+ json.put("refresh_token", REFRESH_TOKEN);
+ json.put("token_url", TOKEN_URL);
+
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.fromStream(TestUtils.jsonToInputStream(json));
+
+ assertEquals(CLIENT_ID, credentials.getClientId());
+ assertEquals(CLIENT_SECRET, credentials.getClientSecret());
+ assertEquals(REFRESH_TOKEN, credentials.getRefreshToken());
+ assertEquals(TOKEN_URL, credentials.getTokenUrl());
+ assertNull(credentials.getAudience());
+ assertNull(credentials.getTokenInfoUrl());
+ assertNull(credentials.getRevokeUrl());
+ assertNull(credentials.getAccessToken());
+ assertNull(credentials.getQuotaProjectId());
+ }
+
+ @Test
+ public void fromStream_accessTokenOnly_notSupported() throws IOException {
+ GenericJson json = new GenericJson();
+ json.put("access_token", ACCESS_TOKEN);
+ try {
+ ExternalAccountAuthorizedUserCredentials.fromStream(TestUtils.jsonToInputStream(json));
+ fail("Should not be able to continue without exception.");
+ } catch (IllegalStateException exception) {
+ assertEquals(
+ "ExternalAccountAuthorizedUserCredentials must be initialized with "
+ + "an access token or fields to enable refresh: "
+ + "('refresh_token', 'token_url', 'client_id', 'client_secret').",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ public void fromStream_missingRefreshToken_throws() throws IOException {
+ try {
+ GenericJson json = buildJsonCredentials();
+ json.remove("refresh_token");
+ ExternalAccountAuthorizedUserCredentials.fromStream(TestUtils.jsonToInputStream(json));
+ fail("Should not be able to continue without exception.");
+ } catch (IllegalStateException exception) {
+ assertEquals(
+ "ExternalAccountAuthorizedUserCredentials must be initialized with "
+ + "an access token or fields to enable refresh: "
+ + "('refresh_token', 'token_url', 'client_id', 'client_secret').",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ public void fromStream_missingTokenUrl_throws() throws IOException {
+ try {
+ GenericJson json = buildJsonCredentials();
+ json.remove("token_url");
+ ExternalAccountAuthorizedUserCredentials.fromStream(TestUtils.jsonToInputStream(json));
+ fail("Should not be able to continue without exception.");
+ } catch (IllegalStateException exception) {
+ assertEquals(
+ "ExternalAccountAuthorizedUserCredentials must be initialized with "
+ + "an access token or fields to enable refresh: "
+ + "('refresh_token', 'token_url', 'client_id', 'client_secret').",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ public void fromStream_missingClientId_throws() throws IOException {
+ try {
+ GenericJson json = buildJsonCredentials();
+ json.remove("client_id");
+ ExternalAccountAuthorizedUserCredentials.fromStream(TestUtils.jsonToInputStream(json));
+ fail("Should not be able to continue without exception.");
+ } catch (IllegalStateException exception) {
+ assertEquals(
+ "ExternalAccountAuthorizedUserCredentials must be initialized with "
+ + "an access token or fields to enable refresh: "
+ + "('refresh_token', 'token_url', 'client_id', 'client_secret').",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ public void fromStream_missingClientSecret_throws() throws IOException {
+ try {
+ GenericJson json = buildJsonCredentials();
+ json.remove("client_secret");
+ ExternalAccountAuthorizedUserCredentials.fromStream(TestUtils.jsonToInputStream(json));
+ fail("Should not be able to continue without exception.");
+ } catch (IllegalStateException exception) {
+ assertEquals(
+ "ExternalAccountAuthorizedUserCredentials must be initialized with "
+ + "an access token or fields to enable refresh: "
+ + "('refresh_token', 'token_url', 'client_id', 'client_secret').",
+ exception.getMessage());
+ }
+ }
+
+ @Test
+ public void fromStream_invalidInputStream_throws() throws IOException {
+ try {
+ GenericJson json = buildJsonCredentials();
+ json.put("audience", new HashMap<>());
+ ExternalAccountAuthorizedUserCredentials.fromStream(TestUtils.jsonToInputStream(json));
+ fail("Should not be able to continue without exception.");
+ } catch (CredentialFormatException e) {
+ assertEquals("Invalid input stream provided.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void createScoped_noChange() {
+ ExternalAccountAuthorizedUserCredentials externalAccountAuthorizedUserCredentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setTokenUrl(TOKEN_URL)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .build();
+ assertSame(
+ externalAccountAuthorizedUserCredentials,
+ externalAccountAuthorizedUserCredentials.createScoped(SCOPES));
+ }
+
+ @Test
+ public void createScopedRequired_false() {
+ ExternalAccountAuthorizedUserCredentials externalAccountAuthorizedUserCredentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setTokenUrl(TOKEN_URL)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .build();
+ assertFalse(externalAccountAuthorizedUserCredentials.createScopedRequired());
+ }
+
+ @Test
+ public void getRequestMetadata() throws IOException {
+ GoogleCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.fromJson(buildJsonCredentials(), transportFactory);
+
+ Map> metadata = credentials.getRequestMetadata(CALL_URI);
+
+ TestUtils.assertContainsBearerToken(metadata, transportFactory.transport.getAccessToken());
+ validateAuthHeader(transportFactory.transport.getRequest());
+ }
+
+ @Test
+ public void getRequestMetadata_withQuotaProjectId() throws IOException {
+ GoogleCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.fromJson(buildJsonCredentials(), transportFactory);
+
+ Map> metadata = credentials.getRequestMetadata(CALL_URI);
+
+ assertTrue(metadata.containsKey(GoogleCredentials.QUOTA_PROJECT_ID_HEADER_KEY));
+ assertEquals(
+ metadata.get(GoogleCredentials.QUOTA_PROJECT_ID_HEADER_KEY),
+ Collections.singletonList(QUOTA_PROJECT));
+
+ validateAuthHeader(transportFactory.transport.getRequest());
+ }
+
+ @Test
+ public void getRequestMetadata_withAccessToken() throws IOException {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setHttpTransportFactory(transportFactory)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .build();
+
+ Map> metadata = credentials.getRequestMetadata(CALL_URI);
+
+ TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN);
+ }
+
+ @Test
+ public void refreshAccessToken() throws IOException {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.fromJson(buildJsonCredentials(), transportFactory);
+
+ AccessToken accessToken = credentials.refreshAccessToken();
+
+ assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
+ validateAuthHeader(transportFactory.transport.getRequest());
+ }
+
+ @Test
+ public void refreshAccessToken_withRefreshTokenRotation() throws IOException {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.fromJson(buildJsonCredentials(), transportFactory);
+
+ transportFactory.transport.addRefreshTokenSequence("aNewRefreshToken");
+
+ AccessToken accessToken = credentials.refreshAccessToken();
+
+ assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
+ // Validate new refresh token was set.
+ assertEquals("aNewRefreshToken", credentials.getRefreshToken());
+ assertNotEquals(REFRESH_TOKEN, credentials.getRefreshToken());
+ validateAuthHeader(transportFactory.transport.getRequest());
+ }
+
+ @Test
+ public void refreshAccessToken_genericAuthError_throws() throws IOException {
+ transportFactory.transport.addResponseErrorSequence(
+ TestUtils.buildHttpResponseException(
+ "invalid_request", "Invalid request.", /* errorUri= */ null));
+
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.fromJson(buildJsonCredentials(), transportFactory);
+
+ try {
+ credentials.refreshAccessToken();
+ fail("");
+ } catch (OAuthException e) {
+ assertEquals("invalid_request", e.getErrorCode());
+ assertEquals("Invalid request.", e.getErrorDescription());
+ }
+ }
+
+ @Test(expected = IOException.class)
+ public void refreshAccessToken_genericIOError_throws() throws IOException {
+ transportFactory.transport.addResponseErrorSequence(new IOException(""));
+
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.fromJson(buildJsonCredentials(), transportFactory);
+
+ credentials.refreshAccessToken();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void refreshAccessToken_missingRefreshFields_throws() throws IOException {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setTokenUrl(TOKEN_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ credentials.refreshAccessToken();
+ }
+
+ @Test
+ public void hashCode_sameCredentials() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .build();
+
+ ExternalAccountAuthorizedUserCredentials secondCredentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .build();
+
+ assertEquals(credentials, secondCredentials);
+ assertEquals(secondCredentials, credentials);
+ assertEquals(credentials.hashCode(), secondCredentials.hashCode());
+ }
+
+ @Test
+ public void hashCode_differentCredentials() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .build();
+
+ // Second credentials have an access token set.
+ ExternalAccountAuthorizedUserCredentials secondCredentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .build();
+
+ assertNotEquals(secondCredentials, credentials);
+ assertNotEquals(credentials, secondCredentials);
+ assertNotEquals(credentials.hashCode(), secondCredentials.hashCode());
+ }
+
+ @Test
+ public void hashCode_differentCredentialsWithCredentialsFile() throws IOException {
+ // Optional fields that can be specified in the credentials file.
+ List fields = Arrays.asList("audience", "revoke_url", "quota_project_id");
+
+ // Credential initialized with all fields.
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.fromJson(buildJsonCredentials(), transportFactory);
+
+ for (String field : fields) {
+ // Build credential with one of these fields missing.
+ GenericJson json = buildJsonCredentials();
+ json.remove(field);
+
+ ExternalAccountAuthorizedUserCredentials secondCredentials =
+ ExternalAccountAuthorizedUserCredentials.fromJson(json, transportFactory);
+ assertNotEquals(secondCredentials, credentials);
+ assertNotEquals(credentials, secondCredentials);
+ assertNotEquals(credentials.hashCode(), secondCredentials.hashCode());
+ }
+ }
+
+ @Test
+ public void equals_differentCredentials() throws IOException {
+ UserCredentials userCredentials =
+ UserCredentials.newBuilder()
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setHttpTransportFactory(transportFactory)
+ .setTokenServerUri(URI.create(TOKEN_URL))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .build();
+
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.fromJson(buildJsonCredentials(), transportFactory);
+
+ assertNotEquals(userCredentials, credentials);
+ assertNotEquals(credentials, userCredentials);
+ }
+
+ @Test
+ public void equals_differentAudience() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ ExternalAccountAuthorizedUserCredentials secondCredentials =
+ credentials.toBuilder().setAudience("different").build();
+
+ assertNotEquals(secondCredentials, credentials);
+ assertNotEquals(credentials, secondCredentials);
+ assertNotEquals(credentials.hashCode(), secondCredentials.hashCode());
+ }
+
+ @Test
+ public void equals_differentClientId() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ ExternalAccountAuthorizedUserCredentials secondCredentials =
+ credentials.toBuilder().setClientId("different").build();
+
+ assertNotEquals(secondCredentials, credentials);
+ assertNotEquals(credentials, secondCredentials);
+ assertNotEquals(credentials.hashCode(), secondCredentials.hashCode());
+ }
+
+ @Test
+ public void equals_differentClientSecret() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ ExternalAccountAuthorizedUserCredentials secondCredentials =
+ credentials.toBuilder().setClientSecret("different").build();
+
+ assertNotEquals(secondCredentials, credentials);
+ assertNotEquals(credentials, secondCredentials);
+ assertNotEquals(credentials.hashCode(), secondCredentials.hashCode());
+ }
+
+ @Test
+ public void equals_differentRefreshToken() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ ExternalAccountAuthorizedUserCredentials secondCredentials =
+ credentials.toBuilder().setRefreshToken("different").build();
+
+ assertNotEquals(secondCredentials, credentials);
+ assertNotEquals(credentials, secondCredentials);
+ assertNotEquals(credentials.hashCode(), secondCredentials.hashCode());
+ }
+
+ @Test
+ public void equals_differentTokenUrl() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ ExternalAccountAuthorizedUserCredentials secondCredentials =
+ credentials.toBuilder().setTokenUrl("different").build();
+
+ assertNotEquals(secondCredentials, credentials);
+ assertNotEquals(credentials, secondCredentials);
+ assertNotEquals(credentials.hashCode(), secondCredentials.hashCode());
+ }
+
+ @Test
+ public void equals_differentTokenInfoUrl() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ ExternalAccountAuthorizedUserCredentials secondCredentials =
+ credentials.toBuilder().setTokenInfoUrl("different").build();
+
+ assertNotEquals(secondCredentials, credentials);
+ assertNotEquals(credentials, secondCredentials);
+ assertNotEquals(credentials.hashCode(), secondCredentials.hashCode());
+ }
+
+ @Test
+ public void equals_differentRevokeUrl() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ ExternalAccountAuthorizedUserCredentials secondCredentials =
+ credentials.toBuilder().setRevokeUrl("different").build();
+
+ assertNotEquals(secondCredentials, credentials);
+ assertNotEquals(credentials, secondCredentials);
+ assertNotEquals(credentials.hashCode(), secondCredentials.hashCode());
+ }
+
+ @Test
+ public void equals_differentAccessToken() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, new Date()))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ ExternalAccountAuthorizedUserCredentials secondCredentials =
+ credentials.toBuilder().setAccessToken(new AccessToken("different", new Date())).build();
+
+ assertNotEquals(secondCredentials, credentials);
+ assertNotEquals(credentials, secondCredentials);
+ assertNotEquals(credentials.hashCode(), secondCredentials.hashCode());
+ }
+
+ @Test
+ public void equals_differentQuotaProjectId() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ ExternalAccountAuthorizedUserCredentials secondCredentials =
+ credentials.toBuilder().setQuotaProjectId("different").build();
+
+ assertNotEquals(secondCredentials, credentials);
+ assertNotEquals(credentials, secondCredentials);
+ assertNotEquals(credentials.hashCode(), secondCredentials.hashCode());
+ }
+
+ @Test
+ public void equals_differentTransportFactory() {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ ExternalAccountAuthorizedUserCredentials secondCredentials =
+ credentials.toBuilder().setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY).build();
+
+ assertNotEquals(secondCredentials, credentials);
+ assertNotEquals(credentials, secondCredentials);
+ assertNotEquals(credentials.hashCode(), secondCredentials.hashCode());
+ }
+
+ @Test
+ public void toString_expectedFormat() {
+ AccessToken accessToken = new AccessToken(ACCESS_TOKEN, new Date());
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(accessToken)
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .setHttpTransportFactory(transportFactory)
+ .build();
+
+ String expectedToString =
+ String.format(
+ "ExternalAccountAuthorizedUserCredentials{requestMetadata=%s, temporaryAccess=%s, "
+ + "clientId=%s, clientSecret=%s, refreshToken=%s, tokenUrl=%s, tokenInfoUrl=%s, "
+ + "revokeUrl=%s, audience=%s, transportFactoryClassName=%s, quotaProjectId=%s}",
+ ImmutableMap.of(
+ AuthHttpConstants.AUTHORIZATION,
+ ImmutableList.of(OAuth2Utils.BEARER_PREFIX + accessToken.getTokenValue())),
+ accessToken,
+ CLIENT_ID,
+ CLIENT_SECRET,
+ REFRESH_TOKEN,
+ TOKEN_URL,
+ TOKEN_INFO_URL,
+ REVOKE_URL,
+ AUDIENCE,
+ transportFactory.getClass().getName(),
+ QUOTA_PROJECT);
+
+ assertEquals(expectedToString, credentials.toString());
+ }
+
+ @Test
+ public void serialize() throws IOException, ClassNotFoundException {
+ ExternalAccountAuthorizedUserCredentials credentials =
+ ExternalAccountAuthorizedUserCredentials.newBuilder()
+ .setAudience(AUDIENCE)
+ .setClientId(CLIENT_ID)
+ .setClientSecret(CLIENT_SECRET)
+ .setRefreshToken(REFRESH_TOKEN)
+ .setTokenUrl(TOKEN_URL)
+ .setTokenInfoUrl(TOKEN_INFO_URL)
+ .setRevokeUrl(REVOKE_URL)
+ .setAccessToken(new AccessToken(ACCESS_TOKEN, /* expirationTime= */ null))
+ .setQuotaProjectId(QUOTA_PROJECT)
+ .build();
+
+ ExternalAccountAuthorizedUserCredentials deserializedCredentials =
+ serializeAndDeserialize(credentials);
+ assertEquals(credentials, deserializedCredentials);
+ assertEquals(credentials.hashCode(), deserializedCredentials.hashCode());
+ assertEquals(credentials.toString(), deserializedCredentials.toString());
+ assertSame(deserializedCredentials.clock, Clock.SYSTEM);
+ }
+
+ static GenericJson buildJsonCredentials() {
+ GenericJson json = new GenericJson();
+ json.put("type", EXTERNAL_ACCOUNT_AUTHORIZED_USER_FILE_TYPE);
+ json.put("audience", AUDIENCE);
+ json.put("refresh_token", REFRESH_TOKEN);
+ json.put("client_id", CLIENT_ID);
+ json.put("client_secret", CLIENT_SECRET);
+ json.put("token_url", TOKEN_URL);
+ json.put("token_info_url", TOKEN_INFO_URL);
+ json.put("revoke_url", REVOKE_URL);
+ json.put("quota_project_id", QUOTA_PROJECT);
+ return json;
+ }
+
+ private static void validateAuthHeader(MockLowLevelHttpRequest request) {
+ Map> headers = request.getHeaders();
+ List authHeader = headers.get("authorization");
+
+ assertEquals(BASIC_AUTH, authHeader.iterator().next());
+ assertEquals(1, authHeader.size());
+ }
+}
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
index 2c311c481..295fdc663 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java
@@ -42,6 +42,7 @@
import com.google.api.client.testing.http.MockHttpTransport;
import com.google.auth.TestUtils;
import com.google.auth.http.HttpTransportFactory;
+import com.google.auth.oauth2.ExternalAccountAuthorizedUserCredentialsTest.MockExternalAccountAuthorizedUserCredentialsTransportFactory;
import com.google.auth.oauth2.IdentityPoolCredentialsTest.MockExternalAccountCredentialsTransportFactory;
import com.google.auth.oauth2.ImpersonatedCredentialsTest.MockIAMCredentialsServiceTransportFactory;
import com.google.common.collect.ImmutableList;
@@ -468,6 +469,21 @@ public void fromStream_pluggableAuthCredentials_providesToken() throws IOExcepti
TestUtils.assertContainsBearerToken(metadata, transportFactory.transport.getAccessToken());
}
+ @Test
+ public void fromStream_externalAccountAuthorizedUserCredentials_providesToken()
+ throws IOException {
+ MockExternalAccountAuthorizedUserCredentialsTransportFactory transportFactory =
+ new MockExternalAccountAuthorizedUserCredentialsTransportFactory();
+ InputStream stream =
+ TestUtils.jsonToInputStream(
+ ExternalAccountAuthorizedUserCredentialsTest.buildJsonCredentials());
+
+ GoogleCredentials credentials = GoogleCredentials.fromStream(stream, transportFactory);
+
+ Map> metadata = credentials.getRequestMetadata(CALL_URI);
+ TestUtils.assertContainsBearerToken(metadata, transportFactory.transport.getAccessToken());
+ }
+
@Test
public void fromStream_Impersonation_providesToken_WithQuotaProject() throws IOException {
MockTokenServerTransportFactory transportFactoryForSource =
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java
index 1695c8450..8251be13c 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java
@@ -59,6 +59,7 @@ public final class MockStsTransport extends MockHttpTransport {
"urn:ietf:params:oauth:grant-type:token-exchange";
private static final String ISSUED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
private static final String STS_URL = "https://sts.googleapis.com/v1/token";
+ private static final String STS_OAUTHTOKEN_URL = "https://sts.googleapis.com/v1/oauthtoken";
private static final String ACCESS_TOKEN = "accessToken";
private static final String TOKEN_TYPE = "Bearer";
private static final Long EXPIRES_IN = 3600L;
@@ -88,7 +89,7 @@ public LowLevelHttpRequest buildRequest(final String method, final String url) {
new MockLowLevelHttpRequest(url) {
@Override
public LowLevelHttpResponse execute() throws IOException {
- if (!STS_URL.equals(url)) {
+ if (!STS_URL.equals(url) && !STS_OAUTHTOKEN_URL.equals(url)) {
return makeErrorResponse();
}
@@ -96,27 +97,38 @@ public LowLevelHttpResponse execute() throws IOException {
throw responseErrorSequence.poll();
}
- Map query = TestUtils.parseQuery(getContentAsString());
- assertEquals(EXPECTED_GRANT_TYPE, query.get("grant_type"));
- assertNotNull(query.get("subject_token_type"));
- assertNotNull(query.get("subject_token"));
-
GenericJson response = new GenericJson();
response.setFactory(new GsonFactory());
- response.put("token_type", TOKEN_TYPE);
- response.put("access_token", ACCESS_TOKEN);
- response.put("issued_token_type", ISSUED_TOKEN_TYPE);
- if (returnExpiresIn) {
+ Map query = TestUtils.parseQuery(getContentAsString());
+ if (STS_URL.equals(url)) {
+ assertEquals(EXPECTED_GRANT_TYPE, query.get("grant_type"));
+ assertNotNull(query.get("subject_token_type"));
+ assertNotNull(query.get("subject_token"));
+
+ response.put("token_type", TOKEN_TYPE);
+ response.put("access_token", ACCESS_TOKEN);
+ response.put("issued_token_type", ISSUED_TOKEN_TYPE);
+
+ if (returnExpiresIn) {
+ response.put("expires_in", EXPIRES_IN);
+ }
+ if (!refreshTokenSequence.isEmpty()) {
+ response.put("refresh_token", refreshTokenSequence.poll());
+ }
+ if (!scopeSequence.isEmpty()) {
+ response.put("scope", Joiner.on(' ').join(scopeSequence.poll()));
+ }
+ } else {
+ assertEquals("refresh_token", query.get("grant_type"));
+
+ response.put("access_token", ACCESS_TOKEN);
response.put("expires_in", EXPIRES_IN);
- }
- if (!refreshTokenSequence.isEmpty()) {
- response.put("refresh_token", refreshTokenSequence.poll());
- }
- if (!scopeSequence.isEmpty()) {
- response.put("scope", Joiner.on(' ').join(scopeSequence.poll()));
- }
+ if (!refreshTokenSequence.isEmpty()) {
+ response.put("refresh_token", refreshTokenSequence.poll());
+ }
+ }
return new MockLowLevelHttpResponse()
.setContentType(Json.MEDIA_TYPE)
.setContent(response.toPrettyString());
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java b/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java
index f864f4791..84e522a73 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/OAuthExceptionTest.java
@@ -34,6 +34,8 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
+import com.google.auth.TestUtils;
+import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -84,4 +86,50 @@ public void getMessage_baseFormat() {
String expectedMessage = String.format(BASE_MESSAGE_FORMAT, "errorCode");
assertEquals(expectedMessage, e.getMessage());
}
+
+ @Test
+ public void createFromHttpResponseException() throws IOException {
+ OAuthException e =
+ OAuthException.createFromHttpResponseException(
+ TestUtils.buildHttpResponseException("errorCode", "errorDescription", "errorUri"));
+
+ assertEquals("errorCode", e.getErrorCode());
+ assertEquals("errorDescription", e.getErrorDescription());
+ assertEquals("errorUri", e.getErrorUri());
+
+ String expectedMessage =
+ String.format(FULL_MESSAGE_FORMAT, "errorCode", "errorDescription", "errorUri");
+ assertEquals(expectedMessage, e.getMessage());
+ }
+
+ @Test
+ public void createFromHttpResponseException_descriptionFormat() throws IOException {
+ OAuthException e =
+ OAuthException.createFromHttpResponseException(
+ TestUtils.buildHttpResponseException(
+ "errorCode", "errorDescription", /* errorUri= */ null));
+
+ assertEquals("errorCode", e.getErrorCode());
+ assertEquals("errorDescription", e.getErrorDescription());
+ assertNull(e.getErrorUri());
+
+ String expectedMessage =
+ String.format(ERROR_DESCRIPTION_FORMAT, "errorCode", "errorDescription");
+ assertEquals(expectedMessage, e.getMessage());
+ }
+
+ @Test
+ public void createFromHttpResponseException_baseFormat() throws IOException {
+ OAuthException e =
+ OAuthException.createFromHttpResponseException(
+ TestUtils.buildHttpResponseException(
+ "errorCode", /* errorDescription= */ null, /* errorUri= */ null));
+
+ assertEquals("errorCode", e.getErrorCode());
+ assertNull(e.getErrorDescription());
+ assertNull(e.getErrorUri());
+
+ String expectedMessage = String.format(BASE_MESSAGE_FORMAT, "errorCode");
+ assertEquals(expectedMessage, e.getMessage());
+ }
}