source: vendor/google/auth/src/CredentialSource/ExecutableSource.php@ f9c482b

Last change on this file since f9c482b was f9c482b, checked in by Vlado 222039 <vlado.popovski@…>, 7 days ago

Upload new project files

  • Property mode set to 100644
File size: 10.7 KB
RevLine 
[f9c482b]1<?php
2/*
3 * Copyright 2024 Google Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18namespace Google\Auth\CredentialSource;
19
20use Google\Auth\ExecutableHandler\ExecutableHandler;
21use Google\Auth\ExecutableHandler\ExecutableResponseError;
22use Google\Auth\ExternalAccountCredentialSourceInterface;
23use RuntimeException;
24
25/**
26 * ExecutableSource enables the exchange of workload identity pool external credentials for
27 * Google access tokens by retrieving 3rd party tokens through a user supplied executable. These
28 * scripts/executables are completely independent of the Google Cloud Auth libraries. These
29 * credentials plug into ADC and will call the specified executable to retrieve the 3rd party token
30 * to be exchanged for a Google access token.
31 *
32 * To use these credentials, the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment variable
33 * must be set to '1'. This is for security reasons.
34 *
35 * Both OIDC and SAML are supported. The executable must adhere to a specific response format
36 * defined below.
37 *
38 * The executable must print out the 3rd party token to STDOUT in JSON format. When an
39 * output_file is specified in the credential configuration, the executable must also handle writing the
40 * JSON response to this file.
41 *
42 * <pre>
43 * OIDC response sample:
44 * {
45 * "version": 1,
46 * "success": true,
47 * "token_type": "urn:ietf:params:oauth:token-type:id_token",
48 * "id_token": "HEADER.PAYLOAD.SIGNATURE",
49 * "expiration_time": 1620433341
50 * }
51 *
52 * SAML2 response sample:
53 * {
54 * "version": 1,
55 * "success": true,
56 * "token_type": "urn:ietf:params:oauth:token-type:saml2",
57 * "saml_response": "...",
58 * "expiration_time": 1620433341
59 * }
60 *
61 * Error response sample:
62 * {
63 * "version": 1,
64 * "success": false,
65 * "code": "401",
66 * "message": "Error message."
67 * }
68 * </pre>
69 *
70 * The "expiration_time" field in the JSON response is only required for successful
71 * responses when an output file was specified in the credential configuration
72 *
73 * The auth libraries will populate certain environment variables that will be accessible by the
74 * executable, such as: GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE, GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE,
75 * GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE, GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL, and
76 * GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE.
77 */
78class ExecutableSource implements ExternalAccountCredentialSourceInterface
79{
80 private const GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES = 'GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES';
81 private const SAML_SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:saml2';
82 private const OIDC_SUBJECT_TOKEN_TYPE1 = 'urn:ietf:params:oauth:token-type:id_token';
83 private const OIDC_SUBJECT_TOKEN_TYPE2 = 'urn:ietf:params:oauth:token-type:jwt';
84
85 private string $command;
86 private ExecutableHandler $executableHandler;
87 private ?string $outputFile;
88
89 /**
90 * @param string $command The string command to run to get the subject token.
91 * @param string|null $outputFile
92 */
93 public function __construct(
94 string $command,
95 ?string $outputFile,
96 ?ExecutableHandler $executableHandler = null,
97 ) {
98 $this->command = $command;
99 $this->outputFile = $outputFile;
100 $this->executableHandler = $executableHandler ?: new ExecutableHandler();
101 }
102
103 /**
104 * Gets the unique key for caching
105 * The format for the cache key is:
106 * Command.OutputFile
107 *
108 * @return ?string
109 */
110 public function getCacheKey(): ?string
111 {
112 return $this->command . '.' . $this->outputFile;
113 }
114
115 /**
116 * @param callable|null $httpHandler unused.
117 * @return string
118 * @throws RuntimeException if the executable is not allowed to run.
119 * @throws ExecutableResponseError if the executable response is invalid.
120 */
121 public function fetchSubjectToken(?callable $httpHandler = null): string
122 {
123 // Check if the executable is allowed to run.
124 if (getenv(self::GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES) !== '1') {
125 throw new RuntimeException(
126 'Pluggable Auth executables need to be explicitly allowed to run by '
127 . 'setting the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment '
128 . 'Variable to 1.'
129 );
130 }
131
132 if (!$executableResponse = $this->getCachedExecutableResponse()) {
133 // Run the executable.
134 $exitCode = ($this->executableHandler)($this->command);
135 $output = $this->executableHandler->getOutput();
136
137 // If the exit code is not 0, throw an exception with the output as the error details
138 if ($exitCode !== 0) {
139 throw new ExecutableResponseError(
140 'The executable failed to run'
141 . ($output ? ' with the following error: ' . $output : '.'),
142 (string) $exitCode
143 );
144 }
145
146 $executableResponse = $this->parseExecutableResponse($output);
147
148 // Validate expiration.
149 if (isset($executableResponse['expiration_time']) && time() >= $executableResponse['expiration_time']) {
150 throw new ExecutableResponseError('Executable response is expired.');
151 }
152 }
153
154 // Throw error when the request was unsuccessful
155 if ($executableResponse['success'] === false) {
156 throw new ExecutableResponseError($executableResponse['message'], (string) $executableResponse['code']);
157 }
158
159 // Return subject token field based on the token type
160 return $executableResponse['token_type'] === self::SAML_SUBJECT_TOKEN_TYPE
161 ? $executableResponse['saml_response']
162 : $executableResponse['id_token'];
163 }
164
165 /**
166 * @return array<string, mixed>|null
167 */
168 private function getCachedExecutableResponse(): ?array
169 {
170 if (
171 $this->outputFile
172 && file_exists($this->outputFile)
173 && !empty(trim($outputFileContents = (string) file_get_contents($this->outputFile)))
174 ) {
175 try {
176 $executableResponse = $this->parseExecutableResponse($outputFileContents);
177 } catch (ExecutableResponseError $e) {
178 throw new ExecutableResponseError(
179 'Error in output file: ' . $e->getMessage(),
180 'INVALID_OUTPUT_FILE'
181 );
182 }
183
184 if ($executableResponse['success'] === false) {
185 // If the cached token was unsuccessful, run the executable to get a new one.
186 return null;
187 }
188
189 if (isset($executableResponse['expiration_time']) && time() >= $executableResponse['expiration_time']) {
190 // If the cached token is expired, run the executable to get a new one.
191 return null;
192 }
193
194 return $executableResponse;
195 }
196
197 return null;
198 }
199
200 /**
201 * @return array<string, mixed>
202 */
203 private function parseExecutableResponse(string $response): array
204 {
205 $executableResponse = json_decode($response, true);
206 if (json_last_error() !== JSON_ERROR_NONE) {
207 throw new ExecutableResponseError(
208 'The executable returned an invalid response: ' . $response,
209 'INVALID_RESPONSE'
210 );
211 }
212 if (!array_key_exists('version', $executableResponse)) {
213 throw new ExecutableResponseError('Executable response must contain a "version" field.');
214 }
215 if (!array_key_exists('success', $executableResponse)) {
216 throw new ExecutableResponseError('Executable response must contain a "success" field.');
217 }
218
219 // Validate required fields for a successful response.
220 if ($executableResponse['success']) {
221 // Validate token type field.
222 $tokenTypes = [self::SAML_SUBJECT_TOKEN_TYPE, self::OIDC_SUBJECT_TOKEN_TYPE1, self::OIDC_SUBJECT_TOKEN_TYPE2];
223 if (!isset($executableResponse['token_type'])) {
224 throw new ExecutableResponseError(
225 'Executable response must contain a "token_type" field when successful'
226 );
227 }
228 if (!in_array($executableResponse['token_type'], $tokenTypes)) {
229 throw new ExecutableResponseError(sprintf(
230 'Executable response "token_type" field must be one of %s.',
231 implode(', ', $tokenTypes)
232 ));
233 }
234
235 // Validate subject token for SAML and OIDC.
236 if ($executableResponse['token_type'] === self::SAML_SUBJECT_TOKEN_TYPE) {
237 if (empty($executableResponse['saml_response'])) {
238 throw new ExecutableResponseError(sprintf(
239 'Executable response must contain a "saml_response" field when token_type=%s.',
240 self::SAML_SUBJECT_TOKEN_TYPE
241 ));
242 }
243 } elseif (empty($executableResponse['id_token'])) {
244 throw new ExecutableResponseError(sprintf(
245 'Executable response must contain a "id_token" field when '
246 . 'token_type=%s.',
247 $executableResponse['token_type']
248 ));
249 }
250
251 // Validate expiration exists when an output file is specified.
252 if ($this->outputFile) {
253 if (!isset($executableResponse['expiration_time'])) {
254 throw new ExecutableResponseError(
255 'The executable response must contain a "expiration_time" field for successful responses ' .
256 'when an output_file has been specified in the configuration.'
257 );
258 }
259 }
260 } else {
261 // Both code and message must be provided for unsuccessful responses.
262 if (!array_key_exists('code', $executableResponse)) {
263 throw new ExecutableResponseError('Executable response must contain a "code" field when unsuccessful.');
264 }
265 if (empty($executableResponse['message'])) {
266 throw new ExecutableResponseError('Executable response must contain a "message" field when unsuccessful.');
267 }
268 }
269
270 return $executableResponse;
271 }
272}
Note: See TracBrowser for help on using the repository browser.