• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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