1 /* 2 * Copyright 2021 Google LLC 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * 15 * * Neither the name of Google LLC nor the names of its 16 * contributors may be used to endorse or promote products derived from 17 * this software without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 package com.google.auth.oauth2; 33 34 import static com.google.common.base.MoreObjects.firstNonNull; 35 import static com.google.common.base.Preconditions.checkNotNull; 36 37 import com.google.auth.Credentials; 38 import com.google.auth.http.HttpTransportFactory; 39 import com.google.common.annotations.VisibleForTesting; 40 import com.google.errorprone.annotations.CanIgnoreReturnValue; 41 import java.io.IOException; 42 43 /** 44 * DownscopedCredentials enables the ability to downscope, or restrict, the Identity and Access 45 * Management (IAM) permissions that a short-lived credential can use for Cloud Storage. 46 * 47 * <p>To downscope permissions you must define a {@link CredentialAccessBoundary} which specifies 48 * the upper bound of permissions that the credential can access. You must also provide a source 49 * credential which will be used to acquire the downscoped credential. 50 * 51 * <p>See <a href='https://cloud.google.com/iam/docs/downscoping-short-lived-credentials'>for more 52 * information.</a> 53 * 54 * <p>Usage: 55 * 56 * <pre><code> 57 * GoogleCredentials sourceCredentials = GoogleCredentials.getApplicationDefault() 58 * .createScoped("https://www.googleapis.com/auth/cloud-platform"); 59 * 60 * CredentialAccessBoundary.AccessBoundaryRule rule = 61 * CredentialAccessBoundary.AccessBoundaryRule.newBuilder() 62 * .setAvailableResource( 63 * "//storage.googleapis.com/projects/_/buckets/bucket") 64 * .addAvailablePermission("inRole:roles/storage.objectViewer") 65 * .build(); 66 * 67 * DownscopedCredentials downscopedCredentials = 68 * DownscopedCredentials.newBuilder() 69 * .setSourceCredential(sourceCredentials) 70 * .setCredentialAccessBoundary( 71 * CredentialAccessBoundary.newBuilder().addRule(rule).build()) 72 * .build(); 73 * 74 * AccessToken accessToken = downscopedCredentials.refreshAccessToken(); 75 * 76 * OAuth2Credentials credentials = OAuth2Credentials.create(accessToken); 77 * 78 * Storage storage = 79 * StorageOptions.newBuilder().setCredentials(credentials).build().getService(); 80 * 81 * Blob blob = storage.get(BlobId.of("bucket", "object")); 82 * System.out.printf("Blob %s retrieved.", blob.getBlobId()); 83 * </code></pre> 84 * 85 * Note that {@link OAuth2CredentialsWithRefresh} can instead be used to consume the downscoped 86 * token, allowing for automatic token refreshes by providing a {@link 87 * OAuth2CredentialsWithRefresh.OAuth2RefreshHandler}. 88 */ 89 public final class DownscopedCredentials extends OAuth2Credentials { 90 91 private final String TOKEN_EXCHANGE_URL_FORMAT = "https://sts.{universe_domain}/v1/token"; 92 private final GoogleCredentials sourceCredential; 93 private final CredentialAccessBoundary credentialAccessBoundary; 94 private final String universeDomain; 95 96 private final transient HttpTransportFactory transportFactory; 97 98 private final String tokenExchangeEndpoint; 99 100 /** Internal constructor. See {@link Builder}. */ DownscopedCredentials(Builder builder)101 private DownscopedCredentials(Builder builder) { 102 this.transportFactory = 103 firstNonNull( 104 builder.transportFactory, 105 getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY)); 106 this.sourceCredential = checkNotNull(builder.sourceCredential); 107 this.credentialAccessBoundary = checkNotNull(builder.credentialAccessBoundary); 108 109 // Default to GDU when not supplied. 110 if (builder.universeDomain == null || builder.universeDomain.trim().isEmpty()) { 111 this.universeDomain = Credentials.GOOGLE_DEFAULT_UNIVERSE; 112 } else { 113 this.universeDomain = builder.universeDomain; 114 } 115 116 // Ensure source credential's universe domain matches. 117 try { 118 if (!this.universeDomain.equals(sourceCredential.getUniverseDomain())) { 119 throw new IllegalArgumentException( 120 "The downscoped credential's universe domain must be the same as the source " 121 + "credential."); 122 } 123 } catch (IOException e) { 124 // Throwing an IOException would be a breaking change, so wrap it here. 125 throw new IllegalStateException( 126 "Error occurred when attempting to retrieve source credential universe domain.", e); 127 } 128 this.tokenExchangeEndpoint = 129 TOKEN_EXCHANGE_URL_FORMAT.replace("{universe_domain}", universeDomain); 130 } 131 132 @Override refreshAccessToken()133 public AccessToken refreshAccessToken() throws IOException { 134 try { 135 this.sourceCredential.refreshIfExpired(); 136 } catch (IOException e) { 137 throw new IOException("Unable to refresh the provided source credential.", e); 138 } 139 140 StsTokenExchangeRequest request = 141 StsTokenExchangeRequest.newBuilder( 142 sourceCredential.getAccessToken().getTokenValue(), 143 OAuth2Utils.TOKEN_TYPE_ACCESS_TOKEN) 144 .setRequestTokenType(OAuth2Utils.TOKEN_TYPE_ACCESS_TOKEN) 145 .build(); 146 147 StsRequestHandler handler = 148 StsRequestHandler.newBuilder( 149 tokenExchangeEndpoint, request, transportFactory.create().createRequestFactory()) 150 .setInternalOptions(credentialAccessBoundary.toJson()) 151 .build(); 152 153 AccessToken downscopedAccessToken = handler.exchangeToken().getAccessToken(); 154 155 // The STS endpoint will only return the expiration time for the downscoped token if the 156 // original access token represents a service account. 157 // The downscoped token's expiration time will always match the source credential expiration. 158 // When no expires_in is returned, we can copy the source credential's expiration time. 159 if (downscopedAccessToken.getExpirationTime() == null) { 160 AccessToken sourceAccessToken = this.sourceCredential.getAccessToken(); 161 if (sourceAccessToken.getExpirationTime() != null) { 162 return new AccessToken( 163 downscopedAccessToken.getTokenValue(), sourceAccessToken.getExpirationTime()); 164 } 165 } 166 return downscopedAccessToken; 167 } 168 getSourceCredentials()169 public GoogleCredentials getSourceCredentials() { 170 return sourceCredential; 171 } 172 getCredentialAccessBoundary()173 public CredentialAccessBoundary getCredentialAccessBoundary() { 174 return credentialAccessBoundary; 175 } 176 177 /** 178 * Returns the universe domain for the credential. 179 * 180 * @return An explicit universe domain if it was explicitly provided, otherwise the default Google 181 * universe will be returned. 182 */ 183 @Override getUniverseDomain()184 public String getUniverseDomain() { 185 return universeDomain; 186 } 187 188 @VisibleForTesting getTransportFactory()189 HttpTransportFactory getTransportFactory() { 190 return transportFactory; 191 } 192 newBuilder()193 public static Builder newBuilder() { 194 return new Builder(); 195 } 196 197 public static class Builder extends OAuth2Credentials.Builder { 198 199 private GoogleCredentials sourceCredential; 200 private CredentialAccessBoundary credentialAccessBoundary; 201 private HttpTransportFactory transportFactory; 202 private String universeDomain; 203 Builder()204 private Builder() {} 205 206 /** 207 * Sets the required source credential used to acquire the downscoped credential. 208 * 209 * @param sourceCredential the {@code GoogleCredentials} to set 210 * @return this {@code Builder} object 211 */ 212 @CanIgnoreReturnValue setSourceCredential(GoogleCredentials sourceCredential)213 public Builder setSourceCredential(GoogleCredentials sourceCredential) { 214 this.sourceCredential = sourceCredential; 215 return this; 216 } 217 218 /** 219 * Sets the required credential access boundary which specifies the upper bound of permissions 220 * that the credential can access. See {@link CredentialAccessBoundary} for more information. 221 * 222 * @param credentialAccessBoundary the {@code CredentialAccessBoundary} to set 223 * @return this {@code Builder} object 224 */ 225 @CanIgnoreReturnValue setCredentialAccessBoundary(CredentialAccessBoundary credentialAccessBoundary)226 public Builder setCredentialAccessBoundary(CredentialAccessBoundary credentialAccessBoundary) { 227 this.credentialAccessBoundary = credentialAccessBoundary; 228 return this; 229 } 230 231 /** 232 * Sets the HTTP transport factory. 233 * 234 * @param transportFactory the {@code HttpTransportFactory} to set 235 * @return this {@code Builder} object 236 */ 237 @CanIgnoreReturnValue setHttpTransportFactory(HttpTransportFactory transportFactory)238 public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { 239 this.transportFactory = transportFactory; 240 return this; 241 } 242 243 /** 244 * Sets the optional universe domain. 245 * 246 * @param universeDomain the universe domain to set 247 * @return this {@code Builder} object 248 */ 249 @CanIgnoreReturnValue setUniverseDomain(String universeDomain)250 public Builder setUniverseDomain(String universeDomain) { 251 this.universeDomain = universeDomain; 252 return this; 253 } 254 255 @Override build()256 public DownscopedCredentials build() { 257 return new DownscopedCredentials(this); 258 } 259 } 260 } 261