com.google.auth.oauth2.DownscopedCredentials Maven / Gradle / Ivy
/*
* Copyright 2021 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.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.auth.Credentials;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
/**
* DownscopedCredentials enables the ability to downscope, or restrict, the Identity and Access
* Management (IAM) permissions that a short-lived credential can use for Cloud Storage.
*
* To downscope permissions you must define a {@link CredentialAccessBoundary} which specifies
* the upper bound of permissions that the credential can access. You must also provide a source
* credential which will be used to acquire the downscoped credential.
*
*
See for more
* information.
*
*
Usage:
*
*
* GoogleCredentials sourceCredentials = GoogleCredentials.getApplicationDefault()
* .createScoped("https://www.googleapis.com/auth/cloud-platform");
*
* CredentialAccessBoundary.AccessBoundaryRule rule =
* CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
* .setAvailableResource(
* "//storage.googleapis.com/projects/_/buckets/bucket")
* .addAvailablePermission("inRole:roles/storage.objectViewer")
* .build();
*
* DownscopedCredentials downscopedCredentials =
* DownscopedCredentials.newBuilder()
* .setSourceCredential(sourceCredentials)
* .setCredentialAccessBoundary(
* CredentialAccessBoundary.newBuilder().addRule(rule).build())
* .build();
*
* AccessToken accessToken = downscopedCredentials.refreshAccessToken();
*
* OAuth2Credentials credentials = OAuth2Credentials.create(accessToken);
*
* Storage storage =
* StorageOptions.newBuilder().setCredentials(credentials).build().getService();
*
* Blob blob = storage.get(BlobId.of("bucket", "object"));
* System.out.printf("Blob %s retrieved.", blob.getBlobId());
*
*
* Note that {@link OAuth2CredentialsWithRefresh} can instead be used to consume the downscoped
* token, allowing for automatic token refreshes by providing a {@link
* OAuth2CredentialsWithRefresh.OAuth2RefreshHandler}.
*/
public final class DownscopedCredentials extends OAuth2Credentials {
private final String TOKEN_EXCHANGE_URL_FORMAT = "https://sts.{universe_domain}/v1/token";
private final GoogleCredentials sourceCredential;
private final CredentialAccessBoundary credentialAccessBoundary;
private final String universeDomain;
private final transient HttpTransportFactory transportFactory;
private final String tokenExchangeEndpoint;
/** Internal constructor. See {@link Builder}. */
private DownscopedCredentials(Builder builder) {
this.transportFactory =
firstNonNull(
builder.transportFactory,
getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
this.sourceCredential = checkNotNull(builder.sourceCredential);
this.credentialAccessBoundary = checkNotNull(builder.credentialAccessBoundary);
// Default to GDU when not supplied.
if (builder.universeDomain == null || builder.universeDomain.trim().isEmpty()) {
this.universeDomain = Credentials.GOOGLE_DEFAULT_UNIVERSE;
} else {
this.universeDomain = builder.universeDomain;
}
// Ensure source credential's universe domain matches.
try {
if (!this.universeDomain.equals(sourceCredential.getUniverseDomain())) {
throw new IllegalArgumentException(
"The downscoped credential's universe domain must be the same as the source "
+ "credential.");
}
} catch (IOException e) {
// Throwing an IOException would be a breaking change, so wrap it here.
throw new IllegalStateException(
"Error occurred when attempting to retrieve source credential universe domain.", e);
}
this.tokenExchangeEndpoint =
TOKEN_EXCHANGE_URL_FORMAT.replace("{universe_domain}", universeDomain);
}
@Override
public AccessToken refreshAccessToken() throws IOException {
try {
this.sourceCredential.refreshIfExpired();
} catch (IOException e) {
throw new IOException("Unable to refresh the provided source credential.", e);
}
StsTokenExchangeRequest request =
StsTokenExchangeRequest.newBuilder(
sourceCredential.getAccessToken().getTokenValue(),
OAuth2Utils.TOKEN_TYPE_ACCESS_TOKEN)
.setRequestTokenType(OAuth2Utils.TOKEN_TYPE_ACCESS_TOKEN)
.build();
StsRequestHandler handler =
StsRequestHandler.newBuilder(
tokenExchangeEndpoint, request, transportFactory.create().createRequestFactory())
.setInternalOptions(credentialAccessBoundary.toJson())
.build();
AccessToken downscopedAccessToken = handler.exchangeToken().getAccessToken();
// The STS endpoint will only return the expiration time for the downscoped token if the
// original access token represents a service account.
// The downscoped token's expiration time will always match the source credential expiration.
// When no expires_in is returned, we can copy the source credential's expiration time.
if (downscopedAccessToken.getExpirationTime() == null) {
AccessToken sourceAccessToken = this.sourceCredential.getAccessToken();
if (sourceAccessToken.getExpirationTime() != null) {
return new AccessToken(
downscopedAccessToken.getTokenValue(), sourceAccessToken.getExpirationTime());
}
}
return downscopedAccessToken;
}
public GoogleCredentials getSourceCredentials() {
return sourceCredential;
}
public CredentialAccessBoundary getCredentialAccessBoundary() {
return credentialAccessBoundary;
}
/**
* Returns the universe domain for the credential.
*
* @return An explicit universe domain if it was explicitly provided, otherwise the default Google
* universe will be returned.
*/
@Override
public String getUniverseDomain() {
return universeDomain;
}
@VisibleForTesting
HttpTransportFactory getTransportFactory() {
return transportFactory;
}
public static Builder newBuilder() {
return new Builder();
}
public static class Builder extends OAuth2Credentials.Builder {
private GoogleCredentials sourceCredential;
private CredentialAccessBoundary credentialAccessBoundary;
private HttpTransportFactory transportFactory;
private String universeDomain;
private Builder() {}
/**
* Sets the required source credential used to acquire the downscoped credential.
*
* @param sourceCredential the {@code GoogleCredentials} to set
* @return this {@code Builder} object
*/
@CanIgnoreReturnValue
public Builder setSourceCredential(GoogleCredentials sourceCredential) {
this.sourceCredential = sourceCredential;
return this;
}
/**
* Sets the required credential access boundary which specifies the upper bound of permissions
* that the credential can access. See {@link CredentialAccessBoundary} for more information.
*
* @param credentialAccessBoundary the {@code CredentialAccessBoundary} to set
* @return this {@code Builder} object
*/
@CanIgnoreReturnValue
public Builder setCredentialAccessBoundary(CredentialAccessBoundary credentialAccessBoundary) {
this.credentialAccessBoundary = credentialAccessBoundary;
return this;
}
/**
* Sets the HTTP transport factory.
*
* @param transportFactory the {@code HttpTransportFactory} to set
* @return this {@code Builder} object
*/
@CanIgnoreReturnValue
public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
this.transportFactory = transportFactory;
return this;
}
/**
* Sets the optional universe domain.
*
* @param universeDomain the universe domain to set
* @return this {@code Builder} object
*/
@CanIgnoreReturnValue
public Builder setUniverseDomain(String universeDomain) {
this.universeDomain = universeDomain;
return this;
}
@Override
public DownscopedCredentials build() {
return new DownscopedCredentials(this);
}
}
}