temp client

This commit is contained in:
Charles 2025-03-27 11:40:04 +01:00
parent d9ce5bbb3f
commit 485b7b7712
27 changed files with 2200 additions and 11 deletions

View File

@ -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": {

655
composer.lock generated
View File

@ -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",

View File

@ -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],
];

View File

@ -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

View File

@ -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 }

View File

@ -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

9
libs/sudalys/oauth2-client/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
/build
/log
/vendor
.*.cache
composer.phar
composer.lock
infection.json
infection.phar*
phpunit.xml

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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"
}
}
}

View File

@ -0,0 +1,57 @@
<?php
/*
* Sudalys OAuth2 Provider
*
*/
namespace Sudalys\OAuth2\Client\Provider\Exception;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use Psr\Http\Message\ResponseInterface;
/**
* GitlabIdentityProviderException.
*
* @author Niels Keurentjes <niels.keurentjes@omines.com>
*/
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());
}
}

View File

@ -0,0 +1,158 @@
<?php
/*
* Sudalys OAuth2 Provider
*/
namespace Sudalys\OAuth2\Client\Provider;
use GuzzleHttp\Exception\BadResponseException;
use http\Exception\UnexpectedValueException;
use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Sudalys\OAuth2\Client\Provider\SudalysResourceOwner;
use Sudalys\OAuth2\Client\Provider\Exception\SudalysIdentityProviderException;
class Sudalys extends AbstractProvider
{
use BearerAuthorizationTrait;
const PATH_API_USER = '/oauth/api/user';
const PATH_AUTHORIZE = '/authorize';
const PATH_TOKEN = '/token';
const DEFAULT_SCOPE = 'email';
const SCOPE_SEPARATOR = ' ';
/** @var string */
public $domain = 'http://localhost:8000';
/**
* Sudalys constructor.
*/
public function __construct(array $options, array $collaborators = [])
{
if (isset($options['domain'])) {
$this->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 "<br>----------------------------- PARSED RESPONSE -------------------------<br>";
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 '<br> '.$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);
}
}

View File

@ -0,0 +1,156 @@
<?php
/*
* Sudalys OAuth2 Provider
*/
namespace Sudalys\OAuth2\Client\Provider;
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
use League\OAuth2\Client\Token\AccessToken;
class SudalysResourceOwner implements ResourceOwnerInterface
{
const PATH_API = '/api/v4/';
/** @var array */
private $data;
/** @var string */
private $domain;
/** @var AccessToken */
private $token;
/**
* Creates new resource owner.
*/
public function __construct(array $response, AccessToken $token)
{
$this->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;
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250326093406 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->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');
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
class ClientTestController extends AbstractController
{
#[Route('/client/test', name: 'app_client_test')]
public function index(): Response
{
// Get OAuth configuration
$clientId = $_ENV['SSO_CLIENT_ID'] ?? 'not set';
$clientSecret = $_ENV['SSO_CLIENT_SECRET'] ?? 'not set';
$authorizeUrl = $_ENV['SSO_AUTHORIZE_URL'] ?? 'not set';
$tokenUrl = $_ENV['SSO_TOKEN_URL'] ?? 'not set';
$userInfoUrl = $_ENV['SSO_USERINFO_URL'] ?? 'not set';
// Test 1: Check if URLs are accessible
$urlTests = [];
$client = HttpClient::create(['verify_peer' => 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,
]);
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Component\HttpFoundation\JsonResponse;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
final class DashboardController extends AbstractController
{
#[Route('/', name: 'app_home')]
public function home(): Response
{
return $this->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,
]);
}
}

109
src/Entity/User.php Normal file
View File

@ -0,0 +1,109 @@
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, unique: true)]
private ?string $username = null;
#[ORM\Column(length: 255)]
private ?string $password = null;
#[ORM\Column(length: 255, unique: true)]
private ?string $email = null;
#[ORM\Column(type: 'json')]
private array $roles = [];
public function getId(): ?int
{
return $this->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;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<User>
*/
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()
// ;
// }
}

View File

