Skip to content

Commit

Permalink
feat: add support for BYOID / STS (#473)
Browse files Browse the repository at this point in the history
  • Loading branch information
bshaffer authored Sep 7, 2023
1 parent 06cfb7e commit 2938e58
Show file tree
Hide file tree
Showing 15 changed files with 1,013 additions and 41 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ composer.lock
.cache
.docs
.gitmodules
.phpunit.result.cache

# IntelliJ
.idea
Expand Down
75 changes: 75 additions & 0 deletions src/CredentialSource/FileSource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php
/*
* Copyright 2023 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Auth\CredentialSource;

use Google\Auth\ExternalAccountCredentialSourceInterface;
use InvalidArgumentException;
use UnexpectedValueException;

/**
* Retrieve a token from a file.
*/
class FileSource implements ExternalAccountCredentialSourceInterface
{
private string $file;
private ?string $format;
private ?string $subjectTokenFieldName;

/**
* @param string $file The file to read the subject token from.
* @param string $format The format of the token in the file. Can be null or "json".
* @param string $subjectTokenFieldName The name of the field containing the token in the file. This is required
* when format is "json".
*/
public function __construct(
string $file,
string $format = null,
string $subjectTokenFieldName = null
) {
$this->file = $file;

if ($format === 'json' && is_null($subjectTokenFieldName)) {
throw new InvalidArgumentException(
'subject_token_field_name must be set when format is JSON'
);
}

$this->format = $format;
$this->subjectTokenFieldName = $subjectTokenFieldName;
}

public function fetchSubjectToken(callable $httpHandler = null): string
{
$contents = file_get_contents($this->file);
if ($this->format === 'json') {
if (!$json = json_decode((string) $contents, true)) {
throw new UnexpectedValueException(
'Unable to decode JSON file'
);
}
if (!isset($json[$this->subjectTokenFieldName])) {
throw new UnexpectedValueException(
'subject_token_field_name not found in JSON file'
);
}
$contents = $json[$this->subjectTokenFieldName];
}

return $contents;
}
}
97 changes: 97 additions & 0 deletions src/CredentialSource/UrlSource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php
/*
* Copyright 2023 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Auth\CredentialSource;

use Google\Auth\ExternalAccountCredentialSourceInterface;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use GuzzleHttp\Psr7\Request;
use InvalidArgumentException;
use UnexpectedValueException;

/**
* Retrieve a token from a URL.
*/
class UrlSource implements ExternalAccountCredentialSourceInterface
{
private string $url;
private ?string $format;
private ?string $subjectTokenFieldName;

/**
* @var array<string, string|string[]>
*/
private ?array $headers;

/**
* @param string $url The URL to fetch the subject token from.
* @param string $format The format of the token in the response. Can be null or "json".
* @param string $subjectTokenFieldName The name of the field containing the token in the response. This is required
* when format is "json".
* @param array<string, string|string[]> $headers Request headers to send in with the request to the URL.
*/
public function __construct(
string $url,
string $format = null,
string $subjectTokenFieldName = null,
array $headers = null
) {
$this->url = $url;

if ($format === 'json' && is_null($subjectTokenFieldName)) {
throw new InvalidArgumentException(
'subject_token_field_name must be set when format is JSON'
);
}

$this->format = $format;
$this->subjectTokenFieldName = $subjectTokenFieldName;
$this->headers = $headers;
}

public function fetchSubjectToken(callable $httpHandler = null): string
{
if (is_null($httpHandler)) {
$httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient());
}

$request = new Request(
'GET',
$this->url,
$this->headers ?: []
);

$response = $httpHandler($request);
$body = (string) $response->getBody();
if ($this->format === 'json') {
if (!$json = json_decode((string) $body, true)) {
throw new UnexpectedValueException(
'Unable to decode JSON response'
);
}
if (!isset($json[$this->subjectTokenFieldName])) {
throw new UnexpectedValueException(
'subject_token_field_name not found in JSON file'
);
}
$body = $json[$this->subjectTokenFieldName];
}

return $body;
}
}
143 changes: 143 additions & 0 deletions src/Credentials/ExternalAccountCredentials.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php
/*
* Copyright 2023 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Auth\Credentials;

use Google\Auth\CredentialSource\FileSource;
use Google\Auth\CredentialSource\UrlSource;
use Google\Auth\ExternalAccountCredentialSourceInterface;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\OAuth2;
use Google\Auth\UpdateMetadataInterface;
use Google\Auth\UpdateMetadataTrait;
use InvalidArgumentException;

class ExternalAccountCredentials implements FetchAuthTokenInterface, UpdateMetadataInterface
{
use UpdateMetadataTrait;

private const EXTERNAL_ACCOUNT_TYPE = 'external_account';

private OAuth2 $auth;

/**
* @param string|string[] $scope The scope of the access request, expressed either as an array
* or as a space-delimited string.
* @param array<mixed> $jsonKey JSON credentials as an associative array.
*/
public function __construct(
$scope,
array $jsonKey
) {
if (!array_key_exists('type', $jsonKey)) {
throw new InvalidArgumentException('json key is missing the type field');
}
if ($jsonKey['type'] !== self::EXTERNAL_ACCOUNT_TYPE) {
throw new InvalidArgumentException(sprintf(
'expected "%s" type but received "%s"',
self::EXTERNAL_ACCOUNT_TYPE,
$jsonKey['type']
));
}

if (!array_key_exists('token_url', $jsonKey)) {
throw new InvalidArgumentException(
'json key is missing the token_url field'
);
}

if (!array_key_exists('audience', $jsonKey)) {
throw new InvalidArgumentException(
'json key is missing the audience field'
);
}

if (!array_key_exists('subject_token_type', $jsonKey)) {
throw new InvalidArgumentException(
'json key is missing the subject_token_type field'
);
}

if (!array_key_exists('credential_source', $jsonKey)) {
throw new InvalidArgumentException(
'json key is missing the credential_source field'
);
}

$this->auth = new OAuth2([
'tokenCredentialUri' => $jsonKey['token_url'],
'audience' => $jsonKey['audience'],
'scope' => $scope,
'subjectTokenType' => $jsonKey['subject_token_type'],
'subjectTokenFetcher' => self::buildCredentialSource($jsonKey),
]);
}

/**
* @param array<mixed> $jsonKey
*/
private static function buildCredentialSource(array $jsonKey): ExternalAccountCredentialSourceInterface
{
$credentialSource = $jsonKey['credential_source'];
if (isset($credentialSource['file'])) {
return new FileSource(
$credentialSource['file'],
$credentialSource['format']['type'] ?? null,
$credentialSource['format']['subject_token_field_name'] ?? null
);
}

if (isset($credentialSource['url'])) {
return new UrlSource(
$credentialSource['url'],
$credentialSource['format']['type'] ?? null,
$credentialSource['format']['subject_token_field_name'] ?? null,
$credentialSource['headers'] ?? null,
);
}

throw new InvalidArgumentException('Unable to determine credential source from json key.');
}

/**
* @param callable $httpHandler
*
* @return array<mixed> {
* A set of auth related metadata, containing the following
*
* @type string $access_token
* @type int $expires_in
* @type string $scope
* @type string $token_type
* @type string $id_token
* }
*/
public function fetchAuthToken(callable $httpHandler = null)
{
return $this->auth->fetchAuthToken($httpHandler);
}

public function getCacheKey()
{
return $this->auth->getCacheKey();
}

public function getLastReceivedToken()
{
return $this->auth->getLastReceivedToken();
}
}
Loading

0 comments on commit 2938e58

Please sign in to comment.