/* * 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 com.google.errorprone.annotations.CanIgnoreReturnValue; 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(),
getAccessToken(),
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.getAccessToken(), credentials.getAccessToken())
&& 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);
}
@Override
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