diff --git a/composer.json b/composer.json index be710c0..f2b1fb1 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "doctrine/doctrine-bundle": "^2.13", "doctrine/doctrine-migrations-bundle": "^3.4", "doctrine/orm": "^3.3", + "knpuniversity/oauth2-client-bundle": "^2.18", "phpdocumentor/reflection-docblock": "^5.6", "phpstan/phpdoc-parser": "^2.1", "symfony/asset": "7.2.*", @@ -56,7 +57,8 @@ }, "autoload": { "psr-4": { - "App\\": "src/" + "App\\": "src/", + "Sudalys\\OAuth2\\Client\\": "libs/sudalys/oauth2-client/src" } }, "autoload-dev": { diff --git a/composer.lock b/composer.lock index 344fd99..529b63c 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": "eaa614d3c00b5654c4cc228dcad4b8c8", + "content-hash": "a2e7a54b8d79cc97db7598ed43170f6c", "packages": [ { "name": "composer/semver", @@ -1368,6 +1368,455 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "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.2" + }, + "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-24T11:22:20+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "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.4" + }, + "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-10-17T10:06:22+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.3", + "source": { + "type": "git", + "url": "https://github.com/knpuniversity/oauth2-client-bundle.git", + "reference": "c38ca88a70aae3694ca346a41b13b9a8f6e33ed4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/knpuniversity/oauth2-client-bundle/zipball/c38ca88a70aae3694ca346a41b13b9a8f6e33ed4", + "reference": "c38ca88a70aae3694ca346a41b13b9a8f6e33ed4", + "shasum": "" + }, + "require": { + "league/oauth2-client": "^2.0", + "php": ">=8.1", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "league/oauth2-facebook": "^1.1|^2.0", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", + "symfony/security-guard": "^5.4", + "symfony/yaml": "^5.4|^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.3" + }, + "time": "2024-10-02T14:26:09+00:00" + }, + { + "name": "league/oauth2-client", + "version": "2.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "9df2924ca644736c835fc60466a3a60390d334f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/9df2924ca644736c835fc60466a3a60390d334f9", + "reference": "9df2924ca644736c835fc60466a3a60390d334f9", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "php": "^7.1 || >=8.0.0 <8.5.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.11" + }, + "type": "library", + "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.8.1" + }, + "time": "2025-02-26T04:37:30+00:00" + }, { "name": "monolog/monolog", "version": "3.8.1", @@ -1893,6 +2342,166 @@ }, "time": "2019-01-08T18:20:26+00:00" }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "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", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.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": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, { "name": "psr/link", "version": "2.0.1", @@ -1999,6 +2608,50 @@ }, "time": "2024-09-11T13:17:53+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": "symfony/asset", "version": "v7.2.0", diff --git a/config/bundles.php b/config/bundles.php index 4e3a560..e9ad544 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -13,4 +13,5 @@ return [ Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => 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..3027e1c --- /dev/null +++ b/config/packages/knpu_oauth2_client.yaml @@ -0,0 +1,12 @@ +knpu_oauth2_client: + clients: + sudalys: + type: generic + provider_class: Sudalys\OAuth2\Client\Provider\Sudalys + client_id: '%env(SSO_CLIENT_ID)%' + client_secret: '%env(SSO_CLIENT_SECRET)%' + redirect_route: app_home + redirect_params: {} + provider_options: + domain: 'https://portail.solutions-easy.moi' + use_state: false \ No newline at end of file diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 367af25..0ffd559 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -4,24 +4,34 @@ security: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider providers: - users_in_memory: { memory: null } + sudalys: + entity: + class: App\Entity\User + property: email + firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: - lazy: true - provider: users_in_memory - - # activate different ways to authenticate - # https://symfony.com/doc/current/security.html#the-firewall - - # https://symfony.com/doc/current/security/impersonating_user.html - # switch_user: true + provider: sudalys + custom_authenticators: + - App\Security\OAuthAuthenticator + entry_point: App\Security\OAuthAuthenticator + logout: + path: app_logout + target: app_home + invalidate_session: true + clear_site_data: + - cookies + - storage # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used access_control: + - { path: ^/dashboard, roles: ROLE_USER } + - { path: ^/user-info, roles: ROLE_USER } + - { path: ^/connect, roles: PUBLIC_ACCESS } # - { path: ^/admin, roles: ROLE_ADMIN } # - { path: ^/profile, roles: ROLE_USER } diff --git a/config/services.yaml b/config/services.yaml index 2d6a76f..28fcd32 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -20,5 +20,12 @@ services: - '../src/Entity/' - '../src/Kernel.php' + # Register our custom OAuth user provider + knpu.oauth2.client.user_provider: + class: App\Security\OAuthUserProvider + arguments: + - '@doctrine.orm.entity_manager' + - '@knpu.oauth2.registry' + # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones diff --git a/libs/sudalys/oauth2-client/.gitignore b/libs/sudalys/oauth2-client/.gitignore new file mode 100644 index 0000000..25efb0f --- /dev/null +++ b/libs/sudalys/oauth2-client/.gitignore @@ -0,0 +1,9 @@ +/build +/log +/vendor +.*.cache +composer.phar +composer.lock +infection.json +infection.phar* +phpunit.xml \ No newline at end of file diff --git a/libs/sudalys/oauth2-client/CHANGELOG.md b/libs/sudalys/oauth2-client/CHANGELOG.md new file mode 100644 index 0000000..e9229f5 --- /dev/null +++ b/libs/sudalys/oauth2-client/CHANGELOG.md @@ -0,0 +1,78 @@ +# Changelog +All notable changes to `oauth2-gitlab` will be documented in this file +This project adheres to [Semantic Versioning](http://semver.org/). + +## [Unreleased] +Nothing yet. + +## [3.4.0] - 2021-02-08 +### Added + - Compatibility with php-gitlab-api v10 + - Test suite compatible with PHP8 + +## [3.3.0] - 2020-02-10 +### Added + - Compatibility with php-gitlab-api v10 + +## [3.2.0] - 2020-02-10 +### Changed + - Updated dependencies to those requiring up to date PHP versions + +### Removed + - Support for outdated and unsupported PHP versions (<7.2) + +## [3.1.2] - 2018-11-23 +### Changed + - Added conflict with `oauth2-client:2.4.0` due to [breaking change upstream](https://github.com/thephpleague/oauth2-client/issues/752) (#6) + +## [3.1.1] - 2018-10-01 +### Added + - PHP 7.2 and nightly added to test suite + - Infection testing added + +### Changed + - Test suite upgraded to PHPUnit 5/7 hybrid + +## [3.1.0] - 2017-11-01 +### Added + - Access scope support was implemented + +## [3.0.0] - 2017-05-31 +### Changed + - **Breaking**: Upgrade Gitlab API from v3 to v4 + - Test suite upgraded from PHPUnit 4 to 5/6 hybrid + +## [2.0.0] - 2017-02-03 +### Added + - PHP 7.1 is now officially supported and tested + +### Changed + - **Breaking**: Upgrade league/oauth2-client to major version 2 + - Included PHP-CS-Fixer + +### Removed + - PHP 5.5 is end of life and no longer supported + +## [1.1.0] - 2016-08-28 +### Added + - Added `getApiClient` method on `GitlabResourceOwner` to get an API connector + +## [1.0.0] - 2016-05-20 +### Changed + - Cleaned up everything after definitive testing for stable release + +## 1.0.0-alpha-1 - 2016-05-16 +### Added + - Original fork, feature complete + +[Unreleased]: https://github.com/omines/oauth2-gitlab/compare/3.4.0...master +[3.4.0]: https://github.com/omines/oauth2-gitlab/compare/3.3.0...3.2.0 +[3.3.0]: https://github.com/omines/oauth2-gitlab/compare/3.2.0...3.3.0 +[3.2.0]: https://github.com/omines/oauth2-gitlab/compare/3.1.2...3.2.0 +[3.1.2]: https://github.com/omines/oauth2-gitlab/compare/3.1.1...3.1.2 +[3.1.1]: https://github.com/omines/oauth2-gitlab/compare/3.1.0...3.1.1 +[3.1.0]: https://github.com/omines/oauth2-gitlab/compare/3.0.0...3.1.0 +[3.0.0]: https://github.com/omines/oauth2-gitlab/compare/2.0.0...3.0.0 +[2.0.0]: https://github.com/omines/oauth2-gitlab/compare/1.1.0...2.0.0 +[1.1.0]: https://github.com/omines/oauth2-gitlab/compare/1.0.0...1.1.0 +[1.0.0]: https://github.com/omines/oauth2-gitlab/compare/1.0.0-alpha.1...1.0.0 diff --git a/libs/sudalys/oauth2-client/CONTRIBUTING.md b/libs/sudalys/oauth2-client/CONTRIBUTING.md new file mode 100644 index 0000000..c3c5b21 --- /dev/null +++ b/libs/sudalys/oauth2-client/CONTRIBUTING.md @@ -0,0 +1,7 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +We accept contributions via Pull Requests on [Github](https://github.com/omines/oauth2-gitlab). Follow +[good standards](http://www.phptherightway.com/), keep code coverage at 100%, and run `vendor/bin/php-cs-fixer fix` +before committing. diff --git a/libs/sudalys/oauth2-client/LICENSE b/libs/sudalys/oauth2-client/LICENSE new file mode 100644 index 0000000..1cdd1f4 --- /dev/null +++ b/libs/sudalys/oauth2-client/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Omines Internetbureau B.V. / Steven Maguire + +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/libs/sudalys/oauth2-client/README.md b/libs/sudalys/oauth2-client/README.md new file mode 100644 index 0000000..557e395 --- /dev/null +++ b/libs/sudalys/oauth2-client/README.md @@ -0,0 +1,121 @@ +# GitLab Provider for OAuth 2.0 Client +[![Latest Version](https://img.shields.io/github/release/omines/oauth2-gitlab.svg?style=flat-square)](https://github.com/omines/oauth2-gitlab/releases) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) +[![Build Status](https://img.shields.io/travis/omines/oauth2-gitlab/master.svg?style=flat-square)](https://travis-ci.org/omines/oauth2-gitlab) +[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/omines/oauth2-gitlab.svg?style=flat-square)](https://scrutinizer-ci.com/g/omines/oauth2-gitlab/code-structure) +[![Quality Score](https://img.shields.io/scrutinizer/g/omines/oauth2-gitlab.svg?style=flat-square)](https://scrutinizer-ci.com/g/omines/oauth2-gitlab) +[![Total Downloads](https://img.shields.io/packagist/dt/omines/oauth2-gitlab.svg?style=flat-square)](https://packagist.org/packages/omines/oauth2-gitlab) + +This package provides GitLab OAuth 2.0 support for the PHP League's [OAuth 2.0 Client](https://github.com/thephpleague/oauth2-client). GitLab 8.17 or later is required. + +## Installation + +To install, use composer: + +``` +composer require omines/oauth2-gitlab +``` + +## Usage + +Usage is similar to the basic OAuth client, using `\Omines\OAuth2\Client\Provider\Gitlab` as the provider. + +### Authorization Code Flow + +```php +$provider = new \Omines\OAuth2\Client\Provider\Sudalys([ + 'clientId' => '{gitlab-client-id}', + 'clientSecret' => '{gitlab-client-secret}', + 'redirectUri' => 'https://example.com/callback-url', + 'domain' => 'https://my.gitlab.example', // Optional base URL for self-hosted +]); + +if (!isset($_GET['code'])) { + + // If we don't have an authorization code then get one + $authUrl = $provider->getAuthorizationUrl(); + $_SESSION['oauth2state'] = $provider->getState(); + header('Location: '.$authUrl); + exit; + +// Check given state against previously stored one to mitigate CSRF attack +} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) { + + unset($_SESSION['oauth2state']); + exit('Invalid state'); + +} else { + + // Try to get an access token (using the authorization code grant) + $token = $provider->getAccessToken('authorization_code', [ + 'code' => $_GET['code'], + ]); + + // Optional: Now you have a token you can look up a users profile data + try { + + // We got an access token, let's now get the user's details + $user = $provider->getResourceOwner($token); + + // Use these details to create a new profile + printf('Hello %s!', $user->getName()); + + } catch (Exception $e) { + + // Failed to get user details + exit('Oh dear...'); + } + + // Use this to interact with an API on the users behalf + echo $token->getToken(); +} +``` + +### Managing Scopes + +When creating your GitLab authorization URL, you can specify the state and scopes your application may authorize. + +```php +$options = [ + 'state' => 'OPTIONAL_CUSTOM_CONFIGURED_STATE', + 'scope' => ['read_user','openid'] // array or string +]; + +$authorizationUrl = $provider->getAuthorizationUrl($options); +``` +If neither are defined, the provider will utilize internal defaults ```'api'```. + + +### Performing API calls + +Install [`m4tthumphrey/php-gitlab-api`](https://packagist.org/packages/m4tthumphrey/php-gitlab-api) to interact with the +Gitlab API after authentication. Either connect manually: + +```php +$client = new \Gitlab\Client(); +$client->setUrl('https://my.gitlab.url/api/v4/'); +$client->authenticate($token->getToken(), \Gitlab\Client::AUTH_OAUTH_TOKEN); +``` +Or call the `getApiClient` method on `GitlabResourceOwner` which does the same implicitly. + +## Testing + +```bash +$ ./vendor/bin/phpunit +``` + +## Contributing + +Please see [CONTRIBUTING](https://github.com/omines/oauth2-gitlab/blob/master/CONTRIBUTING.md) for details. + + +## Credits + +This code is a modified fork from the [official Github provider](https://github.com/thephpleague/oauth2-github) adapted +for Gitlab use, so many credits go to [Steven Maguire](https://github.com/stevenmaguire). + +## Legal + +This software was developed for internal use at [Omines Full Service Internetbureau](https://www.omines.nl/) +in Eindhoven, the Netherlands. It is shared with the general public under the permissive MIT license, without +any guarantee of fitness for any particular purpose. Refer to the included `LICENSE` file for more details. diff --git a/libs/sudalys/oauth2-client/composer.json b/libs/sudalys/oauth2-client/composer.json new file mode 100644 index 0000000..1b7503c --- /dev/null +++ b/libs/sudalys/oauth2-client/composer.json @@ -0,0 +1,43 @@ +{ + "name": "sudalys/oauth2-sudalys", + "description": "GitLab OAuth 2.0 Client Provider for The PHP League OAuth2-Client", + "license": "MIT", + "authors": [ + { + "name": "JB LOPEZ", + "email": "contact@jblopez.fr", + "homepage": "https://www.jblopez.fr" + } + ], + "keywords": [ + "oauth", + "oauth2", + "client", + "authorization", + "authorisation", + "gitlab" + ], + "require": { + "php": ">=7.2", + "league/oauth2-client": "^2.4.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.0", + "guzzlehttp/psr7": "^1.6", + "http-interop/http-factory-guzzle": "^1.0", + "mockery/mockery": "^1.0", + "m4tthumphrey/php-gitlab-api": "^10.0|^11.0", + "php-http/guzzle7-adapter": "^0.1", + "phpunit/phpunit": "^8.0|^9.0" + }, + "autoload": { + "psr-4": { + "Sudalys\\OAuth2Sudalys\\Client\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + } +} diff --git a/libs/sudalys/oauth2-client/src/Provider/Exception/SudalysIdentityProviderException.php b/libs/sudalys/oauth2-client/src/Provider/Exception/SudalysIdentityProviderException.php new file mode 100644 index 0000000..9595ed7 --- /dev/null +++ b/libs/sudalys/oauth2-client/src/Provider/Exception/SudalysIdentityProviderException.php @@ -0,0 +1,57 @@ + + */ +class SudalysIdentityProviderException extends IdentityProviderException +{ + /** + * Creates client exception from response. + * + * @param mixed $data Parsed response data + */ + public static function clientException(ResponseInterface $response, $data): IdentityProviderException + { + return static::fromResponse( + $response, + isset($data['message']) ? $data['message'] : $response->getReasonPhrase() + ); + } + + /** + * Creates oauth exception from response. + * + * @param ResponseInterface $response Response received from upstream + * @param array $data Parsed response data + */ + public static function oauthException(ResponseInterface $response, $data): IdentityProviderException + { + return static::fromResponse( + $response, + isset($data['error']) ? $data['error'] : $response->getReasonPhrase() + ); + } + + /** + * Creates identity exception from response. + * + * @param ResponseInterface $response Response received from upstream + * @param string|null $message Parsed message + */ + protected static function fromResponse(ResponseInterface $response, $message = null): IdentityProviderException + { + return new static($message, $response->getStatusCode(), (string) $response->getBody()); + } +} diff --git a/libs/sudalys/oauth2-client/src/Provider/Sudalys.php b/libs/sudalys/oauth2-client/src/Provider/Sudalys.php new file mode 100644 index 0000000..9a56c2b --- /dev/null +++ b/libs/sudalys/oauth2-client/src/Provider/Sudalys.php @@ -0,0 +1,158 @@ +domain = $options['domain']; + } + parent::__construct($options, $collaborators); + } + + /** + * Get authorization url to begin OAuth flow. + */ + public function getBaseAuthorizationUrl(): string + { + return $this->domain . self::PATH_AUTHORIZE; + } + + /** + * Requests resource owner details. + * + * @param AccessToken $token + * @return mixed + */ + protected function fetchResourceOwnerDetails(AccessToken $token) + { + $url = $this->getResourceOwnerDetailsUrl($token); + $request = $this->getAuthenticatedRequest(self::METHOD_GET, $url, $token); + $response = $this->getParsedResponse($request); + if (false === is_array($response)) { + throw new UnexpectedValueException( + 'Invalid response received from Authorization Server. Expected JSON.' + ); + } + + return $response; + } + + /** + * Sends a request and returns the parsed response. + * + * @param RequestInterface $request + * @throws IdentityProviderException + * @return mixed + */ + /* public function getParsedResponse(RequestInterface $request) + { + try { + $response = $this->getResponse($request); + } catch (BadResponseException $e) { + $response = $e->getResponse(); + } + + $parsed = $this->parseResponse($response); + + echo "
----------------------------- PARSED RESPONSE -------------------------
"; + print_r($parsed); + + $this->checkResponse($response, $parsed); + + return $parsed; + }*/ + + /** + * Get access token url to retrieve token. + */ + public function getBaseAccessTokenUrl(array $params): string + { + return $this->domain . self::PATH_TOKEN; + } + + /** + * Get provider url to fetch user details. + */ + public function getResourceOwnerDetailsUrl(AccessToken $token): string + { + //DEBUG echo '
'.$this->domain . self::PATH_API_USER; + return $this->domain . self::PATH_API_USER; + } + + /** + * Get the default scopes used by GitLab. + * Current scopes are 'api', 'read_user', 'openid'. + * + * This returns an array with 'api' scope as default. + */ + protected function getDefaultScopes(): array + { + return [self::DEFAULT_SCOPE]; + } + + /** + * GitLab uses a space to separate scopes. + */ + protected function getScopeSeparator(): string + { + return self::SCOPE_SEPARATOR; + } + + /** + * Check a provider response for errors. + * + * @param ResponseInterface $response Parsed response data + * @throws IdentityProviderException + */ + protected function checkResponse(ResponseInterface $response, $data) + { + if ($response->getStatusCode() >= 400) { + throw SudalysIdentityProviderException::clientException($response, $data); + } elseif (isset($data['error'])) { + throw SudalysIdentityProviderException::oauthException($response, $data); + } + } + + /** + * Generate a user object from a successful user details request. + */ + protected function createResourceOwner(array $response, AccessToken $token): ResourceOwnerInterface + { + $user = new SudalysResourceOwner($response, $token); + + return $user->setDomain($this->domain); + } +} diff --git a/libs/sudalys/oauth2-client/src/Provider/SudalysResourceOwner.php b/libs/sudalys/oauth2-client/src/Provider/SudalysResourceOwner.php new file mode 100644 index 0000000..53ba32e --- /dev/null +++ b/libs/sudalys/oauth2-client/src/Provider/SudalysResourceOwner.php @@ -0,0 +1,156 @@ +data = $response; + $this->token = $token; + } + + /** + * Returns the identifier of the authorized resource owner. + */ + public function getId(): int + { + return (int) $this->get('id'); + } + + + public function getDomain(): string + { + return $this->domain; + } + + /** + * Retrieves ful token resource owner. + * + * @return string|null + */ + public function getIdToken() + { + return $this->token; + } + + + /** + * @return $this + */ + public function setDomain(string $domain): self + { + $this->domain = $domain; + + return $this; + } + + /** + * The full name of the owner. + */ + public function getName(): string + { + return $this->get('name'); + } + + /** + * Username of the owner. + */ + public function getUsername(): string + { + return $this->get('username'); + } + + /** + * Email address of the owner. + */ + public function getEmail(): string + { + return $this->get('email'); + } + + /** + * URL to the user's avatar. + * + * @return string|null + */ + public function getAvatarUrl(): string + { + return $this->get('avatar_url'); + } + + /** + * URL to the user's profile page. + */ + public function getProfileUrl(): string + { + return $this->get('web_url'); + } + + public function getToken(): AccessToken + { + return $this->token; + } + + /** + * Whether the user is active. + */ + public function isActive(): bool + { + return 'active' === $this->get('state'); + } + + /** + * Whether the user is an admin. + */ + public function isAdmin(): bool + { + return (bool) $this->get('is_admin', false); + } + + /** + * Whether the user is external. + */ + public function isExternal(): bool + { + return (bool) $this->get('external', true); + } + + /** + * Return all of the owner details available as an array. + */ + public function toArray(): array + { + return $this->data; + } + + /** + * @param mixed|null $default + * @return mixed|null + */ + protected function get(string $key, $default = null) + { + return isset($this->data[$key]) ? $this->data[$key] : $default; + } +} diff --git a/migrations/Version20250326093406.php b/migrations/Version20250326093406.php new file mode 100644 index 0000000..b497276 --- /dev/null +++ b/migrations/Version20250326093406.php @@ -0,0 +1,50 @@ +addSql('CREATE TABLE "user" (id SERIAL NOT NULL, username VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, roles JSON NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649F85E0677 ON "user" (username)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649E7927C74 ON "user" (email)'); + $this->addSql('CREATE TABLE messenger_messages (id BIGSERIAL NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)'); + $this->addSql('CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)'); + $this->addSql('CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)'); + $this->addSql('COMMENT ON COLUMN messenger_messages.created_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN messenger_messages.available_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN messenger_messages.delivered_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('CREATE OR REPLACE FUNCTION notify_messenger_messages() RETURNS TRIGGER AS $$ + BEGIN + PERFORM pg_notify(\'messenger_messages\', NEW.queue_name::text); + RETURN NEW; + END; + $$ LANGUAGE plpgsql;'); + $this->addSql('DROP TRIGGER IF EXISTS notify_trigger ON messenger_messages;'); + $this->addSql('CREATE TRIGGER notify_trigger AFTER INSERT OR UPDATE ON messenger_messages FOR EACH ROW EXECUTE PROCEDURE notify_messenger_messages();'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP TABLE "user"'); + $this->addSql('DROP TABLE messenger_messages'); + } +} diff --git a/src/Controller/ClientTestController.php b/src/Controller/ClientTestController.php new file mode 100644 index 0000000..aeda58a --- /dev/null +++ b/src/Controller/ClientTestController.php @@ -0,0 +1,82 @@ + false, 'verify_host' => false]); + + foreach ([$authorizeUrl, $tokenUrl, $userInfoUrl] as $url) { + try { + $response = $client->request('GET', $url, ['timeout' => 3]); + $status = $response->getStatusCode(); + $urlTests[$url] = "Status: $status"; + } catch (TransportExceptionInterface $e) { + $urlTests[$url] = "Error: " . $e->getMessage(); + } catch (\Exception $e) { + $urlTests[$url] = "Exception: " . $e->getMessage(); + } + } + + // Test 2: Try to get a token with client credentials + $tokenTest = "Not tested"; + try { + $guzzleClient = new Client([ + 'verify' => false, + 'timeout' => 5, + ]); + + $response = $guzzleClient->post($tokenUrl, [ + 'form_params' => [ + 'grant_type' => 'client_credentials', + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + ], + 'headers' => [ + 'Accept' => 'application/json', + ], + ]); + + $tokenTest = $response->getBody()->getContents(); + } catch (RequestException $e) { + $response = $e->getResponse(); + if ($response) { + $tokenTest = "Error: " . $response->getStatusCode() . " - " . $response->getBody()->getContents(); + } else { + $tokenTest = "Error: " . $e->getMessage(); + } + } catch (\Exception $e) { + $tokenTest = "Exception: " . $e->getMessage(); + } + + return $this->render('client_test/index.html.twig', [ + 'client_id' => $clientId, + 'client_secret' => substr($clientSecret, 0, 3) . '***', + 'authorize_url' => $authorizeUrl, + 'token_url' => $tokenUrl, + 'userinfo_url' => $userInfoUrl, + 'url_tests' => $urlTests, + 'token_test' => $tokenTest, + ]); + } +} \ No newline at end of file diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php new file mode 100644 index 0000000..0c0c4c1 --- /dev/null +++ b/src/Controller/DashboardController.php @@ -0,0 +1,122 @@ +render('dashboard/home.html.twig', [ + 'controller_name' => 'DashboardController', + ]); + } + + #[Route('/dashboard', name: 'app_dashboard')] + public function index(): Response + { + $this->denyAccessUnlessGranted('ROLE_USER'); + + return $this->render('dashboard/index.html.twig', [ + 'controller_name' => 'DashboardController', + ]); + } + + #[Route('/user-info', name: 'app_user_info')] + public function userInfo(LoggerInterface $logger): Response + { + if (!$this->getUser()) { + throw new AccessDeniedException('You must be logged in to view this page'); + } + + $user = $this->getUser(); + $logger->info('User info accessed', [ + 'user' => $user->getUserIdentifier() + ]); + + return $this->render('dashboard/user_info.html.twig', [ + 'user' => $user + ]); + } + + #[Route('/connect/sso', name: 'connect_sso_start')] + public function connectAction(ClientRegistry $clientRegistry, LoggerInterface $logger): Response + { + try { + $client = $clientRegistry->getClient('sudalys'); + + $logger->info('Redirecting to OAuth authorization endpoint'); + + // Don't pass any options - use defaults configured in knpu_oauth2_client.yaml + return $client->redirect(); + } catch (\Exception $e) { + $logger->error('OAuth connection error: ' . $e->getMessage(), [ + 'exception' => get_class($e), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + + return new JsonResponse([ + 'error' => 'OAuth connection failed', + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + 'environment' => [ + 'SSO_CLIENT_ID' => $_ENV['SSO_CLIENT_ID'] ?? 'not set', + 'SSO_CLIENT_SECRET' => substr($_ENV['SSO_CLIENT_SECRET'] ?? 'not set', 0, 3) . '***', + 'SSO_AUTHORIZE_URL' => $_ENV['SSO_AUTHORIZE_URL'] ?? 'not set', + 'SSO_TOKEN_URL' => $_ENV['SSO_TOKEN_URL'] ?? 'not set', + 'SSO_USERINFO_URL' => $_ENV['SSO_USERINFO_URL'] ?? 'not set', + ] + ], 500); + } + } + + #[Route('/connect/sso/check', name: 'connect_sso_check')] + public function connectCheckAction(LoggerInterface $logger): Response + { + $logger->info('OAuth callback called'); + // This method will be intercepted by the OAuth authenticator + return new JsonResponse(['status' => 'This should not be displayed if OAuth is working properly']); + } + + #[Route('/logout', name: 'app_logout')] + public function logout(): void + { + // This method can be empty - it will be intercepted by the logout key on your firewall + throw new \Exception('This should never be reached!'); + } + + #[Route('/oauth-debug', name: 'oauth_debug')] + public function oauthDebug(): Response + { + return new JsonResponse([ + 'SSO_CLIENT_ID' => $_ENV['SSO_CLIENT_ID'] ?? 'not set', + 'SSO_CLIENT_SECRET' => substr($_ENV['SSO_CLIENT_SECRET'] ?? 'not set', 0, 3) . '***', + 'SSO_AUTHORIZE_URL' => $_ENV['SSO_AUTHORIZE_URL'] ?? 'not set', + 'SSO_TOKEN_URL' => $_ENV['SSO_TOKEN_URL'] ?? 'not set', + 'SSO_USERINFO_URL' => $_ENV['SSO_USERINFO_URL'] ?? 'not set', + ]); + } + + #[Route('/login-status', name: 'app_login_status')] + public function loginStatus(): JsonResponse + { + $user = $this->getUser(); + + return new JsonResponse([ + 'logged_in' => $user !== null, + 'user' => $user ? [ + 'identifier' => $user->getUserIdentifier(), + 'roles' => $user->getRoles(), + ] : null, + ]); + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..68fe10f --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,109 @@ +id; + } + + public function getUsername(): ?string + { + return $this->username; + } + + public function setUsername(string $username): static + { + $this->username = $username; + + return $this; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(string $password): static + { + $this->password = $password; + + return $this; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): static + { + $this->email = $email; + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->email; + } + + /** + * @see UserInterface + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + public function setRoles(array $roles): static + { + $this->roles = $roles; + + return $this; + } + + /** + * @see UserInterface + */ + public function eraseCredentials(): void + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..b29153b --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,43 @@ + + */ +class UserRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + // /** + // * @return User[] Returns an array of User objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('u.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?User + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Security/OAuthAuthenticator.php b/src/Security/OAuthAuthenticator.php new file mode 100644 index 0000000..4c91fcd --- /dev/null +++ b/src/Security/OAuthAuthenticator.php @@ -0,0 +1,141 @@ +clientRegistry = $clientRegistry; + $this->router = $router; + $this->logger = $logger; + } + + public function supports(Request $request): ?bool + { + return $request->attributes->get('_route') === 'connect_sso_check' && $request->query->has('code'); + } + + public function authenticate(Request $request): Passport + { + $client = $this->getSudalysClient(); + $accessToken = $this->fetchAccessToken($client); + + return new SelfValidatingPassport( + new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) { + $sudalysSsoUser = $this->getSudalysClient()->fetchUserFromToken($accessToken); + + $email = $sudalysSsoUser->getEmail(); + + /* + * On regarde si l'utilisateur est deja connecté + */ + $connectedUser = $this->em->getRepository(User::class)->findOneBy(['ssoToken' => $sudalysSsoUser->getIdToken()]); + if ($connectedUser) { + return $connectedUser; + } + + // On reggarde si user existe en bdd + /** @var User $userInDatabase */ + $user = $this->em->getRepository(User::class)->findOneBy(['email' => $email]); + + + /** + * A commenter si on ne veut pas creer l utilisateur s il n existe pas en bdd locale + * **/ + if (!$user) { + $user = new User(); + $user->setEmail($sudalysSsoUser->getEmail()); + $user->setPassword("createAPasswordMethod"); + $this->em->persist($user); + } + + // On met a jour le token + $user->setSsoToken($sudalysSsoUser->getIdToken()); + $this->em->flush(); + + return $user; + }) + ); + } + + private function getUserIdentifier(ResourceOwnerInterface $resourceOwner): string + { + // Get the unique identifier from the resource owner + // This depends on your SSO server's response format + // For standard OAuth servers, this might be the 'sub' field + $data = $resourceOwner->toArray(); + + $this->logger->debug('Resource owner data', [ + 'data' => $data + ]); + + // Try to get a unique identifier from the data + if (isset($data['sub'])) { + return $data['sub']; + } elseif (isset($data['id'])) { + return $data['id']; + } elseif (isset($data['email'])) { + return $data['email']; + } + + // Fallback to a hash of the entire data if no good identifier is found + return md5(json_encode($data)); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + $this->logger->info('OAuth authentication successful', [ + 'user' => $token->getUserIdentifier() + ]); + + return new RedirectResponse($this->router->generate('app_home')); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + $this->logger->error('OAuth authentication failure', [ + 'message' => $exception->getMessage(), + 'code' => $exception->getCode() + ]); + + return new JsonResponse([ + 'error' => 'Authentication failed', + 'message' => $exception->getMessage(), + 'query_params' => $request->query->all() + ], Response::HTTP_UNAUTHORIZED); + } + + public function start(Request $request, AuthenticationException $authException = null): Response + { + return new RedirectResponse( + $this->router->generate('connect_sso_start'), + Response::HTTP_TEMPORARY_REDIRECT + ); + } + + private function getSudalysClient() + { + return $this->clientRegistry->getClient('sudalys'); + } +} \ No newline at end of file diff --git a/src/Security/OAuthUserProvider.php b/src/Security/OAuthUserProvider.php new file mode 100644 index 0000000..3a0a02b --- /dev/null +++ b/src/Security/OAuthUserProvider.php @@ -0,0 +1,76 @@ +entityManager = $entityManager; + $this->clientRegistry = $clientRegistry; + } + + /** + * Loads a user from the given token. + */ + public function loadUserByIdentifier(string $identifier): UserInterface + { + // Try to find an existing user with this identifier + $userRepository = $this->entityManager->getRepository(User::class); + $user = $userRepository->findOneBy(['username' => $identifier]); + + // If the user doesn't exist, create one + if (!$user) { + $user = new User(); + $user->setUsername($identifier); + $user->setPassword(''); // No password for OAuth users + + // Try to get an email from the current client's token data if available + try { + $client = $this->clientRegistry->getClient('sso_server'); + $accessToken = $client->getAccessToken(); + if ($accessToken) { + $resourceOwner = $client->fetchUserFromToken($accessToken); + $userData = $resourceOwner->toArray(); + + // Set email if available in the response + if (isset($userData['email'])) { + $user->setEmail($userData['email']); + } else { + $user->setEmail($identifier . '@example.com'); + } + } else { + $user->setEmail($identifier . '@example.com'); + } + } catch (\Exception $e) { + // Fallback if we can't get the email + $user->setEmail($identifier . '@example.com'); + } + + $user->setRoles(['ROLE_USER']); + + $this->entityManager->persist($user); + $this->entityManager->flush(); + } + + return $user; + } + + /** + * Check if this provider supports the given user class. + */ + public function supportsClass($class): bool + { + return $class === User::class || is_subclass_of($class, User::class); + } +} \ No newline at end of file diff --git a/symfony.lock b/symfony.lock index 4947424..0d4b7cd 100644 --- a/symfony.lock +++ b/symfony.lock @@ -26,6 +26,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" + ] + }, "phpunit/phpunit": { "version": "9.6", "recipe": { diff --git a/templates/client_test/index.html.twig b/templates/client_test/index.html.twig new file mode 100644 index 0000000..2a55ee3 --- /dev/null +++ b/templates/client_test/index.html.twig @@ -0,0 +1,54 @@ +{% extends 'base.html.twig' %} + +{% block title %}OAuth Client Test{% endblock %} + +{% block body %} +
+

OAuth Client Test

+ +
+
+

OAuth Configuration

+
+
+
    +
  • Client ID: {{ client_id }}
  • +
  • Client Secret: {{ client_secret }}
  • +
  • Authorize URL: {{ authorize_url }}
  • +
  • Token URL: {{ token_url }}
  • +
  • UserInfo URL: {{ userinfo_url }}
  • +
+
+
+ +
+
+

URL Connectivity Tests

+
+
+
    + {% for url, result in url_tests %} +
  • + {{ url }}: +
    {{ result }}
    +
  • + {% endfor %} +
+
+
+ +
+
+

Token Request Test

+
+
+
{{ token_test }}
+
+
+ +
+ Back to Home + Try SSO Login +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/dashboard/home.html.twig b/templates/dashboard/home.html.twig new file mode 100644 index 0000000..12659b8 --- /dev/null +++ b/templates/dashboard/home.html.twig @@ -0,0 +1,43 @@ +{% extends 'base.html.twig' %} + +{% block title %}Welcome{% endblock %} + +{% block body %} +
+
+
+
+
+

Welcome to the OAuth SSO Client

+
+
+ {% if app.user %} +
+

✅ You are successfully logged in!

+

Your identifier: {{ app.user.userIdentifier }}

+
+ + + {% else %} +
+

⚠️ You are not logged in

+

Click the button below to login with SSO

+
+ + + {% endif %} +
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/dashboard/index.html.twig b/templates/dashboard/index.html.twig new file mode 100644 index 0000000..fefb85b --- /dev/null +++ b/templates/dashboard/index.html.twig @@ -0,0 +1,53 @@ +{% extends 'base.html.twig' %} + +{% block title %}Dashboard - Protected Area{% endblock %} + +{% block body %} +
+
+
+
+
+

Dashboard - Protected Area

+
+
+
+

✅ Secure Dashboard

+

This page is only accessible to authenticated users with ROLE_USER.

+
+ +
+
+ User Information +
+
+

You are logged in as: {{ app.user.userIdentifier }}

+

Roles: + {% for role in app.user.roles %} + {{ role }} + {% endfor %} +

+
+
+ +
+
+ Protected Content +
+
+
Welcome to the protected area of the application
+

This is sensitive information that only authenticated users can see.

+

Your SSO authentication has successfully granted you access to this protected resource.

+
+
+
+ +
+
+
+
+{% endblock %} diff --git a/templates/dashboard/user_info.html.twig b/templates/dashboard/user_info.html.twig new file mode 100644 index 0000000..af4e09a --- /dev/null +++ b/templates/dashboard/user_info.html.twig @@ -0,0 +1,69 @@ +{% extends 'base.html.twig' %} + +{% block title %}User Information{% endblock %} + +{% block body %} +
+
+
+
+
+

User Information

+
+
+
+

✅ You are successfully logged in!

+
+ +

User Details

+ + + + + + + + + + + + + + + + + + + +
Identifier:{{ user.userIdentifier }}
Username:{{ user.username }}
Email:{{ user.email }}
Roles: +
    + {% for role in user.roles %} +
  • {{ role }}
  • + {% endfor %} +
+
+ +

User Object

+
+
+
{{ dump(user) }}
+
+
+ +

Session Information

+
+
+
{{ dump(app.session) }}
+
+
+
+ +
+
+
+
+{% endblock %} \ No newline at end of file