diff --git a/.env b/.env index 47f3c7ff..d67e0dda 100644 --- a/.env +++ b/.env @@ -34,12 +34,20 @@ DATABASE_URL="mysql://davis:davis@127.0.0.1:3306/davis?serverVersion=10.9.3-Mari MAILER_DSN=smtp://localhost:465?encryption=ssl&auth_mode=login&username=&password= ###< symfony/mailer ### -# The admin password for the backend +# Admin authentication method: "local" (default), "oidc", or "none" +# "none" bypasses authentication entirely (for upstream reverse-proxy auth) +ADMIN_AUTH_METHOD=local +# The admin password for the backend (in case of 'local' auth) ADMIN_LOGIN=admin ADMIN_PASSWORD=test -# You can bypass auth entirely by setting this to "true" (case sensitive). -# Useful if you use an external authorization provider such as Authelia -ADMIN_AUTH_BYPASS=false +# Required when ADMIN_AUTH_METHOD=oidc +OIDC_CLIENT_ID=davis +OIDC_CLIENT_SECRET= +OIDC_URL_AUTHORIZE=https://authelia.example.com/api/oidc/authorization +OIDC_URL_ACCESS_TOKEN=https://authelia.example.com/api/oidc/token +OIDC_URL_RESOURCE_OWNER=https://authelia.example.com/api/oidc/userinfo +# Space-separated scopes; "openid" is mandatory +OIDC_SCOPES=openid profile email # Auth Realm for HTTP auth AUTH_REALM=SabreDAV diff --git a/composer.json b/composer.json index 3f32ca18..fcecfb35 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "doctrine/doctrine-bundle": "^2.15.1", "doctrine/doctrine-migrations-bundle": "^3.4.2", "doctrine/orm": "^2.20.6", + "knpuniversity/oauth2-client-bundle": "^2.20", "sabre/dav": "^4.7.0", "symfony/apache-pack": "^1.0.1", "symfony/asset": "^7.4", diff --git a/composer.lock b/composer.lock index ddcb1e49..c05f67b6 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": "6ef4adb8a363d64ac0165dfd7eb5edae", + "content-hash": "30b4fc190a3a511fdc3a328cc99339af", "packages": [ { "name": "carbonphp/carbon-doctrine-types", @@ -1582,6 +1582,332 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "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.10.0" + }, + "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": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "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.3.0" + }, + "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": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", + "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", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "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.9.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": "2026-03-10T16:41:02+00:00" + }, { "name": "illuminate/collections", "version": "v12.53.0", @@ -1963,6 +2289,131 @@ }, "time": "2026-02-23T15:44:06+00:00" }, + { + "name": "knpuniversity/oauth2-client-bundle", + "version": "v2.20.2", + "source": { + "type": "git", + "url": "https://github.com/knpuniversity/oauth2-client-bundle.git", + "reference": "9ce4fcea69dbbf4d19ee7368b8d623ec2d73d3c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/knpuniversity/oauth2-client-bundle/zipball/9ce4fcea69dbbf4d19ee7368b8d623ec2d73d3c7", + "reference": "9ce4fcea69dbbf4d19ee7368b8d623ec2d73d3c7", + "shasum": "" + }, + "require": { + "league/oauth2-client": "^2.0", + "php": ">=8.1", + "symfony/dependency-injection": "^6.4|^7.3|^8.0", + "symfony/framework-bundle": "^6.4|^7.3|^8.0", + "symfony/http-foundation": "^6.4|^7.3|^8.0", + "symfony/routing": "^6.4|^7.3|^8.0", + "symfony/security-core": "^6.4|^7.3|^8.0", + "symfony/security-http": "^6.4|^7.3|^8.0" + }, + "require-dev": { + "league/oauth2-facebook": "^1.1|^2.0", + "symfony/phpunit-bridge": "^7.3", + "symfony/yaml": "^6.4|^7.3|^8.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.20.2" + }, + "time": "2026-02-12T17:07:18+00:00" + }, + { + "name": "league/oauth2-client", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "26e8c5da4f3d78cede7021e09b1330a0fc093d5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/26e8c5da4f3d78cede7021e09b1330a0fc093d5e", + "reference": "26e8c5da4f3d78cede7021e09b1330a0fc093d5e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "php": "^7.1 || >=8.0.0 <8.6.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.9.0" + }, + "time": "2025-11-25T22:17:17+00:00" + }, { "name": "monolog/monolog", "version": "3.10.0", @@ -2371,6 +2822,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", @@ -2528,6 +3139,50 @@ }, "time": "2021-10-29T13:26:27+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "sabre/dav", "version": "4.7.0", diff --git a/config/bundles.php b/config/bundles.php index 8e6db214..ccba4b2f 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -11,4 +11,5 @@ Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => 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 00000000..19d98594 --- /dev/null +++ b/config/packages/knpu_oauth2_client.yaml @@ -0,0 +1,14 @@ +knpu_oauth2_client: + clients: + oidc: + type: generic + provider_class: League\OAuth2\Client\Provider\GenericProvider + provider_options: + urlAuthorize: '%env(OIDC_URL_AUTHORIZE)%' + urlAccessToken: '%env(OIDC_URL_ACCESS_TOKEN)%' + urlResourceOwnerDetails: '%env(OIDC_URL_RESOURCE_OWNER)%' + # Request the openid scope so we get an id_token with claims + scopes: '%env(OIDC_SCOPES)%' + client_id: '%env(OIDC_CLIENT_ID)%' + client_secret: '%env(OIDC_CLIENT_SECRET)%' + redirect_route: app_oidc_check diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 3b697cb8..3c4a7ce0 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -21,7 +21,13 @@ security: logout: path: app_logout target: dashboard - + dashboard: + entry_point: App\Security\OidcAuthenticator + custom_authenticators: + - App\Security\OidcAuthenticator + form_login: + login_path: login + check_path: login access_control: - { path: ^/$, roles: PUBLIC_ACCESS } diff --git a/config/reference.php b/config/reference.php index 393b031a..63a3b50f 100644 --- a/config/reference.php +++ b/config/reference.php @@ -1475,6 +1475,15 @@ * generate_final_classes?: bool|Param, // Default: true * generate_final_entities?: bool|Param, // Default: false * } + * @psalm-type KnpuOauth2ClientConfig = array{ + * http_client?: scalar|Param|null, // Service id of HTTP client to use (must implement GuzzleHttp\ClientInterface) // Default: null + * http_client_options?: array{ + * timeout?: int|Param, + * proxy?: scalar|Param|null, + * verify?: bool|Param, // Use only with proxy option set + * }, + * clients?: array>, + * } * @psalm-type ConfigType = array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -1487,6 +1496,7 @@ * web_profiler?: WebProfilerConfig, * monolog?: MonologConfig, * debug?: DebugConfig, + * knpu_oauth2_client?: KnpuOauth2ClientConfig, * "when@dev"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -1500,6 +1510,7 @@ * monolog?: MonologConfig, * debug?: DebugConfig, * maker?: MakerConfig, + * knpu_oauth2_client?: KnpuOauth2ClientConfig, * }, * "when@test"?: array{ * imports?: ImportsConfig, @@ -1513,6 +1524,7 @@ * web_profiler?: WebProfilerConfig, * monolog?: MonologConfig, * debug?: DebugConfig, + * knpu_oauth2_client?: KnpuOauth2ClientConfig, * }, * ...getClient('oidc')->redirect([], []); + } + + // The actual token exchange is done by OidcAuthenticator. + // This action is never reached; it just needs to exist for routing. + #[Route('/dashboard/connect/oidc/check', name: 'app_oidc_check')] + public function check(): void {} +} diff --git a/src/Security/OidcAuthenticator.php b/src/Security/OidcAuthenticator.php new file mode 100644 index 00000000..5f506a78 --- /dev/null +++ b/src/Security/OidcAuthenticator.php @@ -0,0 +1,79 @@ +adminAuthMethod !== 'oidc') { + return false; + } + + return $request->attributes->get('_route') === 'app_oidc_check'; + } + + public function authenticate(Request $request): Passport + { + $client = $this->clientRegistry->getClient('oidc'); + $accessToken = $this->fetchAccessToken($client); + + return new SelfValidatingPassport( + new UserBadge($accessToken->getToken(), function () use ($client, $accessToken) { + // Fetch claims from userinfo endpoint + $resourceOwner = $client->fetchUserFromToken($accessToken); + $claims = $resourceOwner->toArray(); + + // Prefer email, fall back to sub + $username = $claims['email'] ?? $claims['sub'] ?? $resourceOwner->getId(); + + return new \Symfony\Component\Security\Core\User\InMemoryUser( + $username, + null, + ['ROLE_ADMIN'], + ); + }) + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + return new RedirectResponse($this->router->generate('dashboard')); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + return new RedirectResponse($this->router->generate('login', [ + 'error' => $exception->getMessageKey(), + ])); + } + + // AuthenticationEntryPointInterface: redirect to OIDC when unauthenticated + public function start(Request $request, ?AuthenticationEntryPointInterface $authException = null): Response + { + if ($this->adminAuthMethod === 'oidc') { + return new RedirectResponse($this->router->generate('app_oidc_connect')); + } + // Fall back to the local login page + return new RedirectResponse($this->router->generate('login')); + } +} diff --git a/symfony.lock b/symfony.lock index 22b7a976..17761067 100644 --- a/symfony.lock +++ b/symfony.lock @@ -108,6 +108,18 @@ ".php_cs.dist" ] }, + "knpuniversity/oauth2-client-bundle": { + "version": "2.20", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.20", + "ref": "1ff300d8c030f55c99219cc55050b97a695af3f6" + }, + "files": [ + "config/packages/knpu_oauth2_client.yaml" + ] + }, "monolog/monolog": { "version": "2.1.1" }, diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig index b76dac61..e5bf1b28 100644 --- a/templates/security/login.html.twig +++ b/templates/security/login.html.twig @@ -8,29 +8,37 @@ {{ "login.already"|trans({username: app.user.username}) }}, {{ "logout"|trans }} {% else %} -
-
-
- {% if error %} -
{{ error.messageKey|trans(error.messageData, 'security') }}
- {% endif %} + {% if app.request.server.get('ADMIN_AUTH_METHOD') == 'oidc' %} + {# Show OIDC button, optionally hide the local form #} + + {{ 'login.oidc_button'|trans }} + + {# Optionally show local form as fallback, or hide entirely #} + {% else %} +
+
+ + {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} -

{{ "login.signin"|trans }}

+

{{ "login.signin"|trans }}

-
- - -
-
- - -
- +
+ + +
+
+ + +
+ - - + + +
-
+ {% endif %} {% endif %} {% endblock %}