diff --git a/README.md b/README.md index e47a2e94e..695d4f3ca 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ See full documentation on [https://async-aws.com](https://async-aws.com). | [async-aws/sqs](https://github.com/async-aws/sqs) | [![Latest Stable Version](https://poser.pugx.org/async-aws/sqs/v/stable)](https://packagist.org/packages/async-aws/sqs) [![Total Downloads](https://poser.pugx.org/async-aws/sqs/downloads)](https://packagist.org/packages/async-aws/sqs) | [![](https://github.com/async-aws/sqs/workflows/BC%20Check/badge.svg?branch=master)](https://github.com/async-aws/sqs/actions) | [![](https://async-aws-pr.github.io/commits-since-release-counter/sqs.svg)](https://github.com/async-aws/sqs/releases) | | [async-aws/ssm](https://github.com/async-aws/ssm) | [![Latest Stable Version](https://poser.pugx.org/async-aws/ssm/v/stable)](https://packagist.org/packages/async-aws/ssm) [![Total Downloads](https://poser.pugx.org/async-aws/ssm/downloads)](https://packagist.org/packages/async-aws/ssm) | [![](https://github.com/async-aws/ssm/workflows/BC%20Check/badge.svg?branch=master)](https://github.com/async-aws/ssm/actions) | [![](https://async-aws-pr.github.io/commits-since-release-counter/ssm.svg)](https://github.com/async-aws/ssm/releases) | | [async-aws/sso](https://github.com/async-aws/sso) | [![Latest Stable Version](https://poser.pugx.org/async-aws/sso/v/stable)](https://packagist.org/packages/async-aws/sso) [![Total Downloads](https://poser.pugx.org/async-aws/sso/downloads)](https://packagist.org/packages/async-aws/sso) | [![](https://github.com/async-aws/sso/workflows/BC%20Check/badge.svg?branch=master)](https://github.com/async-aws/sso/actions) | [![](https://async-aws-pr.github.io/commits-since-release-counter/sso.svg)](https://github.com/async-aws/sso/releases) | +| [async-aws/sso-oidc](https://github.com/async-aws/sso-oidc) | [![Latest Stable Version](https://poser.pugx.org/async-aws/sso-oidc/v/stable)](https://packagist.org/packages/async-aws/sso-oidc) [![Total Downloads](https://poser.pugx.org/async-aws/sso-oidc/downloads)](https://packagist.org/packages/async-aws/sso-oidc) | [![](https://github.com/async-aws/sso-oidc/workflows/BC%20Check/badge.svg?branch=master)](https://github.com/async-aws/sso-oidc/actions) | [![](https://async-aws-pr.github.io/commits-since-release-counter/sso-oidc.svg)](https://github.com/async-aws/sso-oidc/releases) | | [async-aws/step-functions](https://github.com/async-aws/step-functions) | [![Latest Stable Version](https://poser.pugx.org/async-aws/step-functions/v/stable)](https://packagist.org/packages/async-aws/step-functions) [![Total Downloads](https://poser.pugx.org/async-aws/step-functions/downloads)](https://packagist.org/packages/async-aws/step-functions) | [![](https://github.com/async-aws/step-functions/workflows/BC%20Check/badge.svg?branch=master)](https://github.com/async-aws/step-functions/actions) | [![](https://async-aws-pr.github.io/commits-since-release-counter/step-functions.svg)](https://github.com/async-aws/step-functions/releases) | | [async-aws/timestream-query](https://github.com/async-aws/timestream-query) | [![Latest Stable Version](https://poser.pugx.org/async-aws/timestream-query/v/stable)](https://packagist.org/packages/async-aws/timestream-query) [![Total Downloads](https://poser.pugx.org/async-aws/timestream-query/downloads)](https://packagist.org/packages/async-aws/timestream-query) | [![](https://github.com/async-aws/timestream-query/workflows/BC%20Check/badge.svg?branch=master)](https://github.com/async-aws/timestream-query/actions) | [![](https://async-aws-pr.github.io/commits-since-release-counter/timestream-query.svg)](https://github.com/async-aws/timestream-query/releases) | | [async-aws/timestream-write](https://github.com/async-aws/timestream-write) | [![Latest Stable Version](https://poser.pugx.org/async-aws/timestream-write/v/stable)](https://packagist.org/packages/async-aws/timestream-write) [![Total Downloads](https://poser.pugx.org/async-aws/timestream-write/downloads)](https://packagist.org/packages/async-aws/timestream-write) | [![](https://github.com/async-aws/timestream-write/workflows/BC%20Check/badge.svg?branch=master)](https://github.com/async-aws/timestream-write/actions) | [![](https://async-aws-pr.github.io/commits-since-release-counter/timestream-write.svg)](https://github.com/async-aws/timestream-write/releases) | diff --git a/composer.json b/composer.json index 835679a46..2ba32faee 100644 --- a/composer.json +++ b/composer.json @@ -88,6 +88,7 @@ "AsyncAws\\Sns\\": "src/Service/Sns/src", "AsyncAws\\Sqs\\": "src/Service/Sqs/src", "AsyncAws\\Ssm\\": "src/Service/Ssm/src", + "AsyncAws\\SsoOidc\\": "src/Service/SsoOidc/src", "AsyncAws\\Sso\\": "src/Service/Sso/src", "AsyncAws\\StepFunctions\\": "src/Service/StepFunctions/src", "AsyncAws\\Symfony\\Bundle\\": "src/Integration/Symfony/Bundle/src", @@ -139,6 +140,7 @@ "AsyncAws\\Sns\\Tests\\": "src/Service/Sns/tests", "AsyncAws\\Sqs\\Tests\\": "src/Service/Sqs/tests", "AsyncAws\\Ssm\\Tests\\": "src/Service/Ssm/tests", + "AsyncAws\\SsoOidc\\Tests\\": "src/Service/SsoOidc/tests", "AsyncAws\\Sso\\Tests\\": "src/Service/Sso/tests", "AsyncAws\\StepFunctions\\Tests\\": "src/Service/StepFunctions/tests", "AsyncAws\\Symfony\\Bundle\\Tests\\": "src/Integration/Symfony/Bundle/tests", diff --git a/couscous.yml b/couscous.yml index f0278f243..ca8a75ebf 100644 --- a/couscous.yml +++ b/couscous.yml @@ -136,6 +136,9 @@ menu: sso: text: SSO url: /clients/sso.html + sso-oidc: + text: SSOOIDC + url: /clients/sso-oidc.html sts: text: STS url: /clients/sts.html diff --git a/docs/clients/index.md b/docs/clients/index.md index 3d7a99495..8900799c4 100644 --- a/docs/clients/index.md +++ b/docs/clients/index.md @@ -150,7 +150,7 @@ for more information. | [Comprehend](./comprehend.md) | [async-aws/comprehend](https://packagist.org/packages/async-aws/comprehend) | | [DynamoDb](./dynamodb.md) | [async-aws/dynamo-db](https://packagist.org/packages/async-aws/dynamo-db) | | [ECR](./ecr.md) | [async-aws/ecr](https://packagist.org/packages/async-aws/ecr) | -| [ElastiCache](./elasti-cache.md) | [async-aws/elasti-cache](https://packagist.org/packages/async-aws/elasti-cache) | +| [ElastiCache](./elasti-cache.md) | [async-aws/elasti-cache](https://packagist.org/packages/async-aws/elasti-cache) | | [EventBridge](./event-bridge.md) | [async-aws/event-bridge](https://packagist.org/packages/async-aws/event-bridge) | | [Firehose](./firehose.md) | [async-aws/event-bridge](https://packagist.org/packages/async-aws/firehose) | | [IAM](./iam.md) | [async-aws/iam](https://packagist.org/packages/async-aws/iam) | @@ -172,6 +172,7 @@ for more information. | [SQS](./sqs.md) | [async-aws/sqs](https://packagist.org/packages/async-aws/sqs) | | [SSM](./ssm.md) | [async-aws/ssm](https://packagist.org/packages/async-aws/ssm) | | [SSO](./sso.md) | [async-aws/sso](https://packagist.org/packages/async-aws/sso) | +| [SSOOIDC](./sso-oidc.md) | [async-aws/sso-oidc](https://packagist.org/packages/async-aws/sso-oidc) | | [STS](./sts.md) | [async-aws/core](https://packagist.org/packages/async-aws/core) | | [StepFunctions](./step-functions.md) | [async-aws/step-functions](https://packagist.org/packages/async-aws/step-functions) | | [TimestreamQuery](./timestream-query.md) | [async-aws/timestream-query](https://packagist.org/packages/async-aws/timestream-query) | diff --git a/docs/clients/sso-oidc.md b/docs/clients/sso-oidc.md new file mode 100644 index 000000000..1fd5f4903 --- /dev/null +++ b/docs/clients/sso-oidc.md @@ -0,0 +1,27 @@ +--- +layout: client +category: clients +name: SSOOIDC +package: async-aws/sso-oidc +fqcn: AsyncAws\Sso\SsoOidcClient +--- + +## Usage + +### Create a token + +```php +use AsyncAws\SsoOidc\Input\CreateToken; +use AsyncAws\SsoOidc\SsoOidcClient; + +$client = new SsoOidcClient(); + +$result = $client->createToken(new CreateToken([ + 'clientId' => 'YourClientId', + 'clientSecret' => 'YourClientSecret', + 'grantType' => 'authorization_code', +])); + +echo 'AccessToken:' . $result->getAccessToken().PHP_EOL; +echo 'Expiration:' . $result->getExpiresIn().PHP_EOL; +``` diff --git a/manifest.json b/manifest.json index 2260fee79..234bce257 100644 --- a/manifest.json +++ b/manifest.json @@ -692,11 +692,22 @@ "source": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/sso/2019-06-10/api-2.json", "documentation": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/sso/2019-06-10/docs-2.json", "pagination": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/sso/2019-06-10/paginators-1.json", + "example": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/sso/2019-06-10/examples-1.json", "api-reference": "https://docs.aws.amazon.com/singlesignon/latest/PortalAPIReference", "methods": [ "GetRoleCredentials" ] }, + "SsoOidc": { + "source": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/sso-oidc/2019-06-10/api-2.json", + "documentation": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/sso-oidc/2019-06-10/docs-2.json", + "pagination": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/sso-oidc/2019-06-10/paginators-1.json", + "example": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/sso-oidc/2019-06-10/examples-1.json", + "api-reference": "https://docs.aws.amazon.com/singlesignon/latest/OIDCAPIReference", + "methods": [ + "CreateToken" + ] + }, "Sts": { "source": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/sts/2011-06-15/api-2.json", "documentation": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/sts/2011-06-15/docs-2.json", diff --git a/src/Core/CHANGELOG.md b/src/Core/CHANGELOG.md index c394f8f2e..86302fae4 100644 --- a/src/Core/CHANGELOG.md +++ b/src/Core/CHANGELOG.md @@ -2,6 +2,10 @@ ## NOT RELEASED +### Added + +- Support for SsoOidc + ### Changed - AWS enhancement: Documentation updates. diff --git a/src/Core/composer.json b/src/Core/composer.json index 3d7ce96d5..fd9948efc 100644 --- a/src/Core/composer.json +++ b/src/Core/composer.json @@ -38,7 +38,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.23-dev" + "dev-master": "1.24-dev" } } } diff --git a/src/Core/src/AwsClientFactory.php b/src/Core/src/AwsClientFactory.php index 6f5bdbde2..31dd577f7 100644 --- a/src/Core/src/AwsClientFactory.php +++ b/src/Core/src/AwsClientFactory.php @@ -45,6 +45,7 @@ use AsyncAws\Sqs\SqsClient; use AsyncAws\Ssm\SsmClient; use AsyncAws\Sso\SsoClient; +use AsyncAws\SsoOidc\SsoOidcClient; use AsyncAws\StepFunctions\StepFunctionsClient; use AsyncAws\TimestreamQuery\TimestreamQueryClient; use AsyncAws\TimestreamWrite\TimestreamWriteClient; @@ -533,6 +534,19 @@ public function sso(): SsoClient return $this->serviceCache[__METHOD__]; } + public function ssoOidc(): SsoOidcClient + { + if (!class_exists(SsoOidcClient::class)) { + throw MissingDependency::create('async-aws/sso-oidc', 'SsoOidc'); + } + + if (!isset($this->serviceCache[__METHOD__])) { + $this->serviceCache[__METHOD__] = new SsoOidcClient($this->configuration, $this->credentialProvider, $this->httpClient, $this->logger); + } + + return $this->serviceCache[__METHOD__]; + } + public function sts(): StsClient { if (!isset($this->serviceCache[__METHOD__])) { diff --git a/src/Integration/Symfony/Bundle/CHANGELOG.md b/src/Integration/Symfony/Bundle/CHANGELOG.md index ce081cdf3..449681ebd 100644 --- a/src/Integration/Symfony/Bundle/CHANGELOG.md +++ b/src/Integration/Symfony/Bundle/CHANGELOG.md @@ -2,6 +2,10 @@ ## NOT RELEASED +### Added + +- Support for SSOOIDC + ## 1.12.3 ### Changed diff --git a/src/Integration/Symfony/Bundle/composer.json b/src/Integration/Symfony/Bundle/composer.json index 56d68b201..b621af302 100644 --- a/src/Integration/Symfony/Bundle/composer.json +++ b/src/Integration/Symfony/Bundle/composer.json @@ -39,7 +39,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.12-dev" + "dev-master": "1.13-dev" } } } diff --git a/src/Integration/Symfony/Bundle/src/DependencyInjection/AwsPackagesProvider.php b/src/Integration/Symfony/Bundle/src/DependencyInjection/AwsPackagesProvider.php index 57e841fbc..85852cb2d 100644 --- a/src/Integration/Symfony/Bundle/src/DependencyInjection/AwsPackagesProvider.php +++ b/src/Integration/Symfony/Bundle/src/DependencyInjection/AwsPackagesProvider.php @@ -158,6 +158,10 @@ public static function getAllServices(): array 'class' => \AsyncAws\Sso\SsoClient::class, 'package' => 'async-aws/sso', ], + 'sso_oidc' => [ + 'class' => \AsyncAws\SsoOidc\SsoOidcClient::class, + 'package' => 'async-aws/sso-oidc', + ], 'sts' => [ 'class' => \AsyncAws\Core\Sts\StsClient::class, 'package' => 'async-aws/core', diff --git a/src/Service/SsoOidc/.gitattributes b/src/Service/SsoOidc/.gitattributes new file mode 100644 index 000000000..410d4a1a6 --- /dev/null +++ b/src/Service/SsoOidc/.gitattributes @@ -0,0 +1,5 @@ +/.github export-ignore +/tests export-ignore +/.gitignore export-ignore +/Makefile export-ignore +/phpunit.xml.dist export-ignore diff --git a/src/Service/SsoOidc/.github/FUNDING.yml b/src/Service/SsoOidc/.github/FUNDING.yml new file mode 100644 index 000000000..ef7eb6190 --- /dev/null +++ b/src/Service/SsoOidc/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [nyholm, jderusse] diff --git a/src/Service/SsoOidc/.github/workflows/.editorconfig b/src/Service/SsoOidc/.github/workflows/.editorconfig new file mode 100644 index 000000000..7bd3346f2 --- /dev/null +++ b/src/Service/SsoOidc/.github/workflows/.editorconfig @@ -0,0 +1,2 @@ +[*.yml] +indent_size = 2 diff --git a/src/Service/SsoOidc/.github/workflows/checks.yml b/src/Service/SsoOidc/.github/workflows/checks.yml new file mode 100644 index 000000000..b9c47e84b --- /dev/null +++ b/src/Service/SsoOidc/.github/workflows/checks.yml @@ -0,0 +1,38 @@ +name: BC Check + +on: + push: + branches: + - master + +jobs: + roave-bc-check: + name: Roave BC Check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Modify composer.json + run: | + sed -i -re 's/"require": \{/"minimum-stability": "dev","prefer-stable": true,"require": \{/' composer.json + cat composer.json + + git config --local user.email "github@async-aws.com" + git config --local user.name "AsyncAws Bot" + git commit -am "Allow unstable dependencies" + + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + tools: composer:v2 + + - name: Install roave/backward-compatibility-check + run: composer require --dev roave/backward-compatibility-check + + - name: Roave BC Check + run: vendor/bin/roave-backward-compatibility-check diff --git a/src/Service/SsoOidc/.github/workflows/ci.yml b/src/Service/SsoOidc/.github/workflows/ci.yml new file mode 100644 index 000000000..0778da59d --- /dev/null +++ b/src/Service/SsoOidc/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: Tests + +on: + push: + branches: + - master + +jobs: + + build: + name: Build + runs-on: ubuntu-latest + strategy: + max-parallel: 10 + matrix: + php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + + steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Initialize tests + run: make initialize + + - name: Download dependencies + run: | + composer config minimum-stability dev + composer req symfony/phpunit-bridge --no-update + composer update --no-interaction --prefer-dist --optimize-autoloader --prefer-stable + + - name: Run tests + run: ./vendor/bin/simple-phpunit diff --git a/src/Service/SsoOidc/.gitignore b/src/Service/SsoOidc/.gitignore new file mode 100644 index 000000000..4ef8091e0 --- /dev/null +++ b/src/Service/SsoOidc/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +*.cache +composer.lock diff --git a/src/Service/SsoOidc/CHANGELOG.md b/src/Service/SsoOidc/CHANGELOG.md new file mode 100644 index 000000000..a64e84b42 --- /dev/null +++ b/src/Service/SsoOidc/CHANGELOG.md @@ -0,0 +1,7 @@ +# Change Log + +## NOT RELEASED + +## 1.0.0 + +First version diff --git a/src/Service/SsoOidc/LICENSE b/src/Service/SsoOidc/LICENSE new file mode 100644 index 000000000..c924ee5c6 --- /dev/null +++ b/src/Service/SsoOidc/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022 Jérémy Derussé, Tobias Nyholm + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Service/SsoOidc/Makefile b/src/Service/SsoOidc/Makefile new file mode 100644 index 000000000..850dffccd --- /dev/null +++ b/src/Service/SsoOidc/Makefile @@ -0,0 +1,12 @@ +.EXPORT_ALL_VARIABLES: + +initialize: start-docker +start-docker: + echo "Noop" + +test: initialize + ./vendor/bin/simple-phpunit + +clean: stop-docker +stop-docker: + echo "Noop" diff --git a/src/Service/SsoOidc/README.md b/src/Service/SsoOidc/README.md new file mode 100644 index 000000000..b0f80195d --- /dev/null +++ b/src/Service/SsoOidc/README.md @@ -0,0 +1,20 @@ +# AsyncAws SsoOidc Client + +![](https://github.com/async-aws/sso-oidc/workflows/Tests/badge.svg?branch=master) +![](https://github.com/async-aws/sso-oidc/workflows/BC%20Check/badge.svg?branch=master) + +An API client for SsoOidc. + +## Install + +```cli +composer require async-aws/sso-oidc +``` + +## Documentation + +See https://async-aws.com/clients/sso-oidc.html for documentation. + +## Contribute + +Contributions are welcome and appreciated. Please read https://async-aws.com/contribute/ diff --git a/src/Service/SsoOidc/composer.json b/src/Service/SsoOidc/composer.json new file mode 100644 index 000000000..baeb8e335 --- /dev/null +++ b/src/Service/SsoOidc/composer.json @@ -0,0 +1,33 @@ +{ + "name": "async-aws/sso-oidc", + "description": "SsoOidc client, part of the AWS SDK provided by AsyncAws.", + "license": "MIT", + "type": "library", + "keywords": [ + "aws", + "amazon", + "sdk", + "async-aws", + "sso-oidc" + ], + "require": { + "php": "^7.2.5 || ^8.0", + "ext-json": "*", + "async-aws/core": "^1.9" + }, + "autoload": { + "psr-4": { + "AsyncAws\\SsoOidc\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "AsyncAws\\SsoOidc\\Tests\\": "tests/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + } +} diff --git a/src/Service/SsoOidc/phpunit.xml.dist b/src/Service/SsoOidc/phpunit.xml.dist new file mode 100644 index 000000000..9894ce353 --- /dev/null +++ b/src/Service/SsoOidc/phpunit.xml.dist @@ -0,0 +1,23 @@ + + + + + ./src + + + + + + + + ./tests/ + + + diff --git a/src/Service/SsoOidc/src/Exception/AccessDeniedException.php b/src/Service/SsoOidc/src/Exception/AccessDeniedException.php new file mode 100644 index 000000000..663eca027 --- /dev/null +++ b/src/Service/SsoOidc/src/Exception/AccessDeniedException.php @@ -0,0 +1,45 @@ +error; + } + + public function getError_description(): ?string + { + return $this->error_description; + } + + protected function populateResult(ResponseInterface $response): void + { + $data = $response->toArray(false); + + $this->error = isset($data['error']) ? (string) $data['error'] : null; + $this->error_description = isset($data['error_description']) ? (string) $data['error_description'] : null; + } +} diff --git a/src/Service/SsoOidc/src/Exception/AuthorizationPendingException.php b/src/Service/SsoOidc/src/Exception/AuthorizationPendingException.php new file mode 100644 index 000000000..ea2a488a9 --- /dev/null +++ b/src/Service/SsoOidc/src/Exception/AuthorizationPendingException.php @@ -0,0 +1,45 @@ +error; + } + + public function getError_description(): ?string + { + return $this->error_description; + } + + protected function populateResult(ResponseInterface $response): void + { + $data = $response->toArray(false); + + $this->error = isset($data['error']) ? (string) $data['error'] : null; + $this->error_description = isset($data['error_description']) ? (string) $data['error_description'] : null; + } +} diff --git a/src/Service/SsoOidc/src/Exception/ExpiredTokenException.php b/src/Service/SsoOidc/src/Exception/ExpiredTokenException.php new file mode 100644 index 000000000..4e9a02716 --- /dev/null +++ b/src/Service/SsoOidc/src/Exception/ExpiredTokenException.php @@ -0,0 +1,45 @@ +error; + } + + public function getError_description(): ?string + { + return $this->error_description; + } + + protected function populateResult(ResponseInterface $response): void + { + $data = $response->toArray(false); + + $this->error = isset($data['error']) ? (string) $data['error'] : null; + $this->error_description = isset($data['error_description']) ? (string) $data['error_description'] : null; + } +} diff --git a/src/Service/SsoOidc/src/Exception/InternalServerException.php b/src/Service/SsoOidc/src/Exception/InternalServerException.php new file mode 100644 index 000000000..be92c667c --- /dev/null +++ b/src/Service/SsoOidc/src/Exception/InternalServerException.php @@ -0,0 +1,45 @@ +error; + } + + public function getError_description(): ?string + { + return $this->error_description; + } + + protected function populateResult(ResponseInterface $response): void + { + $data = $response->toArray(false); + + $this->error = isset($data['error']) ? (string) $data['error'] : null; + $this->error_description = isset($data['error_description']) ? (string) $data['error_description'] : null; + } +} diff --git a/src/Service/SsoOidc/src/Exception/InvalidClientException.php b/src/Service/SsoOidc/src/Exception/InvalidClientException.php new file mode 100644 index 000000000..1e955c10a --- /dev/null +++ b/src/Service/SsoOidc/src/Exception/InvalidClientException.php @@ -0,0 +1,46 @@ +error; + } + + public function getError_description(): ?string + { + return $this->error_description; + } + + protected function populateResult(ResponseInterface $response): void + { + $data = $response->toArray(false); + + $this->error = isset($data['error']) ? (string) $data['error'] : null; + $this->error_description = isset($data['error_description']) ? (string) $data['error_description'] : null; + } +} diff --git a/src/Service/SsoOidc/src/Exception/InvalidGrantException.php b/src/Service/SsoOidc/src/Exception/InvalidGrantException.php new file mode 100644 index 000000000..6628b51ee --- /dev/null +++ b/src/Service/SsoOidc/src/Exception/InvalidGrantException.php @@ -0,0 +1,46 @@ +error; + } + + public function getError_description(): ?string + { + return $this->error_description; + } + + protected function populateResult(ResponseInterface $response): void + { + $data = $response->toArray(false); + + $this->error = isset($data['error']) ? (string) $data['error'] : null; + $this->error_description = isset($data['error_description']) ? (string) $data['error_description'] : null; + } +} diff --git a/src/Service/SsoOidc/src/Exception/InvalidRequestException.php b/src/Service/SsoOidc/src/Exception/InvalidRequestException.php new file mode 100644 index 000000000..01f0e98ab --- /dev/null +++ b/src/Service/SsoOidc/src/Exception/InvalidRequestException.php @@ -0,0 +1,46 @@ +error; + } + + public function getError_description(): ?string + { + return $this->error_description; + } + + protected function populateResult(ResponseInterface $response): void + { + $data = $response->toArray(false); + + $this->error = isset($data['error']) ? (string) $data['error'] : null; + $this->error_description = isset($data['error_description']) ? (string) $data['error_description'] : null; + } +} diff --git a/src/Service/SsoOidc/src/Exception/InvalidScopeException.php b/src/Service/SsoOidc/src/Exception/InvalidScopeException.php new file mode 100644 index 000000000..3674ad784 --- /dev/null +++ b/src/Service/SsoOidc/src/Exception/InvalidScopeException.php @@ -0,0 +1,45 @@ +error; + } + + public function getError_description(): ?string + { + return $this->error_description; + } + + protected function populateResult(ResponseInterface $response): void + { + $data = $response->toArray(false); + + $this->error = isset($data['error']) ? (string) $data['error'] : null; + $this->error_description = isset($data['error_description']) ? (string) $data['error_description'] : null; + } +} diff --git a/src/Service/SsoOidc/src/Exception/SlowDownException.php b/src/Service/SsoOidc/src/Exception/SlowDownException.php new file mode 100644 index 000000000..6bea42c57 --- /dev/null +++ b/src/Service/SsoOidc/src/Exception/SlowDownException.php @@ -0,0 +1,45 @@ +error; + } + + public function getError_description(): ?string + { + return $this->error_description; + } + + protected function populateResult(ResponseInterface $response): void + { + $data = $response->toArray(false); + + $this->error = isset($data['error']) ? (string) $data['error'] : null; + $this->error_description = isset($data['error_description']) ? (string) $data['error_description'] : null; + } +} diff --git a/src/Service/SsoOidc/src/Exception/UnauthorizedClientException.php b/src/Service/SsoOidc/src/Exception/UnauthorizedClientException.php new file mode 100644 index 000000000..25075e315 --- /dev/null +++ b/src/Service/SsoOidc/src/Exception/UnauthorizedClientException.php @@ -0,0 +1,46 @@ +error; + } + + public function getError_description(): ?string + { + return $this->error_description; + } + + protected function populateResult(ResponseInterface $response): void + { + $data = $response->toArray(false); + + $this->error = isset($data['error']) ? (string) $data['error'] : null; + $this->error_description = isset($data['error_description']) ? (string) $data['error_description'] : null; + } +} diff --git a/src/Service/SsoOidc/src/Exception/UnsupportedGrantTypeException.php b/src/Service/SsoOidc/src/Exception/UnsupportedGrantTypeException.php new file mode 100644 index 000000000..cfa66eef4 --- /dev/null +++ b/src/Service/SsoOidc/src/Exception/UnsupportedGrantTypeException.php @@ -0,0 +1,45 @@ +error; + } + + public function getError_description(): ?string + { + return $this->error_description; + } + + protected function populateResult(ResponseInterface $response): void + { + $data = $response->toArray(false); + + $this->error = isset($data['error']) ? (string) $data['error'] : null; + $this->error_description = isset($data['error_description']) ? (string) $data['error_description'] : null; + } +} diff --git a/src/Service/SsoOidc/src/Input/CreateTokenRequest.php b/src/Service/SsoOidc/src/Input/CreateTokenRequest.php new file mode 100644 index 000000000..9ea9c93f2 --- /dev/null +++ b/src/Service/SsoOidc/src/Input/CreateTokenRequest.php @@ -0,0 +1,328 @@ +clientId = $input['clientId'] ?? null; + $this->clientSecret = $input['clientSecret'] ?? null; + $this->grantType = $input['grantType'] ?? null; + $this->deviceCode = $input['deviceCode'] ?? null; + $this->code = $input['code'] ?? null; + $this->refreshToken = $input['refreshToken'] ?? null; + $this->scope = $input['scope'] ?? null; + $this->redirectUri = $input['redirectUri'] ?? null; + $this->codeVerifier = $input['codeVerifier'] ?? null; + parent::__construct($input); + } + + /** + * @param array{ + * clientId?: string, + * clientSecret?: string, + * grantType?: string, + * deviceCode?: null|string, + * code?: null|string, + * refreshToken?: null|string, + * scope?: null|string[], + * redirectUri?: null|string, + * codeVerifier?: null|string, + * '@region'?: string|null, + * }|CreateTokenRequest $input + */ + public static function create($input): self + { + return $input instanceof self ? $input : new self($input); + } + + public function getClientId(): ?string + { + return $this->clientId; + } + + public function getClientSecret(): ?string + { + return $this->clientSecret; + } + + public function getCode(): ?string + { + return $this->code; + } + + public function getCodeVerifier(): ?string + { + return $this->codeVerifier; + } + + public function getDeviceCode(): ?string + { + return $this->deviceCode; + } + + public function getGrantType(): ?string + { + return $this->grantType; + } + + public function getRedirectUri(): ?string + { + return $this->redirectUri; + } + + public function getRefreshToken(): ?string + { + return $this->refreshToken; + } + + /** + * @return string[] + */ + public function getScope(): array + { + return $this->scope ?? []; + } + + /** + * @internal + */ + public function request(): Request + { + // Prepare headers + $headers = [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]; + + // Prepare query + $query = []; + + // Prepare URI + $uriString = '/token'; + + // Prepare Body + $bodyPayload = $this->requestBody(); + $body = empty($bodyPayload) ? '{}' : json_encode($bodyPayload, 4194304); + + // Return the Request + return new Request('POST', $uriString, $query, $headers, StreamFactory::create($body)); + } + + public function setClientId(?string $value): self + { + $this->clientId = $value; + + return $this; + } + + public function setClientSecret(?string $value): self + { + $this->clientSecret = $value; + + return $this; + } + + public function setCode(?string $value): self + { + $this->code = $value; + + return $this; + } + + public function setCodeVerifier(?string $value): self + { + $this->codeVerifier = $value; + + return $this; + } + + public function setDeviceCode(?string $value): self + { + $this->deviceCode = $value; + + return $this; + } + + public function setGrantType(?string $value): self + { + $this->grantType = $value; + + return $this; + } + + public function setRedirectUri(?string $value): self + { + $this->redirectUri = $value; + + return $this; + } + + public function setRefreshToken(?string $value): self + { + $this->refreshToken = $value; + + return $this; + } + + /** + * @param string[] $value + */ + public function setScope(array $value): self + { + $this->scope = $value; + + return $this; + } + + private function requestBody(): array + { + $payload = []; + if (null === $v = $this->clientId) { + throw new InvalidArgument(\sprintf('Missing parameter "clientId" for "%s". The value cannot be null.', __CLASS__)); + } + $payload['clientId'] = $v; + if (null === $v = $this->clientSecret) { + throw new InvalidArgument(\sprintf('Missing parameter "clientSecret" for "%s". The value cannot be null.', __CLASS__)); + } + $payload['clientSecret'] = $v; + if (null === $v = $this->grantType) { + throw new InvalidArgument(\sprintf('Missing parameter "grantType" for "%s". The value cannot be null.', __CLASS__)); + } + $payload['grantType'] = $v; + if (null !== $v = $this->deviceCode) { + $payload['deviceCode'] = $v; + } + if (null !== $v = $this->code) { + $payload['code'] = $v; + } + if (null !== $v = $this->refreshToken) { + $payload['refreshToken'] = $v; + } + if (null !== $v = $this->scope) { + $index = -1; + $payload['scope'] = []; + foreach ($v as $listValue) { + ++$index; + $payload['scope'][$index] = $listValue; + } + } + if (null !== $v = $this->redirectUri) { + $payload['redirectUri'] = $v; + } + if (null !== $v = $this->codeVerifier) { + $payload['codeVerifier'] = $v; + } + + return $payload; + } +} diff --git a/src/Service/SsoOidc/src/Result/CreateTokenResponse.php b/src/Service/SsoOidc/src/Result/CreateTokenResponse.php new file mode 100644 index 000000000..0de40f3a0 --- /dev/null +++ b/src/Service/SsoOidc/src/Result/CreateTokenResponse.php @@ -0,0 +1,101 @@ +initialize(); + + return $this->accessToken; + } + + public function getExpiresIn(): ?int + { + $this->initialize(); + + return $this->expiresIn; + } + + public function getIdToken(): ?string + { + $this->initialize(); + + return $this->idToken; + } + + public function getRefreshToken(): ?string + { + $this->initialize(); + + return $this->refreshToken; + } + + public function getTokenType(): ?string + { + $this->initialize(); + + return $this->tokenType; + } + + protected function populateResult(Response $response): void + { + $data = $response->toArray(); + + $this->accessToken = isset($data['accessToken']) ? (string) $data['accessToken'] : null; + $this->tokenType = isset($data['tokenType']) ? (string) $data['tokenType'] : null; + $this->expiresIn = isset($data['expiresIn']) ? (int) $data['expiresIn'] : null; + $this->refreshToken = isset($data['refreshToken']) ? (string) $data['refreshToken'] : null; + $this->idToken = isset($data['idToken']) ? (string) $data['idToken'] : null; + } +} diff --git a/src/Service/SsoOidc/src/SsoOidcClient.php b/src/Service/SsoOidc/src/SsoOidcClient.php new file mode 100644 index 000000000..56c9881bb --- /dev/null +++ b/src/Service/SsoOidc/src/SsoOidcClient.php @@ -0,0 +1,141 @@ +getResponse($input->request(), new RequestContext(['operation' => 'CreateToken', 'region' => $input->getRegion(), 'exceptionMapping' => [ + 'InvalidRequestException' => InvalidRequestException::class, + 'InvalidClientException' => InvalidClientException::class, + 'InvalidGrantException' => InvalidGrantException::class, + 'UnauthorizedClientException' => UnauthorizedClientException::class, + 'UnsupportedGrantTypeException' => UnsupportedGrantTypeException::class, + 'InvalidScopeException' => InvalidScopeException::class, + 'AuthorizationPendingException' => AuthorizationPendingException::class, + 'SlowDownException' => SlowDownException::class, + 'AccessDeniedException' => AccessDeniedException::class, + 'ExpiredTokenException' => ExpiredTokenException::class, + 'InternalServerException' => InternalServerException::class, + ]])); + + return new CreateTokenResponse($response); + } + + protected function getAwsErrorFactory(): AwsErrorFactoryInterface + { + return new JsonRestAwsErrorFactory(); + } + + protected function getEndpointMetadata(?string $region): array + { + if (null === $region) { + $region = Configuration::DEFAULT_REGION; + } + + switch ($region) { + case 'af-south-1': + case 'ap-east-1': + case 'ap-northeast-1': + case 'ap-northeast-2': + case 'ap-northeast-3': + case 'ap-south-1': + case 'ap-south-2': + case 'ap-southeast-1': + case 'ap-southeast-2': + case 'ap-southeast-3': + case 'ap-southeast-4': + case 'ca-central-1': + case 'ca-west-1': + case 'eu-central-1': + case 'eu-central-2': + case 'eu-north-1': + case 'eu-south-1': + case 'eu-south-2': + case 'eu-west-1': + case 'eu-west-2': + case 'eu-west-3': + case 'il-central-1': + case 'me-central-1': + case 'me-south-1': + case 'sa-east-1': + case 'us-east-1': + case 'us-east-2': + case 'us-gov-east-1': + case 'us-gov-west-1': + case 'us-west-1': + case 'us-west-2': + return [ + 'endpoint' => "https://oidc.$region.amazonaws.com", + 'signRegion' => $region, + 'signService' => 'sso-oauth', + 'signVersions' => ['v4'], + ]; + case 'cn-north-1': + case 'cn-northwest-1': + return [ + 'endpoint' => "https://oidc.$region.amazonaws.com.cn", + 'signRegion' => $region, + 'signService' => 'sso-oauth', + 'signVersions' => ['v4'], + ]; + } + + throw new UnsupportedRegion(\sprintf('The region "%s" is not supported by "SsoOidc".', $region)); + } +} diff --git a/src/Service/SsoOidc/tests/.gitignore b/src/Service/SsoOidc/tests/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/src/Service/SsoOidc/tests/Integration/SsoOidcClientTest.php b/src/Service/SsoOidc/tests/Integration/SsoOidcClientTest.php new file mode 100644 index 000000000..4a2b0584d --- /dev/null +++ b/src/Service/SsoOidc/tests/Integration/SsoOidcClientTest.php @@ -0,0 +1,46 @@ +getClient(); + + $input = new CreateTokenRequest([ + 'clientId' => 'change me', + 'clientSecret' => 'change me', + 'grantType' => 'change me', + 'deviceCode' => 'change me', + 'code' => 'change me', + 'refreshToken' => 'change me', + 'scope' => ['change me'], + 'redirectUri' => 'change me', + 'codeVerifier' => 'change me', + ]); + $result = $client->createToken($input); + + $result->resolve(); + + self::assertSame('changeIt', $result->getAccessToken()); + self::assertSame('changeIt', $result->getTokenType()); + self::assertSame(1337, $result->getExpiresIn()); + self::assertSame('changeIt', $result->getRefreshToken()); + self::assertSame('changeIt', $result->getIdToken()); + } + + private function getClient(): SsoOidcClient + { + self::markTestSkipped('There is no image available for a SSO provider mock.'); + + return new SsoOidcClient([ + 'endpoint' => 'http://localhost', + ], new NullProvider()); + } +} diff --git a/src/Service/SsoOidc/tests/Unit/Input/CreateTokenRequestTest.php b/src/Service/SsoOidc/tests/Unit/Input/CreateTokenRequestTest.php new file mode 100644 index 000000000..4c490636b --- /dev/null +++ b/src/Service/SsoOidc/tests/Unit/Input/CreateTokenRequestTest.php @@ -0,0 +1,35 @@ + '_yzkThXVzLWVhc3QtMQEXAMPLECLIENTID', + 'clientSecret' => 'VERYLONGSECRETeyJraWQiOiJrZXktMTU2NDAyODA5OSIsImFsZyI6IkhTMzg0In0', + 'grantType' => 'urn:ietf:params:oauth:grant-type:device-code', + 'deviceCode' => 'yJraWQiOiJrZXktMTU2Njk2ODA4OCIsImFsZyI6IkhTMzIn0EXAMPLEDEVICECODE', + ]); + + // see example-1.json from SDK + $expected = ' + POST /token HTTP/1.0 + Content-Type: application/json + Accept: application/json + + { + "clientId": "_yzkThXVzLWVhc3QtMQEXAMPLECLIENTID", + "clientSecret": "VERYLONGSECRETeyJraWQiOiJrZXktMTU2NDAyODA5OSIsImFsZyI6IkhTMzg0In0", + "deviceCode": "yJraWQiOiJrZXktMTU2Njk2ODA4OCIsImFsZyI6IkhTMzIn0EXAMPLEDEVICECODE", + "grantType": "urn:ietf:params:oauth:grant-type:device-code" + } + '; + + self::assertRequestEqualsHttpRequest($expected, $input->request()); + } +} diff --git a/src/Service/SsoOidc/tests/Unit/Result/CreateTokenResponseTest.php b/src/Service/SsoOidc/tests/Unit/Result/CreateTokenResponseTest.php new file mode 100644 index 000000000..ce092a800 --- /dev/null +++ b/src/Service/SsoOidc/tests/Unit/Result/CreateTokenResponseTest.php @@ -0,0 +1,32 @@ +request('POST', 'http://localhost'), $client, new NullLogger())); + + self::assertSame('aoal-YigITUDiNX1xZwOMXM5MxOWDL0E0jg9P6_C_jKQPxS_SKCP6f0kh1Up4g7TtvQqkMnD-GJiU_S1gvug6SrggAkc0:MGYCMQD3IatVjV7jAJU91kK3PkS/SfA2wtgWzOgZWDOR7sDGN9t0phCZz5It/aes/3C1Zj0CMQCKWOgRaiz6AIhza3DSXQNMLjRKXC8F8ceCsHlgYLMZ7hZidEXAMPLEACCESSTOKEN', $result->getAccessToken()); + self::assertSame('Bearer', $result->getTokenType()); + self::assertSame(1579729529, $result->getExpiresIn()); + self::assertSame('aorvJYubGpU6i91YnH7Mfo-AT2fIVa1zCfA_Rvq9yjVKIP3onFmmykuQ7E93y2I-9Nyj-A_sVvMufaLNL0bqnDRtgAkc0:MGUCMFrRsktMRVlWaOR70XGMFGLL0SlcCw4DiYveIiOVx1uK9BbD0gvAddsW3UTLozXKMgIxAJ3qxUvjpnlLIOaaKOoa/FuNgqJVvr9GMwDtnAtlh9iZzAkEXAMPLEREFRESHTOKEN', $result->getRefreshToken()); + } +} diff --git a/src/Service/SsoOidc/tests/Unit/SsoOidcClientTest.php b/src/Service/SsoOidc/tests/Unit/SsoOidcClientTest.php new file mode 100644 index 000000000..bc11f46d4 --- /dev/null +++ b/src/Service/SsoOidc/tests/Unit/SsoOidcClientTest.php @@ -0,0 +1,28 @@ + 'change me', + 'clientSecret' => 'change me', + 'grantType' => 'change me', + ]); + $result = $client->createToken($input); + + self::assertInstanceOf(CreateTokenResponse::class, $result); + self::assertFalse($result->info()['resolved']); + } +}