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