├── .gitignore ├── tests ├── Fixtures │ ├── jwt.key.pub │ ├── MyUserFactory.php │ ├── MyUser.php │ └── jwt.key ├── ClientAppTest.php ├── AccessTokenTest.php └── TestCase.php ├── src ├── Contracts │ └── UserFactory.php ├── Issuer.php ├── KeyResolver.php ├── LaravelCognitoServiceProvider.php ├── TokenParser.php ├── Testing │ └── TokenGenerator.php └── CognitoUserProvider.php ├── .github └── workflows │ └── phpunit.yml ├── .scrutinizer.yml ├── phpunit.xml.dist ├── LICENSE ├── composer.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | composer.phar 3 | composer.lock 4 | /vendor/ 5 | .phpunit.result.cache 6 | .phpunit.cache/ -------------------------------------------------------------------------------- /tests/Fixtures/jwt.key.pub: -------------------------------------------------------------------------------- 1 | {"keys":[{"use":"sig","alg":"RS256","kty":"RSA","n":"uHYHfYZrcIedRfOhaXsIhTiquS9fBodIwp0SmckjPM4AX32PwTozRD_YFe8kUCZE6fNM6puMkD9RlmeRE6vxcQ","e":"AQAB"},{"use":"sig","alg":"RS256","kty":"RSA","n":"pi25OhdwjN5h1bxJfAstVA-20wRmxGSplKLfuFOdCpQxRJtFior6yw-qXlW7UbOXgokqnxkNQYTq7jhM4R1r-w","e":"AQAB"}]} -------------------------------------------------------------------------------- /src/Contracts/UserFactory.php: -------------------------------------------------------------------------------- 1 | region, $this->userPoolId); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | tests: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Validate composer.json and composer.lock 16 | run: composer validate 17 | 18 | - name: Install dependencies 19 | run: composer install --prefer-dist --no-progress --no-suggest 20 | 21 | - name: Test 22 | run: ./vendor/bin/phpunit 23 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: 3 | - "tests/" 4 | 5 | checks: 6 | php: 7 | code_rating: true 8 | duplication: true 9 | 10 | build: 11 | nodes: 12 | analysis: 13 | environment: 14 | php: 15 | version: 7.3 16 | tests: 17 | override: 18 | - php-scrutinizer-run 19 | coverage: 20 | tests: 21 | override: 22 | - command: './vendor/bin/phpunit --coverage-clover=clover.xml' 23 | coverage: 24 | file: 'clover.xml' 25 | format: 'clover' -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | 10 | ./src 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/Fixtures/MyUserFactory.php: -------------------------------------------------------------------------------- 1 | issuer->toString() . '/.well-known/jwks.json'; 21 | 22 | return $this->cache->remember('jwks', 7200, static function () use ($url) { 23 | $content = file_get_contents($url); 24 | 25 | if ($content === false) { 26 | throw new InvalidArgumentException('Invalid JWKS file'); 27 | } 28 | 29 | return $content; 30 | }); 31 | } 32 | 33 | public function issuer(): Issuer 34 | { 35 | return $this->issuer; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/ClientAppTest.php: -------------------------------------------------------------------------------- 1 | jwtToken(['client_app' => '555']); 13 | 14 | $provider = $this->container->make(CognitoUserProvider::class); 15 | 16 | $auth = $provider->retrieveByCredentials(['cognito_token' => $token]); 17 | 18 | self::assertInstanceOf(MyUser::class, $auth); 19 | } 20 | 21 | public function test_invalid_token_will_return_null() 22 | { 23 | $provider = $this->container->make(CognitoUserProvider::class); 24 | 25 | $auth = $provider->retrieveByCredentials(['cognito_token' => 'invalid']); 26 | 27 | self::assertNull($auth); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Fixtures/jwt.key: -------------------------------------------------------------------------------- 1 | {"keys":[{"use":"sig","alg":"RS256","kty":"RSA","n":"uHYHfYZrcIedRfOhaXsIhTiquS9fBodIwp0SmckjPM4AX32PwTozRD_YFe8kUCZE6fNM6puMkD9RlmeRE6vxcQ","e":"AQAB","d":"JBmlWaGnATzpQLlvi48yma__aYKl3Ec5rCzFaSd-J_Xr2c6bJH0BleQ2tzsEzrG1bZWMAQamIazlaFzbAmTLKQ","p":"5XKj4tuM6lOem_roLdDTztCZJRLKrsZf8a4R7VeIpE8","q":"zc6pmBPcMCwc7m84s3quZGOfLwRaNElQKfO0x2XB3j8","dp":"VLtzPjGp5lce-ac82r6FmZh7Fa_M3aIwiEWqJSXwNg0","dq":"wv0mWVuXplxlHNJrmkCFsb9hNh6diCRduyGXCcQzJek","qi":"12az-GIBoZKA-_fu1tOVQuC-IuD9p2SzfvycyRVIVjw"},{"use":"sig","alg":"RS256","kty":"RSA","n":"pi25OhdwjN5h1bxJfAstVA-20wRmxGSplKLfuFOdCpQxRJtFior6yw-qXlW7UbOXgokqnxkNQYTq7jhM4R1r-w","e":"AQAB","d":"JWSmiG7b0ab8xtaOho4RThgXIJad9VVdX2fjBZwy-z0OoR1GbpOdze7tBGccPy9zjaTXNZ4aNZQ_KCo1mUw5GQ","p":"0Bg3kFw2XdoEDLDr4Gpg7Mou9arg-a1Bgtq61jcsO6c","q":"zG8-EBzHfcoGpugsTfT9dCzPZzOgtHjhKBT-dz7JB40","dp":"DzWV6hEzUAy9Owg7VT2xM91bLU1-AAGvxhaijUGfFAU","dq":"bHBZgFxzqRKfoqsmzRc11gjq3vgwt2ojLIquS_xe0AU","qi":"i18M-4jhYcYA4zCs6TpenSz1VX00jtUZjcByDOAK-C8"}]} 2 | -------------------------------------------------------------------------------- /tests/AccessTokenTest.php: -------------------------------------------------------------------------------- 1 | jwtToken(['id' => 53]); 13 | 14 | $provider = $this->container->make(CognitoUserProvider::class); 15 | 16 | $auth = $provider->retrieveByCredentials(['cognito_token' => $token]); 17 | 18 | self::assertInstanceOf(MyUser::class, $auth); 19 | } 20 | 21 | public function test_user_factory_can_return_null() 22 | { 23 | $token = $this->jwtToken(['id' => 54]); 24 | 25 | $provider = $this->container->make(CognitoUserProvider::class); 26 | 27 | $auth = $provider->retrieveByCredentials(['cognito_token' => $token]); 28 | 29 | self::assertNull($auth); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/LaravelCognitoServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerIssuer(); 16 | 17 | $this->registerCognitoUserProvider(); 18 | } 19 | 20 | private function registerIssuer(): void 21 | { 22 | $this->app->bind(Issuer::class, function () { 23 | $config = $this->app->get('config'); 24 | 25 | $pool = $config->get('auth.cognito.pool'); 26 | 27 | $region = $config->get('auth.cognito.region'); 28 | 29 | return new Issuer($pool, $region); 30 | }); 31 | } 32 | 33 | private function registerCognitoUserProvider(): void 34 | { 35 | Auth::provider(CognitoUserProvider::class, static function (Container $app) { 36 | return $app->make(CognitoUserProvider::class); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 CustomerGauge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "customergauge/cognito", 3 | "description": "AWS Cognito provider for Laravel Authentication", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": ["laravel", "cognito", "aws", "authentication"], 7 | "authors": [ 8 | { 9 | "name": "Marco Deleu", 10 | "email": "deleugyn@gmail.com" 11 | } 12 | ], 13 | "minimum-stability": "dev", 14 | "prefer-stable": true, 15 | "require": { 16 | "php": ">=8.1", 17 | "ext-gmp": "*", 18 | "ext-json": "*", 19 | "web-token/jwt-library": "^3.0", 20 | "illuminate/contracts": ">=7.1" 21 | }, 22 | "require-dev": { 23 | "illuminate/cache": ">=7.1", 24 | "illuminate/config": ">=7.1", 25 | "illuminate/container": ">=7.1", 26 | "phpunit/phpunit": "^10.0", 27 | "doctrine/coding-standard": "^12.0", 28 | "phpstan/phpstan": "^1.0" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "CustomerGauge\\Cognito\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Tests\\CustomerGauge\\Cognito\\": "tests/" 38 | } 39 | }, 40 | "extra": { 41 | "laravel": { 42 | "providers": [ 43 | "CustomerGauge\\Cognito\\LaravelCognitoServiceProvider" 44 | ] 45 | } 46 | }, 47 | "config": { 48 | "allow-plugins": { 49 | "dealerdirect/phpcodesniffer-composer-installer": true 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | container = $container = Container::getInstance(); 22 | 23 | $container->bind(Issuer::class, function () { 24 | return new Issuer('phpunit-pool-id', 'local'); 25 | }); 26 | 27 | $container->bind(RepositoryContract::class, function () { 28 | $repository = new Repository(new ArrayStore()); 29 | 30 | $repository->set('jwks', file_get_contents(__DIR__ .'/Fixtures/jwt.key.pub')); 31 | 32 | return $repository; 33 | }); 34 | 35 | $container->bind(UserFactory::class, MyUserFactory::class); 36 | } 37 | 38 | protected function jwtToken(array $claims): string 39 | { 40 | $generator = TokenGenerator::fromFile(__DIR__ . '/Fixtures/jwt.key'); 41 | 42 | return $generator->sign($claims); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/TokenParser.php: -------------------------------------------------------------------------------- 1 | loadAndVerifyWithKeySet($token); 30 | 31 | $payload = json_decode($jws->getPayload(), true); 32 | 33 | $claimCheckerManager = new ClaimCheckerManager([ 34 | new IssuerChecker([$this->keyResolver->issuer()->toString()]), 35 | new ExpirationTimeChecker(), 36 | ]); 37 | 38 | $claimCheckerManager->check($payload); 39 | 40 | return $payload; 41 | } 42 | 43 | private function loadAndVerifyWithKeySet(string $token): JWS 44 | { 45 | $jwsVerifier = new JWSVerifier(new AlgorithmManager([new RS256()])); 46 | 47 | $serializerManager = new JWSSerializerManager([new CompactSerializer()]); 48 | 49 | $jwsLoader = new JWSLoader($serializerManager, $jwsVerifier, null); 50 | 51 | $jwkset = JWKSet::createFromJson($this->keyResolver->jwkset()); 52 | 53 | return $jwsLoader->loadAndVerifyWithKeySet($token, $jwkset, $signature); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Testing/TokenGenerator.php: -------------------------------------------------------------------------------- 1 | $time, 52 | 'nbf' => $time, 53 | 'exp' => $time + 3600, 54 | 'iss' => $this->issuer, 55 | 'jti' => $this->jti, 56 | 'sub' => $this->subject, 57 | ] + $attributes); 58 | 59 | $jws = $jwsBuilder->create() 60 | ->withPayload($payload) 61 | ->addSignature($this->jwk->get(0), ['alg' => $this->algorithm]) 62 | ->build(); 63 | 64 | return (new CompactSerializer())->serialize($jws); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/CognitoUserProvider.php: -------------------------------------------------------------------------------- 1 | parser->parse($token); 26 | } catch (Throwable) { 27 | // If we cannot parse the token, that probably means it's an invalid Token. Since 28 | // the Authenticate Middleware implements a Chain Of Responsibility Pattern, 29 | // we have to return null so that other Guards can try to authenticate. 30 | return null; 31 | } 32 | 33 | return $this->factory->make($payload); 34 | } 35 | 36 | /** 37 | * @inheritdoc 38 | * @phpstan ignore 39 | */ 40 | public function validateCredentials(Authenticatable $user, array $credentials) 41 | { 42 | throw new BadMethodCallException('Not implemented'); 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | * @phpstan ignore 48 | */ 49 | public function retrieveById($identifier) 50 | { 51 | throw new BadMethodCallException('Not implemented'); 52 | } 53 | 54 | /** 55 | * @inheritdoc 56 | * @phpstan ignore 57 | */ 58 | public function retrieveByToken($identifier, $token) 59 | { 60 | throw new BadMethodCallException('Not implemented'); 61 | } 62 | 63 | /** 64 | * @inheritdoc 65 | * @phpstan ignore 66 | */ 67 | public function updateRememberToken(Authenticatable $user, $token) 68 | { 69 | throw new BadMethodCallException('Not implemented'); 70 | } 71 | 72 | /** 73 | * @inheritdoc 74 | * @phpstan ignore 75 | */ 76 | public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false) 77 | { 78 | throw new BadMethodCallException('Not implemented'); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![Build](https://github.com/cgauge/laravel-cognito-provider/workflows/Tests/badge.svg) 2 | [![Code Coverage](https://scrutinizer-ci.com/g/cgauge/laravel-cognito-provider/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/cgauge/laravel-cognito-provider/?branch=master) 3 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/cgauge/laravel-cognito-provider/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/cgauge/laravel-cognito-provider/?branch=master) 4 | 5 | # Laravel Cognito Provider 🔑 6 | 7 | This library provides a CognitoUserProvider for Laravel. 8 | 9 | # Installation 10 | 11 | ```bash 12 | composer require customergauge/cognito 13 | ``` 14 | 15 | # Usage 16 | 17 | ### Auth configuration 18 | 19 | In the `auth.php` file, add the following settings: 20 | 21 | Default Guard 22 | 23 | ```php 24 | 'defaults' => [ 25 | 'guard' => 'cognito-token', 26 | 'passwords' => 'users', 27 | ], 28 | ``` 29 | 30 | The new Guard configuration 31 | ```php 32 | 'guards' => [ 33 | 'cognito-token' => [ 34 | 'driver' => 'token', 35 | 'provider' => 'cognito-provider', 36 | 'storage_key' => 'cognito_token', 37 | 'hash' => false, 38 | ], 39 | ], 40 | ``` 41 | 42 | The User Provider configuration 43 | ```php 44 | 45 | 'providers' => [ 46 | 'cognito-provider' => [ 47 | 'driver' => \CustomerGauge\Cognito\CognitoUserProvider::class, 48 | ], 49 | ], 50 | ``` 51 | 52 | Cognito Environment Variables 53 | ```php 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Cognito Custom Configuration 57 | |-------------------------------------------------------------------------- 58 | | 59 | | The following configuration is not part of standard Laravel application. 60 | | We use it to configure the CognitoUserProvider process so that we can 61 | | properly validate the JWT token provided by AWS Cognito. 62 | | 63 | */ 64 | 65 | 'cognito' => [ 66 | 'pool' => env('AWS_COGNITO_USER_POOL_ID'), 67 | 'region' => env('AWS_COGNITO_USER_POOL_REGION'), 68 | ], 69 | ``` 70 | 71 | ### Auth Middleware 72 | 73 | Configure the `auth` middleware at `App\Http\Kernel` with `'auth:cognito-token'` 74 | 75 | ### UserFactory 76 | 77 | The last thing you'll need is to provide your own implementation of `UserFactory` and register it in a ServiceProvider. 78 | 79 | ``` 80 | final class CognitoUserFactory implements UserFactory 81 | { 82 | public function make(array $payload): ?Authenticatable 83 | { 84 | return new MyUserObject( 85 | $payload['username'], 86 | $payload['custom:my_custom_cognito_attribute'], 87 | ); 88 | } 89 | } 90 | ``` 91 | 92 | In the provider: 93 | ``` 94 | $this->app->bind(CustomerGauge\Cognito\Contracts\UserFactory, App\Auth\CognitoUserFactory::class); 95 | ``` 96 | --------------------------------------------------------------------------------