• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2022 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 com.google.auth.http.HttpTransportFactory;
35 import com.google.auth.oauth2.ExecutableHandler.ExecutableOptions;
36 import com.google.common.annotations.VisibleForTesting;
37 import com.google.errorprone.annotations.CanIgnoreReturnValue;
38 import java.io.IOException;
39 import java.util.ArrayList;
40 import java.util.Collection;
41 import java.util.HashMap;
42 import java.util.Map;
43 import javax.annotation.Nullable;
44 
45 /**
46  * PluggableAuthCredentials enables the exchange of workload identity pool external credentials for
47  * Google access tokens by retrieving 3rd party tokens through a user supplied executable. These
48  * scripts/executables are completely independent of the Google Cloud Auth libraries. These
49  * credentials plug into ADC and will call the specified executable to retrieve the 3rd party token
50  * to be exchanged for a Google access token.
51  *
52  * <p>To use these credentials, the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment variable
53  * must be set to '1'. This is for security reasons.
54  *
55  * <p>Both OIDC and SAML are supported. The executable must adhere to a specific response format
56  * defined below.
57  *
58  * <p>The executable must print out the 3rd party token to STDOUT in JSON format. When an
59  * output_file is specified in the credential configuration, the executable must also handle writing
60  * the JSON response to this file.
61  *
62  * <pre>
63  * OIDC response sample:
64  * {
65  *   "version": 1,
66  *   "success": true,
67  *   "token_type": "urn:ietf:params:oauth:token-type:id_token",
68  *   "id_token": "HEADER.PAYLOAD.SIGNATURE",
69  *   "expiration_time": 1620433341
70  * }
71  *
72  * SAML2 response sample:
73  * {
74  *   "version": 1,
75  *   "success": true,
76  *   "token_type": "urn:ietf:params:oauth:token-type:saml2",
77  *   "saml_response": "...",
78  *   "expiration_time": 1620433341
79  * }
80  *
81  * Error response sample:
82  * {
83  *   "version": 1,
84  *   "success": false,
85  *   "code": "401",
86  *   "message": "Error message."
87  * }
88  * </pre>
89  *
90  * <p>The `expiration_time` field in the JSON response is only required for successful responses
91  * when an output file was specified in the credential configuration.
92  *
93  * <p>The auth libraries will populate certain environment variables that will be accessible by the
94  * executable, such as: GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE, GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE,
95  * GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE, GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL, and
96  * GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE.
97  *
98  * <p>Please see this repositories README for a complete executable request/response specification.
99  */
100 public class PluggableAuthCredentials extends ExternalAccountCredentials {
101 
102   static final String PLUGGABLE_AUTH_METRICS_HEADER_VALUE = "executable";
103 
104   private final PluggableAuthCredentialSource config;
105 
106   private final ExecutableHandler handler;
107 
108   /** Internal constructor. See {@link Builder}. */
PluggableAuthCredentials(Builder builder)109   PluggableAuthCredentials(Builder builder) {
110     super(builder);
111     this.config = (PluggableAuthCredentialSource) builder.credentialSource;
112 
113     if (builder.handler != null) {
114       handler = builder.handler;
115     } else {
116       handler = new PluggableAuthHandler(getEnvironmentProvider());
117     }
118   }
119 
120   @Override
refreshAccessToken()121   public AccessToken refreshAccessToken() throws IOException {
122     String credential = retrieveSubjectToken();
123     StsTokenExchangeRequest.Builder stsTokenExchangeRequest =
124         StsTokenExchangeRequest.newBuilder(credential, getSubjectTokenType())
125             .setAudience(getAudience());
126 
127     Collection<String> scopes = getScopes();
128     if (scopes != null && !scopes.isEmpty()) {
129       stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes));
130     }
131     return exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest.build());
132   }
133 
134   /**
135    * Returns the 3rd party subject token by calling the executable specified in the credential
136    * source.
137    *
138    * @throws IOException if an error occurs with the executable execution.
139    */
140   @Override
retrieveSubjectToken()141   public String retrieveSubjectToken() throws IOException {
142     String executableCommand = config.getCommand();
143     String outputFilePath = config.getOutputFilePath();
144     int executableTimeoutMs = config.getTimeoutMs();
145 
146     Map<String, String> envMap = new HashMap<>();
147     envMap.put("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE", getAudience());
148     envMap.put("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE", getSubjectTokenType());
149     // Always set to 0 for Workload Identity Federation.
150     envMap.put("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE", "0");
151     if (getServiceAccountEmail() != null) {
152       envMap.put("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL", getServiceAccountEmail());
153     }
154     if (outputFilePath != null && !outputFilePath.isEmpty()) {
155       envMap.put("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE", outputFilePath);
156     }
157 
158     ExecutableOptions options =
159         new ExecutableOptions() {
160           @Override
161           public String getExecutableCommand() {
162             return executableCommand;
163           }
164 
165           @Override
166           public Map<String, String> getEnvironmentMap() {
167             return envMap;
168           }
169 
170           @Override
171           public int getExecutableTimeoutMs() {
172             return executableTimeoutMs;
173           }
174 
175           @Nullable
176           @Override
177           public String getOutputFilePath() {
178             return outputFilePath;
179           }
180         };
181 
182     // Delegate handling of the executable to the handler.
183     return this.handler.retrieveTokenFromExecutable(options);
184   }
185 
186   /** Clones the PluggableAuthCredentials with the specified scopes. */
187   @Override
createScoped(Collection<String> newScopes)188   public PluggableAuthCredentials createScoped(Collection<String> newScopes) {
189     return new PluggableAuthCredentials(
190         (PluggableAuthCredentials.Builder) newBuilder(this).setScopes(newScopes));
191   }
192 
193   @Override
getCredentialSourceType()194   String getCredentialSourceType() {
195     return PLUGGABLE_AUTH_METRICS_HEADER_VALUE;
196   }
197 
newBuilder()198   public static Builder newBuilder() {
199     return new Builder();
200   }
201 
newBuilder(PluggableAuthCredentials pluggableAuthCredentials)202   public static Builder newBuilder(PluggableAuthCredentials pluggableAuthCredentials) {
203     return new Builder(pluggableAuthCredentials);
204   }
205 
206   @VisibleForTesting
207   @Nullable
getExecutableHandler()208   ExecutableHandler getExecutableHandler() {
209     return this.handler;
210   }
211 
212   public static class Builder extends ExternalAccountCredentials.Builder {
213 
214     private ExecutableHandler handler;
215 
Builder()216     Builder() {}
217 
Builder(PluggableAuthCredentials credentials)218     Builder(PluggableAuthCredentials credentials) {
219       super(credentials);
220       this.handler = credentials.handler;
221     }
222 
223     @CanIgnoreReturnValue
setExecutableHandler(ExecutableHandler handler)224     public Builder setExecutableHandler(ExecutableHandler handler) {
225       this.handler = handler;
226       return this;
227     }
228 
229     @CanIgnoreReturnValue
setHttpTransportFactory(HttpTransportFactory transportFactory)230     public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
231       super.setHttpTransportFactory(transportFactory);
232       return this;
233     }
234 
235     @CanIgnoreReturnValue
setAudience(String audience)236     public Builder setAudience(String audience) {
237       super.setAudience(audience);
238       return this;
239     }
240 
241     @CanIgnoreReturnValue
setSubjectTokenType(String subjectTokenType)242     public Builder setSubjectTokenType(String subjectTokenType) {
243       super.setSubjectTokenType(subjectTokenType);
244       return this;
245     }
246 
247     @CanIgnoreReturnValue
setSubjectTokenType(SubjectTokenTypes subjectTokenType)248     public Builder setSubjectTokenType(SubjectTokenTypes subjectTokenType) {
249       super.setSubjectTokenType(subjectTokenType);
250       return this;
251     }
252 
253     @CanIgnoreReturnValue
setTokenUrl(String tokenUrl)254     public Builder setTokenUrl(String tokenUrl) {
255       super.setTokenUrl(tokenUrl);
256       return this;
257     }
258 
259     @CanIgnoreReturnValue
setCredentialSource(PluggableAuthCredentialSource credentialSource)260     public Builder setCredentialSource(PluggableAuthCredentialSource credentialSource) {
261       super.setCredentialSource(credentialSource);
262       return this;
263     }
264 
265     @CanIgnoreReturnValue
setServiceAccountImpersonationUrl(String serviceAccountImpersonationUrl)266     public Builder setServiceAccountImpersonationUrl(String serviceAccountImpersonationUrl) {
267       super.setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl);
268       return this;
269     }
270 
271     @CanIgnoreReturnValue
setTokenInfoUrl(String tokenInfoUrl)272     public Builder setTokenInfoUrl(String tokenInfoUrl) {
273       super.setTokenInfoUrl(tokenInfoUrl);
274       return this;
275     }
276 
277     @CanIgnoreReturnValue
setQuotaProjectId(String quotaProjectId)278     public Builder setQuotaProjectId(String quotaProjectId) {
279       super.setQuotaProjectId(quotaProjectId);
280       return this;
281     }
282 
283     @CanIgnoreReturnValue
setClientId(String clientId)284     public Builder setClientId(String clientId) {
285       super.setClientId(clientId);
286       return this;
287     }
288 
289     @CanIgnoreReturnValue
setClientSecret(String clientSecret)290     public Builder setClientSecret(String clientSecret) {
291       super.setClientSecret(clientSecret);
292       return this;
293     }
294 
295     @CanIgnoreReturnValue
setScopes(Collection<String> scopes)296     public Builder setScopes(Collection<String> scopes) {
297       super.setScopes(scopes);
298       return this;
299     }
300 
301     @CanIgnoreReturnValue
setWorkforcePoolUserProject(String workforcePoolUserProject)302     public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) {
303       super.setWorkforcePoolUserProject(workforcePoolUserProject);
304       return this;
305     }
306 
307     @CanIgnoreReturnValue
setServiceAccountImpersonationOptions(Map<String, Object> optionsMap)308     public Builder setServiceAccountImpersonationOptions(Map<String, Object> optionsMap) {
309       super.setServiceAccountImpersonationOptions(optionsMap);
310       return this;
311     }
312 
313     @CanIgnoreReturnValue
setUniverseDomain(String universeDomain)314     public Builder setUniverseDomain(String universeDomain) {
315       super.setUniverseDomain(universeDomain);
316       return this;
317     }
318 
319     @CanIgnoreReturnValue
setEnvironmentProvider(EnvironmentProvider environmentProvider)320     Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) {
321       super.setEnvironmentProvider(environmentProvider);
322       return this;
323     }
324 
325     @Override
build()326     public PluggableAuthCredentials build() {
327       return new PluggableAuthCredentials(this);
328     }
329   }
330 }
331