@ -0,0 +1,141 @@
<?php
namespace App\Security;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
class OAuthAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface
{
private $clientRegistry;
private $router;
private $logger;
public function __construct(ClientRegistry $clientRegistry, RouterInterface $router, LoggerInterface $logger)
{
$this->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');
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\User\OAuthUserProvider as BaseOAuthUserProvider;
use Symfony\Component\Security\Core\User\UserInterface;
// TODO : delete this file
class OAuthUserProvider extends BaseOAuthUserProvider
{
private EntityManagerInterface $entityManager;
private ClientRegistry $clientRegistry;
public function __construct(EntityManagerInterface $entityManager, ClientRegistry $clientRegistry)
{
$this->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);
}
}

View File

@ -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": {

View File

@ -0,0 +1,54 @@
{% extends 'base.html.twig' %}
{% block title %}OAuth Client Test{% endblock %}
{% block body %}
<div class="container mt-4">
<h1>OAuth Client Test</h1>
<div class="card mb-4">
<div class="card-header">
<h2>OAuth Configuration</h2>
</div>
<div class="card-body">
<ul class="list-group">
<li class="list-group-item"><strong>Client ID:</strong> {{ client_id }}</li>
<li class="list-group-item"><strong>Client Secret:</strong> {{ client_secret }}</li>
<li class="list-group-item"><strong>Authorize URL:</strong> {{ authorize_url }}</li>
<li class="list-group-item"><strong>Token URL:</strong> {{ token_url }}</li>
<li class="list-group-item"><strong>UserInfo URL:</strong> {{ userinfo_url }}</li>
</ul>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2>URL Connectivity Tests</h2>
</div>
<div class="card-body">
<ul class="list-group">
{% for url, result in url_tests %}
<li class="list-group-item">
<strong>{{ url }}:</strong>
<pre>{{ result }}</pre>
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2>Token Request Test</h2>
</div>
<div class="card-body">
<pre>{{ token_test }}</pre>
</div>
</div>
<div class="mt-4">
<a href="{{ path('app_home') }}" class="btn btn-primary">Back to Home</a>
<a href="{{ path('connect_sso_start') }}" class="btn btn-success">Try SSO Login</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends 'base.html.twig' %}
{% block title %}Welcome{% endblock %}
{% block body %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h1 class="h3 mb-0">Welcome to the OAuth SSO Client</h1>
</div>
<div class="card-body">
{% if app.user %}
<div class="alert alert-success">
<h4>✅ You are successfully logged in!</h4>
<p>Your identifier: <strong>{{ app.user.userIdentifier }}</strong></p>
</div>
<div class="d-grid gap-2 mt-4">
<a href="{{ path('app_user_info') }}" class="btn btn-info">View User Details</a>
<a href="{{ path('app_dashboard') }}" class="btn btn-primary">Go to Dashboard</a>
<a href="{{ path('app_logout') }}" class="btn btn-danger">Logout</a>
</div>
{% else %}
<div class="alert alert-warning">
<h4>⚠️ You are not logged in</h4>
<p>Click the button below to login with SSO</p>
</div>
<div class="d-grid gap-2 mt-4">
<a href="{{ path('connect_sso_start') }}" class="btn btn-primary btn-lg">Login with SSO</a>
</div>
{% endif %}
</div>
<div class="card-footer text-muted">
<small>Powered by Symfony SSO Client</small>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,53 @@
{% extends 'base.html.twig' %}
{% block title %}Dashboard - Protected Area{% endblock %}
{% block body %}
<div class="container mt-4">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h1 class="h3 mb-0">Dashboard - Protected Area</h1>
</div>
<div class="card-body">
<div class="alert alert-success">
<h4>✅ Secure Dashboard</h4>
<p>This page is only accessible to authenticated users with <code>ROLE_USER</code>.</p>
</div>
<div class="card mb-4">
<div class="card-header">
User Information
</div>
<div class="card-body">
<p>You are logged in as: <strong>{{ app.user.userIdentifier }}</strong></p>
<p>Roles:
{% for role in app.user.roles %}
<span class="badge bg-secondary">{{ role }}</span>
{% endfor %}
</p>
</div>
</div>
<div class="card">
<div class="card-header">
Protected Content
</div>
<div class="card-body">
<h5>Welcome to the protected area of the application</h5>
<p>This is sensitive information that only authenticated users can see.</p>
<p>Your SSO authentication has successfully granted you access to this protected resource.</p>
</div>
</div>
</div>
<div class="card-footer">
<a href="{{ path('app_home') }}" class="btn btn-secondary">Back to Home</a>
<a href="{{ path('app_user_info') }}" class="btn btn-info">User Details</a>
<a href="{{ path('app_logout') }}" class="btn btn-danger">Logout</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,69 @@
{% extends 'base.html.twig' %}
{% block title %}User Information{% endblock %}
{% block body %}
<div class="container mt-4">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header bg-primary text-white">
<h1>User Information</h1>
</div>
<div class="card-body">
<div class="alert alert-success">
<h3>✅ You are successfully logged in!</h3>
</div>
<h4 class="mt-4">User Details</h4>
<table class="table table-bordered">
<tbody>
<tr>
<th>Identifier:</th>
<td>{{ user.userIdentifier }}</td>
</tr>
<tr>
<th>Username:</th>
<td>{{ user.username }}</td>
</tr>
<tr>
<th>Email:</th>
<td>{{ user.email }}</td>
</tr>
<tr>
<th>Roles:</th>
<td>
<ul class="list-unstyled">
{% for role in user.roles %}
<li><span class="badge bg-secondary">{{ role }}</span></li>
{% endfor %}
</ul>
</td>
</tr>
</tbody>
</table>
<h4 class="mt-4">User Object</h4>
<div class="card mb-3">
<div class="card-body bg-light">
<pre>{{ dump(user) }}</pre>
</div>
</div>
<h4 class="mt-4">Session Information</h4>
<div class="card">
<div class="card-body bg-light">
<pre>{{ dump(app.session) }}</pre>
</div>
</div>
</div>
<div class="card-footer">
<a href="{{ path('app_home') }}" class="btn btn-secondary">Back to Home</a>
<a href="{{ path('app_dashboard') }}" class="btn btn-primary">Go to Dashboard</a>
<a href="{{ path('app_logout') }}" class="btn btn-danger">Logout</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}