Repository: steverhoades/oauth2-openid-connect-server Branch: master Commit: f20665aeeeec Files: 36 Total size: 57.8 KB Directory structure: gitextract_m4beu0jy/ ├── .gitattributes ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .scrutinizer.yml ├── LICENSE ├── README.md ├── composer.json ├── examples/ │ ├── README.md │ ├── composer.json │ ├── public/ │ │ ├── auth_code.php │ │ ├── client_credentials.php │ │ ├── implicit.php │ │ └── password.php │ └── src/ │ ├── Entities/ │ │ └── UserEntity.php │ └── Repositories/ │ ├── IdentityRepository.php │ └── ScopeRepository.php ├── phpunit.xml.dist ├── src/ │ ├── ClaimExtractor.php │ ├── Entities/ │ │ ├── ClaimSetEntity.php │ │ ├── ClaimSetEntityInterface.php │ │ ├── ClaimSetInterface.php │ │ └── ScopeInterface.php │ ├── Exception/ │ │ └── InvalidArgumentException.php │ ├── IdTokenResponse.php │ └── Repositories/ │ ├── ClaimSetRepositoryInterface.php │ └── IdentityProviderInterface.php └── tests/ ├── Bootstrap.php ├── ClaimExtractorTest.php ├── ResponseTypes/ │ └── IdTokenResponseTest.php └── Stubs/ ├── IdentityProvider.php ├── UserEntity.php ├── UserNoClaimSetEntity.php ├── UserNoIdentifierEntity.php ├── private.key ├── private.key.crlf └── public.key ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ * text=auto /tests export-ignore /.gitattributes export-ignore /.gitignore export-ignore /.github export-ignore .scrutinizer.yml export-ignore /phpunit.xml.dist export-ignore /README.md export-ignore ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - master - develop pull_request: workflow_dispatch: jobs: check_composer: name: Check composer.json runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: coverage: none php-version: '8.3' - run: composer validate --strict --no-check-lock tests: name: "Tests on PHP ${{ matrix.php }}" runs-on: ubuntu-latest strategy: fail-fast: false matrix: php: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: coverage: "none" php-version: "${{ matrix.php }}" ini-file: development - name: Update permissions run: | chmod 600 tests/Stubs/private.key chmod 600 tests/Stubs/public.key - name: Install dependencies run: composer update --ansi --no-progress --no-interaction - name: Run tests run: php -d error_reporting="E_ALL & ~E_USER_DEPRECATED" vendor/bin/phpunit -v --colors=always ================================================ FILE: .gitignore ================================================ /vendor/ phpunit.xml /build composer.lock /examples/*.key ================================================ FILE: .scrutinizer.yml ================================================ build: environment: php: 7.4.30 filter: excluded_paths: - tests/* - vendor/* - examples/* checks: php: code_rating: true remove_extra_empty_lines: true remove_php_closing_tag: true remove_trailing_whitespace: true fix_use_statements: remove_unused: true preserve_multiple: false preserve_blanklines: true order_alphabetically: true fix_php_opening_tag: true fix_linefeed: true fix_line_ending: true fix_identation_4spaces: true fix_doc_comments: true tools: external_code_coverage: timeout: 1800 php_code_coverage: false php_code_sniffer: config: standard: PSR2 filter: paths: ['src'] php_loc: enabled: true excluded_dirs: [vendor, tests, examples] php_cpd: enabled: true excluded_dirs: [vendor, tests, examples] ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2018 Steve Rhoades 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. ================================================ FILE: README.md ================================================ # OAuth 2.0 OpenID Connect Server [![Build Status](https://travis-ci.org/steverhoades/oauth2-openid-connect-server.svg?branch=master)](https://travis-ci.org/steverhoades/oauth2-openid-connect-server) [![Code Coverage](https://scrutinizer-ci.com/g/steverhoades/oauth2-openid-connect-server/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/steverhoades/oauth2-openid-connect-server/?branch=master) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/steverhoades/oauth2-openid-connect-server/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/steverhoades/oauth2-openid-connect-server/?branch=master) This implements the OpenID Connect specification on top of The PHP League's [OAuth2 Server](https://github.com/thephpleague/oauth2-server). ## Requirements * Requires PHP version 7.4 or greater. * [league/oauth2-server](https://github.com/thephpleague/oauth2-server) 8.4.2 or greater. Note: league/oauth2-server version may have a higher PHP requirement. ## Usage The following classes will need to be configured and passed to the AuthorizationServer in order to provide OpenID Connect functionality. 1. IdentityRepository. This MUST implement the OpenIDConnectServer\Repositories\IdentityProviderInterface and return the identity of the user based on the return value of $accessToken->getUserIdentifier(). 1. The IdentityRepository MUST return a UserEntity that implements the following interfaces 1. OpenIDConnectServer\Entities\ClaimSetInterface 1. League\OAuth2\Server\Entities\UserEntityInterface. 1. ClaimSet. ClaimSet is a way to associate claims to a given scope. 1. ClaimExtractor. The ClaimExtractor takes an array of ClaimSets and in addition provides default claims for the OpenID Connect specified scopes of: profile, email, phone and address. 1. IdTokenResponse. This class must be passed to the AuthorizationServer during construction and is responsible for adding the id_token to the response. 1. ScopeRepository. The getScopeEntityByIdentifier($identifier) method must return a ScopeEntity for the `openid` scope in order to enable support. See examples. ### Example Configuration ```php // Init Repositories $clientRepository = new ClientRepository(); $scopeRepository = new ScopeRepository(); $accessTokenRepository = new AccessTokenRepository(); $authCodeRepository = new AuthCodeRepository(); $refreshTokenRepository = new RefreshTokenRepository(); $privateKeyPath = 'file://' . __DIR__ . '/../private.key'; $publicKeyPath = 'file://' . __DIR__ . '/../public.key'; // OpenID Connect Response Type $responseType = new IdTokenResponse(new IdentityRepository(), new ClaimExtractor()); // Setup the authorization server $server = new \League\OAuth2\Server\AuthorizationServer( $clientRepository, $accessTokenRepository, $scopeRepository, $privateKey, $publicKey, $responseType ); $grant = new \League\OAuth2\Server\Grant\AuthCodeGrant( $authCodeRepository, $refreshTokenRepository, new \DateInterval('PT10M') // authorization codes will expire after 10 minutes ); $grant->setRefreshTokenTTL(new \DateInterval('P1M')); // refresh tokens will expire after 1 month // Enable the authentication code grant on the server $server->enableGrantType( $grant, new \DateInterval('PT1H') // access tokens will expire after 1 hour ); return $server; ``` After the server has been configured it should be used as described in the [OAuth2 Server documentation](https://oauth2.thephpleague.com/). ## UserEntity In order for this library to work properly you will need to add your IdentityProvider to the IdTokenResponse object. This will be used internally to lookup a UserEntity by it's identifier. Additionally your UserEntity must implement the ClaimSetInterface which includes a single method getClaims(). The getClaims() method should return a list of attributes as key/value pairs that can be returned if the proper scope has been defined. ```php use League\OAuth2\Server\Entities\Traits\EntityTrait; use League\OAuth2\Server\Entities\UserEntityInterface; use OpenIDConnectServer\Entities\ClaimSetInterface; class UserEntity implements UserEntityInterface, ClaimSetInterface { use EntityTrait; protected $attributes; public function getClaims() { return $this->attributes; } } ``` ## ClaimSets A ClaimSet is a scope that defines a list of claims. ```php // Example of the profile ClaimSet $claimSet = new ClaimSetEntity('profile', [ 'name', 'family_name', 'given_name', 'middle_name', 'nickname', 'preferred_username', 'profile', 'picture', 'website', 'gender', 'birthdate', 'zoneinfo', 'locale', 'updated_at' ]); ``` As you can see from the above, profile lists a set of claims that can be extracted from our UserEntity if the profile scope is included with the authorization request. ### Adding Custom ClaimSets At some point you will likely want to include your own group of custom claims. To do this you will need to create a ClaimSetEntity, give it a scope (the value you will include in the scope parameter of your OAuth2 request) and the list of claims it supports. ```php $extractor = new ClaimExtractor(); // Create your custom scope $claimSet = new ClaimSetEntity('company', [ 'company_name', 'company_phone', 'company_address' ]); // Add it to the ClaimExtract (this is what you pass to IdTokenResponse, see configuration above) $extractor->addClaimSet($claimSet); ``` Now, when you pass the company scope with your request it will attempt to locate those properties from your UserEntity::getClaims(). ## Install Via Composer ``` bash $ composer require steverhoades/oauth2-openid-connect-server ``` ## Testing To run the unit tests you will need to require league/oauth2-server from the source as this repository utilizes some of their existing test infrastructure. ```bash $ composer require league/oauth2-server --prefer-source ``` Run PHPUnit from the root directory: ```bash $ vendor/bin/phpunit ``` ## License The MIT License (MIT). Please see [License File](https://github.com/steverhoades/oauth2-openid-connect-client/blob/master/LICENSE) for more information. [PSR-1]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md [PSR-2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md [PSR-4]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md ================================================ FILE: composer.json ================================================ { "name": "steverhoades/oauth2-openid-connect-server", "description": "An OpenID Connect Server that sites on The PHP League's OAuth2 Server", "license": "MIT", "authors": [ { "name": "Steve Rhoades", "email": "sedonami@gmail.com" } ], "require": { "php": ">=7.4", "league/oauth2-server": "^8.4.2|^9.0", "lcobucci/jwt": "4.1.5|^4.2|^4.3|^5.0" }, "require-dev": { "phpunit/phpunit": "^5.0|^9.5", "laminas/laminas-diactoros": "^1.3.2 || ^3.3" }, "autoload": { "psr-4": { "OpenIDConnectServer\\": "src/" } }, "autoload-dev": { "psr-4": { "OpenIDConnectServer\\Test\\": "tests/", "LeagueTests\\": "vendor/league/oauth2-server/tests/" } }, "config": { "preferred-install": { "league/oauth2-server": "source" } } } ================================================ FILE: examples/README.md ================================================ # OpenID Connect Example Implementations The following examples piggyback off the PHP Leagues OAuth2 Server examples. Please follow the instructions below carefully. ## Installation 0. Run `composer install --prefer-source` in this directory to install dependencies 0. Create a private key `openssl genrsa -out private.key 2048` 0. Create a public key `openssl rsa -in private.key -pubout > public.key` 0. Change permissions of the .key files or a PHP Notice will be thrown `chmod 660 *.key` 0. `cd` into the public directory 0. Start a PHP server `php -S localhost:4444` ## Testing the client credentials grant example Send the following cURL request: ``` curl -X "POST" "http://localhost:4444/client_credentials.php/access_token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Accept: 1.0" \ --data-urlencode "grant_type=client_credentials" \ --data-urlencode "client_id=myawesomeapp" \ --data-urlencode "client_secret=abc123" \ --data-urlencode "scope=openid email" ``` ## Testing the password grant example Send the following cURL request: ``` curl -X "POST" "http://localhost:4444/password.php/access_token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Accept: 1.0" \ --data-urlencode "grant_type=password" \ --data-urlencode "client_id=myawesomeapp" \ --data-urlencode "client_secret=abc123" \ --data-urlencode "username=alex" \ --data-urlencode "password=whisky" \ --data-urlencode "scope=openid email" ``` ================================================ FILE: examples/composer.json ================================================ { "require": { "slim/slim": "3.0.*", "league/oauth2-server": "^7.0" }, "require-dev": { "league/event": "^2.1", "lcobucci/jwt": "^3.1", "paragonie/random_compat": "^2.0", "psr/http-message": "^1.0", "defuse/php-encryption": "^2.1", "zendframework/zend-diactoros": "^1.0" }, "autoload": { "psr-4": { "OpenIDConnectServerExamples\\": "src/", "OpenIDConnectServer\\": "../src/", "OAuth2ServerExamples\\": "vendor/league/oauth2-server/examples/src" } } } ================================================ FILE: examples/public/auth_code.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\AuthCodeGrant; use OAuth2ServerExamples\Entities\UserEntity; use OAuth2ServerExamples\Repositories\AccessTokenRepository; use OAuth2ServerExamples\Repositories\AuthCodeRepository; use OAuth2ServerExamples\Repositories\ClientRepository; use OAuth2ServerExamples\Repositories\RefreshTokenRepository; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\App; use Laminas\Diactoros\Stream; use OpenIDConnectServer\IdTokenResponse; use OpenIDConnectServerExamples\Repositories\IdentityRepository; use OpenIDConnectServerExamples\Repositories\ScopeRepository; use OpenIDConnectServer\ClaimExtractor; include __DIR__ . '/../vendor/autoload.php'; $app = new App([ 'settings' => [ 'displayErrorDetails' => true, ], AuthorizationServer::class => function () { // Init our repositories $clientRepository = new ClientRepository(); $scopeRepository = new ScopeRepository(); $accessTokenRepository = new AccessTokenRepository(); $authCodeRepository = new AuthCodeRepository(); $refreshTokenRepository = new RefreshTokenRepository(); $privateKeyPath = 'file://' . __DIR__ . '/../private.key'; // OpenID Connect Response Type $responseType = new IdTokenResponse(new IdentityRepository(), new ClaimExtractor()); // Setup the authorization server $server = new AuthorizationServer( $clientRepository, $accessTokenRepository, $scopeRepository, $privateKeyPath, 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen', $responseType ); // Enable the authentication code grant on the server with a token TTL of 1 hour $server->enableGrantType( new AuthCodeGrant( $authCodeRepository, $refreshTokenRepository, new \DateInterval('PT10M') ), new \DateInterval('PT1H') ); return $server; }, ]); $app->get('/authorize', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { /* @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get(AuthorizationServer::class); try { // Validate the HTTP request and return an AuthorizationRequest object. // The auth request object can be serialized into a user's session $authRequest = $server->validateAuthorizationRequest($request); // Once the user has logged in set the user on the AuthorizationRequest $authRequest->setUser(new UserEntity()); // Once the user has approved or denied the client update the status // (true = approved, false = denied) $authRequest->setAuthorizationApproved(true); // Return the HTTP redirect response return $server->completeAuthorizationRequest($authRequest, $response); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); } catch (\Exception $exception) { $body = new Stream('php://temp', 'r+'); $body->write($exception->getMessage()); return $response->withStatus(500)->withBody($body); } }); $app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { /* @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get(AuthorizationServer::class); try { return $server->respondToAccessTokenRequest($request, $response); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); } catch (\Exception $exception) { $body = new Stream('php://temp', 'r+'); $body->write($exception->getMessage()); return $response->withStatus(500)->withBody($body); } }); $app->run(); ================================================ FILE: examples/public/client_credentials.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\OAuthServerException; use OAuth2ServerExamples\Repositories\AccessTokenRepository; use OAuth2ServerExamples\Repositories\ClientRepository; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\App; use Laminas\Diactoros\Stream; use OpenIDConnectServer\IdTokenResponse; use OpenIDConnectServerExamples\Repositories\IdentityRepository; use OpenIDConnectServerExamples\Repositories\ScopeRepository; use OpenIDConnectServer\ClaimExtractor; include __DIR__ . '/../vendor/autoload.php'; $app = new App([ 'settings' => [ 'displayErrorDetails' => true, ], AuthorizationServer::class => function () { // Init our repositories $clientRepository = new ClientRepository(); // instance of ClientRepositoryInterface $scopeRepository = new ScopeRepository(); // instance of ScopeRepositoryInterface $accessTokenRepository = new AccessTokenRepository(); // instance of AccessTokenRepositoryInterface // Path to public and private keys $privateKey = 'file://' . __DIR__ . '/../private.key'; //$privateKey = new CryptKey('file://path/to/private.key', 'passphrase'); // if private key has a pass phrase // OpenID Connect Response Type $responseType = new IdTokenResponse(new IdentityRepository(), new ClaimExtractor()); // Setup the authorization server $server = new AuthorizationServer( $clientRepository, $accessTokenRepository, $scopeRepository, $privateKey, 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen', $responseType ); // Enable the client credentials grant on the server $server->enableGrantType( new \League\OAuth2\Server\Grant\ClientCredentialsGrant(), new \DateInterval('PT1H') // access tokens will expire after 1 hour ); return $server; }, ]); $app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { /* @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get(AuthorizationServer::class); try { // Try to respond to the request return $server->respondToAccessTokenRequest($request, $response); } catch (OAuthServerException $exception) { // All instances of OAuthServerException can be formatted into a HTTP response return $exception->generateHttpResponse($response); } catch (\Exception $exception) { // Unknown exception $body = new Stream('php://temp', 'r+'); $body->write($exception->getMessage()); return $response->withStatus(500)->withBody($body); } }); $app->run(); ================================================ FILE: examples/public/implicit.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\ImplicitGrant; use OAuth2ServerExamples\Entities\UserEntity; use OAuth2ServerExamples\Repositories\AccessTokenRepository; use OAuth2ServerExamples\Repositories\ClientRepository; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\App; use Laminas\Diactoros\Stream; use OpenIDConnectServer\IdTokenResponse; use OpenIDConnectServerExamples\Repositories\IdentityRepository; use OpenIDConnectServerExamples\Repositories\ScopeRepository; use OpenIDConnectServer\ClaimExtractor; include __DIR__ . '/../vendor/autoload.php'; $app = new App([ 'settings' => [ 'displayErrorDetails' => true, ], AuthorizationServer::class => function () { // Init our repositories $clientRepository = new ClientRepository(); $scopeRepository = new ScopeRepository(); $accessTokenRepository = new AccessTokenRepository(); $privateKeyPath = 'file://' . __DIR__ . '/../private.key'; // OpenID Connect Response Type $responseType = new IdTokenResponse(new IdentityRepository(), new ClaimExtractor()); // Setup the authorization server $server = new AuthorizationServer( $clientRepository, $accessTokenRepository, $scopeRepository, $privateKeyPath, 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen', $responseType ); // Enable the implicit grant on the server with a token TTL of 1 hour $server->enableGrantType(new ImplicitGrant(new \DateInterval('PT1H'))); return $server; }, ]); $app->get('/authorize', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { /* @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get(AuthorizationServer::class); try { // Validate the HTTP request and return an AuthorizationRequest object. // The auth request object can be serialized into a user's session $authRequest = $server->validateAuthorizationRequest($request); // Once the user has logged in set the user on the AuthorizationRequest $authRequest->setUser(new UserEntity()); // Once the user has approved or denied the client update the status // (true = approved, false = denied) $authRequest->setAuthorizationApproved(true); // Return the HTTP redirect response return $server->completeAuthorizationRequest($authRequest, $response); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); } catch (\Exception $exception) { $body = new Stream('php://temp', 'r+'); $body->write($exception->getMessage()); return $response->withStatus(500)->withBody($body); } }); $app->run(); ================================================ FILE: examples/public/password.php ================================================ function () { // OpenID Connect Response Type $responseType = new IdTokenResponse(new IdentityRepository(), new ClaimExtractor()); // Setup the authorization server $server = new AuthorizationServer( new ClientRepository(), // instance of ClientRepositoryInterface new AccessTokenRepository(), // instance of AccessTokenRepositoryInterface new ScopeRepository(), // instance of ScopeRepositoryInterface 'file://' . __DIR__ . '/../private.key', // path to private key 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen', // encryption key $responseType ); $grant = new PasswordGrant( new UserRepository(), // instance of UserRepositoryInterface new RefreshTokenRepository() // instance of RefreshTokenRepositoryInterface ); $grant->setRefreshTokenTTL(new \DateInterval('P1M')); // refresh tokens will expire after 1 month // Enable the password grant on the server with a token TTL of 1 hour $server->enableGrantType( $grant, new \DateInterval('PT1H') // access tokens will expire after 1 hour ); return $server; }, ]); $app->post( '/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { /* @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get(AuthorizationServer::class); try { // Try to respond to the access token request return $server->respondToAccessTokenRequest($request, $response); } catch (OAuthServerException $exception) { // All instances of OAuthServerException can be converted to a PSR-7 response return $exception->generateHttpResponse($response); } catch (\Exception $exception) { // Catch unexpected exceptions $body = $response->getBody(); $body->write($exception->getMessage()); return $response->withStatus(500)->withBody($body); } } ); $app->run(); ================================================ FILE: examples/src/Entities/UserEntity.php ================================================ 'John Smith', 'family_name' => 'Smith', 'given_name' => 'John', 'middle_name' => 'Doe', 'nickname' => 'JDog', 'preferred_username' => 'jdogsmith77', 'profile' => '', 'picture' => 'avatar.png', 'website' => 'http://www.google.com', 'gender' => 'M', 'birthdate' => '01/01/1990', 'zoneinfo' => '', 'locale' => 'US', 'updated_at' => '01/01/2018', // email 'email' => 'john.doe@example.com', 'email_verified' => true, // phone 'phone_number' => '(866) 555-5555', 'phone_number_verified' => true, // address 'address' => '50 any street, any state, 55555', ]; } } ================================================ FILE: examples/src/Repositories/IdentityRepository.php ================================================ * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace OpenIDConnectServerExamples\Repositories; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use OAuth2ServerExamples\Entities\ScopeEntity; class ScopeRepository extends \OAuth2ServerExamples\Repositories\ScopeRepository { /** * {@inheritdoc} */ public function getScopeEntityByIdentifier($scopeIdentifier) { $scopes = [ // Without this OpenID Connect cannot work. 'openid' => [ 'description' => 'Enable OpenID Connect support' ], 'basic' => [ 'description' => 'Basic details about you', ], 'email' => [ 'description' => 'Your email address', ], ]; if (array_key_exists($scopeIdentifier, $scopes) === false) { return; } $scope = new ScopeEntity(); $scope->setIdentifier($scopeIdentifier); return $scope; } } ================================================ FILE: phpunit.xml.dist ================================================ ./tests/ src ================================================ FILE: src/ClaimExtractor.php ================================================ * @license http://opensource.org/licenses/MIT MIT */ namespace OpenIDConnectServer; use OpenIDConnectServer\Entities\ClaimSetEntity; use OpenIDConnectServer\Entities\ClaimSetEntityInterface; use OpenIDConnectServer\Exception\InvalidArgumentException; use League\OAuth2\Server\Entities\ScopeEntityInterface; class ClaimExtractor { protected $claimSets; protected $protectedClaims = ['profile', 'email', 'address', 'phone']; /** * ClaimExtractor constructor. * @param ClaimSetEntity[] $claimSets */ public function __construct($claimSets = []) { // Add Default OpenID Connect Claims // @see http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims $this->addClaimSet( new ClaimSetEntity('profile', [ 'name', 'family_name', 'given_name', 'middle_name', 'nickname', 'preferred_username', 'profile', 'picture', 'website', 'gender', 'birthdate', 'zoneinfo', 'locale', 'updated_at' ]) ); $this->addClaimSet( new ClaimSetEntity('email', [ 'email', 'email_verified' ]) ); $this->addClaimSet( new ClaimSetEntity('address', [ 'address' ]) ); $this->addClaimSet( new ClaimSetEntity('phone', [ 'phone_number', 'phone_number_verified' ]) ); foreach ($claimSets as $claimSet) { $this->addClaimSet($claimSet); } } /** * @param ClaimSetEntityInterface $claimSet * @return $this * @throws InvalidArgumentException */ public function addClaimSet(ClaimSetEntityInterface $claimSet) { $scope = $claimSet->getScope(); if (in_array($scope, $this->protectedClaims) && !empty($this->claimSets[$scope])) { throw new InvalidArgumentException( sprintf("%s is a protected scope and is pre-defined by the OpenID Connect specification.", $scope) ); } $this->claimSets[$scope] = $claimSet; return $this; } /** * @param string $scope * @return ClaimSetEntity|null */ public function getClaimSet($scope) { if (!$this->hasClaimSet($scope)) { return null; } return $this->claimSets[$scope]; } /** * @param string $scope * @return bool */ public function hasClaimSet($scope) { return array_key_exists($scope, $this->claimSets); } /** * For given scopes and aggregated claims get all claims that have been configured on the extractor. * * @param array $scopes * @param array $claims * @return array */ public function extract(array $scopes, array $claims) { $claimData = []; $keys = array_keys($claims); foreach ($scopes as $scope) { $scopeName = ($scope instanceof ScopeEntityInterface) ? $scope->getIdentifier() : $scope; $claimSet = $this->getClaimSet($scopeName); if (null === $claimSet) { continue; } $intersected = array_intersect($claimSet->getClaims(), $keys); if (empty($intersected)) { continue; } $data = array_filter($claims, function($key) use ($intersected) { return in_array($key, $intersected); }, ARRAY_FILTER_USE_KEY ); $claimData = array_merge($claimData, $data); } return $claimData; } } ================================================ FILE: src/Entities/ClaimSetEntity.php ================================================ * @license http://opensource.org/licenses/MIT MIT */ namespace OpenIDConnectServer\Entities; class ClaimSetEntity implements ClaimSetEntityInterface { protected $scope; protected $claims; public function __construct($scope, array $claims) { $this->scope = $scope; $this->claims = $claims; } public function getScope() { return $this->scope; } public function getClaims() { return $this->claims; } } ================================================ FILE: src/Entities/ClaimSetEntityInterface.php ================================================ * @license http://opensource.org/licenses/MIT MIT */ namespace OpenIDConnectServer\Entities; interface ClaimSetEntityInterface extends ClaimSetInterface, ScopeInterface { } ================================================ FILE: src/Entities/ClaimSetInterface.php ================================================ * @license http://opensource.org/licenses/MIT MIT */ namespace OpenIDConnectServer\Entities; interface ClaimSetInterface { /** * @return array */ public function getClaims(); } ================================================ FILE: src/Entities/ScopeInterface.php ================================================ * @license http://opensource.org/licenses/MIT MIT */ namespace OpenIDConnectServer\Entities; interface ScopeInterface { /** * @return string */ public function getScope(); } ================================================ FILE: src/Exception/InvalidArgumentException.php ================================================ * @license http://opensource.org/licenses/MIT MIT */ namespace OpenIDConnectServer\Exception; class InvalidArgumentException extends \Exception { } ================================================ FILE: src/IdTokenResponse.php ================================================ * @license http://opensource.org/licenses/MIT MIT */ namespace OpenIDConnectServer; use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Key\LocalFileReference; use OpenIDConnectServer\Repositories\IdentityProviderInterface; use OpenIDConnectServer\Entities\ClaimSetInterface; use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\ResponseTypes\BearerTokenResponse; use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\Encoding\ChainedFormatter; use Lcobucci\JWT\Token\Builder; use Lcobucci\JWT\Encoding\JoseEncoder; class IdTokenResponse extends BearerTokenResponse { /** * @var IdentityProviderInterface */ protected $identityProvider; /** * @var ClaimExtractor */ protected $claimExtractor; /** * @var string|null */ protected $keyIdentifier; public function __construct( IdentityProviderInterface $identityProvider, ClaimExtractor $claimExtractor, ?string $keyIdentifier = null ) { $this->identityProvider = $identityProvider; $this->claimExtractor = $claimExtractor; $this->keyIdentifier = $keyIdentifier; } protected function getBuilder(AccessTokenEntityInterface $accessToken, UserEntityInterface $userEntity) { $claimsFormatter = ChainedFormatter::withUnixTimestampDates(); $builder = new Builder(new JoseEncoder(), $claimsFormatter); // Since version 8.0 league/oauth2-server returns \DateTimeImmutable $expiresAt = $accessToken->getExpiryDateTime(); if ($expiresAt instanceof \DateTime) { $expiresAt = \DateTimeImmutable::createFromMutable($expiresAt); } // Add required id_token claims return $builder ->permittedFor($accessToken->getClient()->getIdentifier()) ->issuedBy('https://' . $_SERVER['HTTP_HOST']) ->issuedAt(new \DateTimeImmutable()) ->expiresAt($expiresAt) ->relatedTo($userEntity->getIdentifier()); } /** * @param AccessTokenEntityInterface $accessToken * @return array */ protected function getExtraParams(AccessTokenEntityInterface $accessToken): array { if (false === $this->isOpenIDRequest($accessToken->getScopes())) { return []; } /** @var UserEntityInterface $userEntity */ $userEntity = $this->identityProvider->getUserEntityByIdentifier($accessToken->getUserIdentifier()); if (false === is_a($userEntity, UserEntityInterface::class)) { throw new \RuntimeException('UserEntity must implement UserEntityInterface'); } else if (false === is_a($userEntity, ClaimSetInterface::class)) { throw new \RuntimeException('UserEntity must implement ClaimSetInterface'); } // Add required id_token claims $builder = $this->getBuilder($accessToken, $userEntity); // Need a claim factory here to reduce the number of claims by provided scope. $claims = $this->claimExtractor->extract($accessToken->getScopes(), $userEntity->getClaims()); foreach ($claims as $claimName => $claimValue) { $builder = $builder->withClaim($claimName, $claimValue); } if ($this->keyIdentifier !== null) { $builder = $builder->withHeader('kid', $this->keyIdentifier); } if ( method_exists($this->privateKey, 'getKeyContents') && !empty($this->privateKey->getKeyContents()) ) { $key = InMemory::plainText($this->privateKey->getKeyContents(), (string)$this->privateKey->getPassPhrase()); } else { $key = LocalFileReference::file($this->privateKey->getKeyPath(), (string)$this->privateKey->getPassPhrase()); } $token = $builder->getToken(new Sha256(), $key); return [ 'id_token' => $token->toString() ]; } /** * @param ScopeEntityInterface[] $scopes * @return bool */ private function isOpenIDRequest($scopes) { // Verify scope and make sure openid exists. $valid = false; foreach ($scopes as $scope) { if ($scope->getIdentifier() === 'openid') { $valid = true; break; } } return $valid; } } ================================================ FILE: src/Repositories/ClaimSetRepositoryInterface.php ================================================ * @license http://opensource.org/licenses/MIT MIT */ namespace OpenIDConnectServer\Repositories; interface ClaimSetRepositoryInterface { public function getClaimSetByScopeIdentifier($scopeIdentifier); } ================================================ FILE: src/Repositories/IdentityProviderInterface.php ================================================ * @license http://opensource.org/licenses/MIT MIT */ namespace OpenIDConnectServer\Repositories; use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Repositories\RepositoryInterface; use OpenIDConnectServer\Entities\ClaimSetInterface; interface IdentityProviderInterface extends RepositoryInterface { /** * @return UserEntityInterface&ClaimSetInterface */ public function getUserEntityByIdentifier($identifier); } ================================================ FILE: tests/Bootstrap.php ================================================ wget http://getcomposer.org/composer.phar > php composer.phar install MSG; exit($message); } ================================================ FILE: tests/ClaimExtractorTest.php ================================================ hasClaimSet('profile')); self::assertTrue($extractor->hasClaimSet('email')); self::assertTrue($extractor->hasClaimSet('address')); self::assertTrue($extractor->hasClaimSet('phone')); } public function testCanAddCustomClaimSet() { $claims = new ClaimSetEntity('custom', ['custom_claim']); $extractor = new ClaimExtractor([$claims]); self::assertTrue($extractor->hasClaimSet('custom')); $result = $extractor->extract(['custom'], ['custom_claim' => 'test']); self::assertEquals($result['custom_claim'], 'test'); } public function testCanNotOverrideDefaultScope() { $this->expectException(InvalidArgumentException::class); $claims = new ClaimSetEntity('profile', ['custom_claim']); $extractor = new ClaimExtractor([$claims]); } public function testCanGetClaimSet() { $extractor = new ClaimExtractor(); $claimset = $extractor->getClaimSet('profile'); self::assertEquals($claimset->getScope(), 'profile'); $claimset = $extractor->getClaimSet('unknown'); self::assertNull($claimset); } public function testExtract() { $extractor = new ClaimExtractor(); // no result $result = $extractor->extract(['custom'], ['custom_claim' => 'test']); self::assertEmpty($result); // result $result = $extractor->extract(['profile'], ['name' => 'Steve']); self::assertEquals($result['name'], 'Steve'); // no result $result = $extractor->extract(['profile'], ['invalid' => 'Steve']); self::assertEmpty($result); } } ================================================ FILE: tests/ResponseTypes/IdTokenResponseTest.php ================================================ processResponseType($responseType, $privateKey); self::assertInstanceOf(ResponseInterface::class, $response); self::assertEquals(200, $response->getStatusCode()); self::assertEquals('no-cache', $response->getHeader('pragma')[0]); self::assertEquals('no-store', $response->getHeader('cache-control')[0]); self::assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); $response->getBody()->rewind(); $json = json_decode($response->getBody()->getContents()); self::assertEquals('Bearer', $json->token_type); self::assertObjectHasAttribute('expires_in', $json); self::assertObjectHasAttribute('access_token', $json); self::assertObjectHasAttribute('refresh_token', $json); } /** * @dataProvider provideCryptKeys */ public function testOpenIDConnectHttpResponse($privateKey) { $responseType = new IdTokenResponse(new IdentityProvider(), new ClaimExtractor()); $response = $this->processResponseType($responseType, $privateKey, ['openid']); self::assertInstanceOf(ResponseInterface::class, $response); self::assertEquals(200, $response->getStatusCode()); self::assertEquals('no-cache', $response->getHeader('pragma')[0]); self::assertEquals('no-store', $response->getHeader('cache-control')[0]); self::assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); $response->getBody()->rewind(); $json = json_decode($response->getBody()->getContents()); self::assertEquals('Bearer', $json->token_type); self::assertObjectHasAttribute('expires_in', $json); self::assertObjectHasAttribute('access_token', $json); self::assertObjectHasAttribute('refresh_token', $json); self::assertObjectHasAttribute('id_token', $json); } // test additional claims // test fails without claimsetinterface /** * @dataProvider provideCryptKeys */ public function testThrowsRuntimeExceptionWhenMissingClaimSetInterface($privateKey) { $this->expectException(\RuntimeException::class); $_SERVER['HTTP_HOST'] = 'https://localhost'; $responseType = new IdTokenResponse( new IdentityProvider(IdentityProvider::NO_CLAIMSET), new ClaimExtractor() ); $this->processResponseType($responseType, $privateKey, ['openid']); self::fail('Exception should have been thrown'); } // test fails without identityinterface /** * @dataProvider provideCryptKeys */ public function testThrowsRuntimeExceptionWhenMissingIdentifierSetInterface($privateKey) { $this->expectException(\RuntimeException::class); $responseType = new IdTokenResponse( new IdentityProvider(IdentityProvider::NO_IDENTIFIER), new ClaimExtractor() ); $this->processResponseType($responseType, $privateKey, ['openid']); self::fail('Exception should have been thrown'); } /** * @dataProvider provideCryptKeys */ public function testClaimsGetExtractedFromUserEntity($privateKey) { $responseType = new IdTokenResponse(new IdentityProvider(), new ClaimExtractor()); $response = $this->processResponseType($responseType, $privateKey, ['openid', 'email']); self::assertInstanceOf(ResponseInterface::class, $response); self::assertEquals(200, $response->getStatusCode()); self::assertEquals('no-cache', $response->getHeader('pragma')[0]); self::assertEquals('no-store', $response->getHeader('cache-control')[0]); self::assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); $response->getBody()->rewind(); $json = json_decode($response->getBody()->getContents(),false); self::assertEquals('Bearer', $json->token_type); self::assertObjectHasAttribute('expires_in', $json); self::assertObjectHasAttribute('access_token', $json); self::assertObjectHasAttribute('refresh_token', $json); self::assertObjectHasAttribute('id_token', $json); if (class_exists("\Lcobucci\JWT\Token\Parser")) { $parser = new \Lcobucci\JWT\Token\Parser(new \Lcobucci\JWT\Encoding\JoseEncoder, \Lcobucci\JWT\Encoding\ChainedFormatter::withUnixTimestampDates()); } else { $parser = new \Lcobucci\JWT\Parser(); } $token = $parser->parse($json->id_token); self::assertTrue($token->claims()->has("email")); } public static function provideCryptKeys() { return array( array(new CryptKey('file://'.__DIR__.'/../Stubs/private.key')), array(new CryptKey( <<setPrivateKey($privateKey); // league/oauth2-server 5.1.0 does not support this interface if (method_exists($responseType, 'setEncryptionKey')) { $responseType->setEncryptionKey(base64_encode(random_bytes(36))); } $client = new ClientEntity(); $client->setIdentifier('clientName'); $scopes = []; foreach ($scopeNames as $scopeName) { $scope = new ScopeEntity(); $scope->setIdentifier($scopeName); $scopes[] = $scope; } $accessToken = new AccessTokenEntity(); $accessToken->setIdentifier('abcdef'); if (method_exists($accessToken, 'setPrivateKey')) { $accessToken->setPrivateKey($privateKey); } // Use DateTime for older libraries, DateTimeImmutable for new ones. try { $accessToken->setExpiryDateTime( (new \DateTime())->add(new \DateInterval('PT1H')) ); } catch(\TypeError $e) { $accessToken->setExpiryDateTime( (new \DateTimeImmutable())->add(new \DateInterval('PT1H')) ); } $accessToken->setClient($client); foreach ($scopes as $scope) { $accessToken->addScope($scope); } $refreshToken = new RefreshTokenEntity(); $refreshToken->setIdentifier('abcdef'); $refreshToken->setAccessToken($accessToken); // Use DateTime for older libraries, DateTimeImmutable for new ones. try { $refreshToken->setExpiryDateTime( (new \DateTime())->add(new \DateInterval('PT1H')) ); } catch(\TypeError $e) { $refreshToken->setExpiryDateTime( (new \DateTimeImmutable())->add(new \DateInterval('PT1H')) ); } $responseType->setAccessToken($accessToken); $responseType->setRefreshToken($refreshToken); return $responseType->generateHttpResponse(new Response()); } } ================================================ FILE: tests/Stubs/IdentityProvider.php ================================================ entity = new UserNoClaimSetEntity(); break; case self::NO_IDENTIFIER: $this->entity = new UserNoIdentifierEntity(); break; default: $this->entity = new UserEntity(); } } public function getUserEntityByIdentifier($identifier) { return $this->entity; } } ================================================ FILE: tests/Stubs/UserEntity.php ================================================ setIdentifier(123); } public function getClaims() { return [ 'first_name' => 'Steve', 'last_name' => 'Rhoades', 'email' => 'steve.rhoades@stephenrhoades.com' ]; } } ================================================ FILE: tests/Stubs/UserNoClaimSetEntity.php ================================================ setIdentifier(123); } } ================================================ FILE: tests/Stubs/UserNoIdentifierEntity.php ================================================ 'Steve', 'last_name' => 'Rhoades', 'email' => 'steve.rhoades@stephenrhoades.com' ]; } } ================================================ FILE: tests/Stubs/private.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEAzjgUbG97UD+bZwkceejvxkbcq/17YqmriWGpBu7etXAA2WZp x7vLaQi6ygApfCsYDz13W27DyriH2kg56GOa2v9M88OORiW1rQMaGF4hn/L7agFc cvdNAWBKD8ue+QUPz3prA3TLF+lSqMn5BgF+4j7XNlODvfOX3Tra1JQcVik4pyjg QeTLaaBSf6KaCvDzVCcvVuYISC5oku5v6o+BIug8taRhcXN8/9gLQ9akrGG73z+u dAoZ2+v1k7VZ704steLM9pf7aY+L6kR2Qmc7E4j/WM9Sv8CHUaJG41MRAbjdHtGh zGcsZwf/mNjyjW3UalI1dih2muhPTSAZT0xL+wIDAQABAoIBAAFDBJT5RabjDL9f peX1D+qFqnn+7g9XfG41w8QAGCrCCa9K2iDXvFHjNMlhoN9aoCYPuTg9AEOwR1yF jp0mZt8qKr1fF/LD7k2ltDYr9Ua2ROWMJpWpf7YfcbSRCWL6rfMWC6uUvl1iFxhj S/vGbJFT0xtI/YhfAjHfV1FvqpC4YwmKVe4/QqU0Kw+CjmYKLqeR0lvARP0aRfRm GRIy/9ZtUzbcUSnLScNS8U2HhdKEOl/R9dSjHVG+rr+/sJmFRUiQWJx3UkDZ4lTd lEIqj8i0CZMooARZjOIhIjP+wT9zr1sWFaxU81ourMnfoOQSKIrkUUYGc4rOk3Ao DuqBx9ECgYEA5miGw2v+2YIm1XtmTRvLJiDgr8mTWydylPXxjTlghYNKts4bU5rh EN/Ok+DUPLqPGQLnFgjcadT/clZfJXF3/gOS8V9hxVWI6Gwku1Ez7OqtCv3plA0i 46fxh0PmK6lFOzlbWMN1wpQ/mB3dWh4YaC6ERG1Z3L1DCIqYs/LVFsMCgYEA5R/A dJAwtPWjdEjKFaajH7Z7iB/BSUydyPg++sMDjETbrV3wddO67LEkhH3vB4VDEKKt FnD1iSyWprb94gDK0PTxQyHJ3JdzDP05L+7C+lwLqEKCrh2BZpScvub0FRgECNNO OCuoMtX1HG/dGjkJsxB7e1lr7LGFMyPjR6I+0mkCgYEAzvcznpUCvnTX10naUgdW SzCbQ6xJDkd3+HCYAuh4WFXgJicrisUDyHmRgWoim05lPe1KkJNzEim/MAB/xQ2Q 4H5rXx/zniPAMC78K7q8buM6fzYnu9K09VQldAC835lUU+eosyoYPKmYGlcxP0Lr X6HxM9oaL1tevGxq0LGfUasCgYEA3R4ybouE5e61KxDgLdreTEmgl/MFZwbQs1WX +grfzqvZUUt6N0v5dllSQ6cBWkGqQlCsOB8VZqeoUAYDp+tZ0CTC/SWLmR5zwtJS MUb71f+kpGJjmUMSUXwUdUuPvRerNRUvxJelQEIpxaLTP25SRQQgFx9qP0fmoz78 JXKXrBkCgYEApBfmVsOTG5S+oO7WZFpndeofLnXYn9xRvlc738+dANY4mWwHJlBd z2wzJ5wfjzlXsZoKcV0I6pRWLrgw3Gd5cwu3O5+MUN89cdQuVrfB77KQJHIF+S06 fHDgr/HSgH8LCXDq4DSd5XC0WxCPTDYrTN8iiHop2k35Ex0UXYeE+g0= -----END RSA PRIVATE KEY----- ================================================ FILE: tests/Stubs/private.key.crlf ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEAzjgUbG97UD+bZwkceejvxkbcq/17YqmriWGpBu7etXAA2WZp x7vLaQi6ygApfCsYDz13W27DyriH2kg56GOa2v9M88OORiW1rQMaGF4hn/L7agFc cvdNAWBKD8ue+QUPz3prA3TLF+lSqMn5BgF+4j7XNlODvfOX3Tra1JQcVik4pyjg QeTLaaBSf6KaCvDzVCcvVuYISC5oku5v6o+BIug8taRhcXN8/9gLQ9akrGG73z+u dAoZ2+v1k7VZ704steLM9pf7aY+L6kR2Qmc7E4j/WM9Sv8CHUaJG41MRAbjdHtGh zGcsZwf/mNjyjW3UalI1dih2muhPTSAZT0xL+wIDAQABAoIBAAFDBJT5RabjDL9f peX1D+qFqnn+7g9XfG41w8QAGCrCCa9K2iDXvFHjNMlhoN9aoCYPuTg9AEOwR1yF jp0mZt8qKr1fF/LD7k2ltDYr9Ua2ROWMJpWpf7YfcbSRCWL6rfMWC6uUvl1iFxhj S/vGbJFT0xtI/YhfAjHfV1FvqpC4YwmKVe4/QqU0Kw+CjmYKLqeR0lvARP0aRfRm GRIy/9ZtUzbcUSnLScNS8U2HhdKEOl/R9dSjHVG+rr+/sJmFRUiQWJx3UkDZ4lTd lEIqj8i0CZMooARZjOIhIjP+wT9zr1sWFaxU81ourMnfoOQSKIrkUUYGc4rOk3Ao DuqBx9ECgYEA5miGw2v+2YIm1XtmTRvLJiDgr8mTWydylPXxjTlghYNKts4bU5rh EN/Ok+DUPLqPGQLnFgjcadT/clZfJXF3/gOS8V9hxVWI6Gwku1Ez7OqtCv3plA0i 46fxh0PmK6lFOzlbWMN1wpQ/mB3dWh4YaC6ERG1Z3L1DCIqYs/LVFsMCgYEA5R/A dJAwtPWjdEjKFaajH7Z7iB/BSUydyPg++sMDjETbrV3wddO67LEkhH3vB4VDEKKt FnD1iSyWprb94gDK0PTxQyHJ3JdzDP05L+7C+lwLqEKCrh2BZpScvub0FRgECNNO OCuoMtX1HG/dGjkJsxB7e1lr7LGFMyPjR6I+0mkCgYEAzvcznpUCvnTX10naUgdW SzCbQ6xJDkd3+HCYAuh4WFXgJicrisUDyHmRgWoim05lPe1KkJNzEim/MAB/xQ2Q 4H5rXx/zniPAMC78K7q8buM6fzYnu9K09VQldAC835lUU+eosyoYPKmYGlcxP0Lr X6HxM9oaL1tevGxq0LGfUasCgYEA3R4ybouE5e61KxDgLdreTEmgl/MFZwbQs1WX +grfzqvZUUt6N0v5dllSQ6cBWkGqQlCsOB8VZqeoUAYDp+tZ0CTC/SWLmR5zwtJS MUb71f+kpGJjmUMSUXwUdUuPvRerNRUvxJelQEIpxaLTP25SRQQgFx9qP0fmoz78 JXKXrBkCgYEApBfmVsOTG5S+oO7WZFpndeofLnXYn9xRvlc738+dANY4mWwHJlBd z2wzJ5wfjzlXsZoKcV0I6pRWLrgw3Gd5cwu3O5+MUN89cdQuVrfB77KQJHIF+S06 fHDgr/HSgH8LCXDq4DSd5XC0WxCPTDYrTN8iiHop2k35Ex0UXYeE+g0= -----END RSA PRIVATE KEY----- ================================================ FILE: tests/Stubs/public.key ================================================ -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzjgUbG97UD+bZwkceejv xkbcq/17YqmriWGpBu7etXAA2WZpx7vLaQi6ygApfCsYDz13W27DyriH2kg56GOa 2v9M88OORiW1rQMaGF4hn/L7agFccvdNAWBKD8ue+QUPz3prA3TLF+lSqMn5BgF+ 4j7XNlODvfOX3Tra1JQcVik4pyjgQeTLaaBSf6KaCvDzVCcvVuYISC5oku5v6o+B Iug8taRhcXN8/9gLQ9akrGG73z+udAoZ2+v1k7VZ704steLM9pf7aY+L6kR2Qmc7 E4j/WM9Sv8CHUaJG41MRAbjdHtGhzGcsZwf/mNjyjW3UalI1dih2muhPTSAZT0xL +wIDAQAB -----END PUBLIC KEY-----