Skip to content

Commit 2938e58

Browse files
authored
feat: add support for BYOID / STS (#473)
1 parent 06cfb7e commit 2938e58

15 files changed

Lines changed: 1013 additions & 41 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ composer.lock
44
.cache
55
.docs
66
.gitmodules
7+
.phpunit.result.cache
78

89
# IntelliJ
910
.idea
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
/*
3+
* Copyright 2023 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+
18+
namespace Google\Auth\CredentialSource;
19+
20+
use Google\Auth\ExternalAccountCredentialSourceInterface;
21+
use InvalidArgumentException;
22+
use UnexpectedValueException;
23+
24+
/**
25+
* Retrieve a token from a file.
26+
*/
27+
class FileSource implements ExternalAccountCredentialSourceInterface
28+
{
29+
private string $file;
30+
private ?string $format;
31+
private ?string $subjectTokenFieldName;
32+
33+
/**
34+
* @param string $file The file to read the subject token from.
35+
* @param string $format The format of the token in the file. Can be null or "json".
36+
* @param string $subjectTokenFieldName The name of the field containing the token in the file. This is required
37+
* when format is "json".
38+
*/
39+
public function __construct(
40+
string $file,
41+
string $format = null,
42+
string $subjectTokenFieldName = null
43+
) {
44+
$this->file = $file;
45+
46+
if ($format === 'json' && is_null($subjectTokenFieldName)) {
47+
throw new InvalidArgumentException(
48+
'subject_token_field_name must be set when format is JSON'
49+
);
50+
}
51+
52+
$this->format = $format;
53+
$this->subjectTokenFieldName = $subjectTokenFieldName;
54+
}
55+
56+
public function fetchSubjectToken(callable $httpHandler = null): string
57+
{
58+
$contents = file_get_contents($this->file);
59+
if ($this->format === 'json') {
60+
if (!$json = json_decode((string) $contents, true)) {
61+
throw new UnexpectedValueException(
62+
'Unable to decode JSON file'
63+
);
64+
}
65+
if (!isset($json[$this->subjectTokenFieldName])) {
66+
throw new UnexpectedValueException(
67+
'subject_token_field_name not found in JSON file'
68+
);
69+
}
70+
$contents = $json[$this->subjectTokenFieldName];
71+
}
72+
73+
return $contents;
74+
}
75+
}

src/CredentialSource/UrlSource.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
/*
3+
* Copyright 2023 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+
18+
namespace Google\Auth\CredentialSource;
19+
20+
use Google\Auth\ExternalAccountCredentialSourceInterface;
21+
use Google\Auth\HttpHandler\HttpClientCache;
22+
use Google\Auth\HttpHandler\HttpHandlerFactory;
23+
use GuzzleHttp\Psr7\Request;
24+
use InvalidArgumentException;
25+
use UnexpectedValueException;
26+
27+
/**
28+
* Retrieve a token from a URL.
29+
*/
30+
class UrlSource implements ExternalAccountCredentialSourceInterface
31+
{
32+
private string $url;
33+
private ?string $format;
34+
private ?string $subjectTokenFieldName;
35+
36+
/**
37+
* @var array<string, string|string[]>
38+
*/
39+
private ?array $headers;
40+
41+
/**
42+
* @param string $url The URL to fetch the subject token from.
43+
* @param string $format The format of the token in the response. Can be null or "json".
44+
* @param string $subjectTokenFieldName The name of the field containing the token in the response. This is required
45+
* when format is "json".
46+
* @param array<string, string|string[]> $headers Request headers to send in with the request to the URL.
47+
*/
48+
public function __construct(
49+
string $url,
50+
string $format = null,
51+
string $subjectTokenFieldName = null,
52+
array $headers = null
53+
) {
54+
$this->url = $url;
55+
56+
if ($format === 'json' && is_null($subjectTokenFieldName)) {
57+
throw new InvalidArgumentException(
58+
'subject_token_field_name must be set when format is JSON'
59+
);
60+
}
61+
62+
$this->format = $format;
63+
$this->subjectTokenFieldName = $subjectTokenFieldName;
64+
$this->headers = $headers;
65+
}
66+
67+
public function fetchSubjectToken(callable $httpHandler = null): string
68+
{
69+
if (is_null($httpHandler)) {
70+
$httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
71+
}
72+
73+
$request = new Request(
74+
'GET',
75+
$this->url,
76+
$this->headers ?: []
77+
);
78+
79+
$response = $httpHandler($request);
80+
$body = (string) $response->getBody();
81+
if ($this->format === 'json') {
82+
if (!$json = json_decode((string) $body, true)) {
83+
throw new UnexpectedValueException(
84+
'Unable to decode JSON response'
85+
);
86+
}
87+
if (!isset($json[$this->subjectTokenFieldName])) {
88+
throw new UnexpectedValueException(
89+
'subject_token_field_name not found in JSON file'
90+
);
91+
}
92+
$body = $json[$this->subjectTokenFieldName];
93+
}
94+
95+
return $body;
96+
}
97+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
/*
3+
* Copyright 2023 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+
18+
namespace Google\Auth\Credentials;
19+
20+
use Google\Auth\CredentialSource\FileSource;
21+
use Google\Auth\CredentialSource\UrlSource;
22+
use Google\Auth\ExternalAccountCredentialSourceInterface;
23+
use Google\Auth\FetchAuthTokenInterface;
24+
use Google\Auth\OAuth2;
25+
use Google\Auth\UpdateMetadataInterface;
26+
use Google\Auth\UpdateMetadataTrait;
27+
use InvalidArgumentException;
28+
29+
class ExternalAccountCredentials implements FetchAuthTokenInterface, UpdateMetadataInterface
30+
{
31+
use UpdateMetadataTrait;
32+
33+
private const EXTERNAL_ACCOUNT_TYPE = 'external_account';
34+
35+
private OAuth2 $auth;
36+
37+
/**
38+
* @param string|string[] $scope The scope of the access request, expressed either as an array
39+
* or as a space-delimited string.
40+
* @param array<mixed> $jsonKey JSON credentials as an associative array.
41+
*/
42+
public function __construct(
43+
$scope,
44+
array $jsonKey
45+
) {
46+
if (!array_key_exists('type', $jsonKey)) {
47+
throw new InvalidArgumentException('json key is missing the type field');
48+
}
49+
if ($jsonKey['type'] !== self::EXTERNAL_ACCOUNT_TYPE) {
50+
throw new InvalidArgumentException(sprintf(
51+
'expected "%s" type but received "%s"',
52+
self::EXTERNAL_ACCOUNT_TYPE,
53+
$jsonKey['type']
54+
));
55+
}
56+
57+
if (!array_key_exists('token_url', $jsonKey)) {
58+
throw new InvalidArgumentException(
59+
'json key is missing the token_url field'
60+
);
61+
}
62+
63+
if (!array_key_exists('audience', $jsonKey)) {
64+
throw new InvalidArgumentException(
65+
'json key is missing the audience field'
66+
);
67+
}
68+
69+
if (!array_key_exists('subject_token_type', $jsonKey)) {
70+
throw new InvalidArgumentException(
71+
'json key is missing the subject_token_type field'
72+
);
73+
}
74+
75+
if (!array_key_exists('credential_source', $jsonKey)) {
76+
throw new InvalidArgumentException(
77+
'json key is missing the credential_source field'
78+
);
79+
}
80+
81+
$this->auth = new OAuth2([
82+
'tokenCredentialUri' => $jsonKey['token_url'],
83+
'audience' => $jsonKey['audience'],
84+
'scope' => $scope,
85+
'subjectTokenType' => $jsonKey['subject_token_type'],
86+
'subjectTokenFetcher' => self::buildCredentialSource($jsonKey),
87+
]);
88+
}
89+
90+
/**
91+
* @param array<mixed> $jsonKey
92+
*/
93+
private static function buildCredentialSource(array $jsonKey): ExternalAccountCredentialSourceInterface
94+
{
95+
$credentialSource = $jsonKey['credential_source'];
96+
if (isset($credentialSource['file'])) {
97+
return new FileSource(
98+
$credentialSource['file'],
99+
$credentialSource['format']['type'] ?? null,
100+
$credentialSource['format']['subject_token_field_name'] ?? null
101+
);
102+
}
103+
104+
if (isset($credentialSource['url'])) {
105+
return new UrlSource(
106+
$credentialSource['url'],
107+
$credentialSource['format']['type'] ?? null,
108+
$credentialSource['format']['subject_token_field_name'] ?? null,
109+
$credentialSource['headers'] ?? null,
110+
);
111+
}
112+
113+
throw new InvalidArgumentException('Unable to determine credential source from json key.');
114+
}
115+
116+
/**
117+
* @param callable $httpHandler
118+
*
119+
* @return array<mixed> {
120+
* A set of auth related metadata, containing the following
121+
*
122+
* @type string $access_token
123+
* @type int $expires_in
124+
* @type string $scope
125+
* @type string $token_type
126+
* @type string $id_token
127+
* }
128+
*/
129+
public function fetchAuthToken(callable $httpHandler = null)
130+
{
131+
return $this->auth->fetchAuthToken($httpHandler);
132+
}
133+
134+
public function getCacheKey()
135+
{
136+
return $this->auth->getCacheKey();
137+
}
138+
139+
public function getLastReceivedToken()
140+
{
141+
return $this->auth->getLastReceivedToken();
142+
}
143+
}

0 commit comments

Comments
 (0)