From 9e8523fa53a5dded6c0cf92b856dd0eaaaffd8e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Gangloff?= Date: Mon, 22 Jul 2024 02:17:42 +0200 Subject: [PATCH] feat: implement OAuth 2.0 login flow --- .env | 8 + composer.json | 1 + composer.lock | 606 +++++++++++++++++++++++- config/bundles.php | 1 + config/packages/knpu_oauth2_client.yaml | 14 + config/packages/security.yaml | 7 +- config/routes.yaml | 3 + src/Controller/HomeController.php | 15 + src/Security/OAuthAuthenticator.php | 87 ++++ src/Security/OAuthProvider.php | 58 +++ src/Security/OAuthResourceOwner.php | 40 ++ symfony.lock | 12 + 12 files changed, 850 insertions(+), 2 deletions(-) create mode 100644 config/packages/knpu_oauth2_client.yaml create mode 100644 src/Security/OAuthAuthenticator.php create mode 100644 src/Security/OAuthProvider.php create mode 100644 src/Security/OAuthResourceOwner.php diff --git a/.env b/.env index d6b13e3..fab92c8 100644 --- a/.env +++ b/.env @@ -56,3 +56,11 @@ JWT_PASSPHRASE=827c9f8cce8bb82e75b2aec4a14a61f572ac28c7a8531f08dcdf1652573a7049 # postgresql+advisory://db_user:db_password@localhost/db_name LOCK_DSN=flock ###< symfony/lock ### + +OAUTH_AUTHORIZATION_URL= +OAUTH_CLIENT_ID= +OAUTH_CLIENT_SECRET= +OAUTH_AUTHORIZATION_URL= +OAUTH_TOKEN_URL= +OAUTH_USERINFO_URL= +OAUTH_SCOPE= diff --git a/composer.json b/composer.json index 5ecdd7a..f8a5095 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/orm": "^3.2", "eluceo/ical": "^2.14", + "knpuniversity/oauth2-client-bundle": "^2.18", "lexik/jwt-authentication-bundle": "^3.1", "nelmio/cors-bundle": "^2.5", "phpdocumentor/reflection-docblock": "^5.4", diff --git a/composer.lock b/composer.lock index ba02405..259faa7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a8eb6290221266a2746c56c3cfa4c5d7", + "content-hash": "67ad51bfb7e55fe789bbf6ae425a3d8c", "packages": [ { "name": "api-platform/core", @@ -1628,6 +1628,391 @@ }, "time": "2024-07-11T22:33:13+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "a629e5b69db96eb4939c1b34114130077dd4c6fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a629e5b69db96eb4939c1b34114130077dd4c6fc", + "reference": "a629e5b69db96eb4939c1b34114130077dd4c6fc", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2024-07-19T16:19:57+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2024-07-18T10:29:17+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2024-07-18T11:15:46+00:00" + }, + { + "name": "knpuniversity/oauth2-client-bundle", + "version": "v2.18.1", + "source": { + "type": "git", + "url": "https://github.com/knpuniversity/oauth2-client-bundle.git", + "reference": "1d59f49f164805b45f95f92cf743781bc2ba7d2b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/knpuniversity/oauth2-client-bundle/zipball/1d59f49f164805b45f95f92cf743781bc2ba7d2b", + "reference": "1d59f49f164805b45f95f92cf743781bc2ba7d2b", + "shasum": "" + }, + "require": { + "league/oauth2-client": "^2.0", + "php": ">=8.1", + "symfony/dependency-injection": "^4.4|^5.0|^6.0|^7.0", + "symfony/framework-bundle": "^4.4|^5.0|^6.0|^7.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0|^7.0", + "symfony/routing": "^4.4|^5.0|^6.0|^7.0" + }, + "require-dev": { + "league/oauth2-facebook": "^1.1|^2.0", + "phpstan/phpstan": "^1.0", + "symfony/phpunit-bridge": "^5.3.1|^6.0|^7.0", + "symfony/security-guard": "^4.4|^5.0|^6.0|^7.0", + "symfony/yaml": "^4.4|^5.0|^6.0|^7.0" + }, + "suggest": { + "symfony/security-guard": "For integration with Symfony's Guard Security layer" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "KnpU\\OAuth2ClientBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ryan Weaver", + "email": "ryan@symfonycasts.com" + } + ], + "description": "Integration with league/oauth2-client to provide services", + "homepage": "https://symfonycasts.com", + "keywords": [ + "oauth", + "oauth2" + ], + "support": { + "issues": "https://github.com/knpuniversity/oauth2-client-bundle/issues", + "source": "https://github.com/knpuniversity/oauth2-client-bundle/tree/v2.18.1" + }, + "time": "2024-02-14T17:41:28+00:00" + }, { "name": "lcobucci/clock", "version": "3.2.0", @@ -1765,6 +2150,76 @@ ], "time": "2024-04-11T23:07:54+00:00" }, + { + "name": "league/oauth2-client", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "160d6274b03562ebeb55ed18399281d8118b76c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/160d6274b03562ebeb55ed18399281d8118b76c8", + "reference": "160d6274b03562ebeb55ed18399281d8118b76c8", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "paragonie/random_compat": "^1 || ^2 || ^9.99", + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpunit/phpunit": "^5.7 || ^6.0 || ^9.5", + "squizlabs/php_codesniffer": "^2.3 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Woody Gilk", + "homepage": "https://github.com/shadowhand", + "role": "Contributor" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-client/issues", + "source": "https://github.com/thephpleague/oauth2-client/tree/2.7.0" + }, + "time": "2023-04-16T18:19:15+00:00" + }, { "name": "lexik/jwt-authentication-bundle", "version": "v3.1.0", @@ -2043,6 +2498,56 @@ }, "time": "2024-06-24T21:25:28+00:00" }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -2621,6 +3126,61 @@ }, "time": "2023-09-23T14:17:50+00:00" }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, { "name": "psr/http-message", "version": "2.0", @@ -2780,6 +3340,50 @@ }, "time": "2021-07-14T16:46:02+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "sabre/uri", "version": "3.0.1", diff --git a/config/bundles.php b/config/bundles.php index 1102c9a..9218319 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -17,4 +17,5 @@ return [ ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], + KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true], ]; diff --git a/config/packages/knpu_oauth2_client.yaml b/config/packages/knpu_oauth2_client.yaml new file mode 100644 index 0000000..77b53bb --- /dev/null +++ b/config/packages/knpu_oauth2_client.yaml @@ -0,0 +1,14 @@ +knpu_oauth2_client: + clients: + oauth: + type: generic + provider_class: App\Security\OAuthProvider + client_id: '%env(OAUTH_CLIENT_ID)%' + client_secret: '%env(OAUTH_CLIENT_SECRET)%' + redirect_route: oauth_connect_check + redirect_params: {} + provider_options: + baseAuthorizationUrl: '%env(OAUTH_AUTHORIZATION_URL)%' + baseAccessTokenUrl: '%env(OAUTH_TOKEN_URL)%' + resourceOwnerDetailsUrl: '%env(OAUTH_USERINFO_URL)%' + scope: '%env(OAUTH_SCOPE)%' diff --git a/config/packages/security.yaml b/config/packages/security.yaml index d96ec5a..577b2ec 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -13,7 +13,8 @@ security: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false - login: + + api_login: pattern: ^/api/login stateless: true json_login: @@ -26,6 +27,10 @@ security: stateless: true jwt: ~ + main: + custom_authenticators: + - App\Security\OAuthAuthenticator + # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall diff --git a/config/routes.yaml b/config/routes.yaml index d860126..4f26282 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -6,3 +6,6 @@ controllers: api_login: path: /api/login + +oauth_connect_check: + path: /login/oauth/check diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index fdf0694..72ccf15 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -2,9 +2,13 @@ namespace App\Controller; +use KnpU\OAuth2ClientBundle\Client\ClientRegistry; +use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Core\User\UserInterface; class HomeController extends AbstractController { @@ -16,4 +20,15 @@ class HomeController extends AbstractController } + #[Route(path: "/login/oauth", name: "connect_start")] + public function connectAction(ClientRegistry $clientRegistry): Response + { + return $clientRegistry->getClient('oauth')->redirect(); + } + + #[Route(path: "/login/oauth/token", name: "login_oauth_token")] + public function getToken(UserInterface $user, JWTTokenManagerInterface $JWTManager): JsonResponse + { + return new JsonResponse(['token' => $JWTManager->create($user)]); + } } \ No newline at end of file diff --git a/src/Security/OAuthAuthenticator.php b/src/Security/OAuthAuthenticator.php new file mode 100644 index 0000000..c22bc73 --- /dev/null +++ b/src/Security/OAuthAuthenticator.php @@ -0,0 +1,87 @@ +attributes->get('_route') === 'oauth_connect_check'; + } + + public function authenticate(Request $request): Passport + { + $client = $this->clientRegistry->getClient('oauth'); + $accessToken = $this->fetchAccessToken($client); + + return new SelfValidatingPassport( + new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) { + + /** @var OAuthResourceOwner $userFromToken */ + $userFromToken = $client->fetchUserFromToken($accessToken); + + $existingUser = $this->userRepository->findOneBy(['email' => $userFromToken->getEmail()]); + + if ($existingUser) return $existingUser; + + $user = new User(); + $user->setEmail($userFromToken->getEmail()); + $this->em->persist($user); + $this->em->flush(); + + return $user; + }) + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): RedirectResponse + { + return new RedirectResponse($this->router->generate('index')); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + $message = strtr($exception->getMessageKey(), $exception->getMessageData()); + + return new Response($message, Response::HTTP_UNAUTHORIZED); + } + + public function start(Request $request, ?AuthenticationException $authException = null): Response + { + return new RedirectResponse( + '/login/oauth', + Response::HTTP_TEMPORARY_REDIRECT + ); + } +} diff --git a/src/Security/OAuthProvider.php b/src/Security/OAuthProvider.php new file mode 100644 index 0000000..89ce1f0 --- /dev/null +++ b/src/Security/OAuthProvider.php @@ -0,0 +1,58 @@ +options['baseAuthorizationUrl']; + } + + public function getBaseAccessTokenUrl(array $params): string + { + return $this->options['baseAccessTokenUrl']; + } + + public function getResourceOwnerDetailsUrl(AccessToken $token): string + { + return $this->options['resourceOwnerDetailsUrl']; + } + + protected function getDefaultScopes(): array + { + return explode(',', $this->options['scope']); + } + + protected function checkResponse(ResponseInterface $response, $data): void + { + if ($response->getStatusCode() >= 400) { + throw new IdentityProviderException( + $data['error'] ?? 'Unknown error', + $response->getStatusCode(), + $response + ); + } + } + + protected function createResourceOwner(array $response, AccessToken $token): ResourceOwnerInterface + { + return new OAuthResourceOwner($response); + } +} \ No newline at end of file diff --git a/src/Security/OAuthResourceOwner.php b/src/Security/OAuthResourceOwner.php new file mode 100644 index 0000000..29c3fd7 --- /dev/null +++ b/src/Security/OAuthResourceOwner.php @@ -0,0 +1,40 @@ +response = $response; + } + + public function getId(): string + { + return $this->response['sub']; + } + + public function toArray(): array + { + return $this->response; + } + + public function getEmail(): string + { + return $this->response['email']; + } + + public function getName(): string + { + return $this->response['name']; + } +} \ No newline at end of file diff --git a/symfony.lock b/symfony.lock index 5499387..4303cbf 100644 --- a/symfony.lock +++ b/symfony.lock @@ -40,6 +40,18 @@ "migrations/.gitignore" ] }, + "knpuniversity/oauth2-client-bundle": { + "version": "2.18", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.20", + "ref": "1ff300d8c030f55c99219cc55050b97a695af3f6" + }, + "files": [ + "config/packages/knpu_oauth2_client.yaml" + ] + }, "lexik/jwt-authentication-bundle": { "version": "3.1", "recipe": {