diff --git a/.docker/oidc-server-mock/cert/docker.pfx b/.docker/oidc-server-mock/cert/docker.pfx
new file mode 100644
index 0000000..36f6258
Binary files /dev/null and b/.docker/oidc-server-mock/cert/docker.pfx differ
diff --git a/.env b/.env
index 5aed2be..39d597a 100644
--- a/.env
+++ b/.env
@@ -33,6 +33,27 @@ MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
###< nelmio/cors-bundle ###
+# Set this to a non-empty value in .env.local to disable the OIDC service.
+# DOCKER_OIDC_DISABLE=""
+
DEFAULT_LOCALE=en
SITE_TITLE="RPA Process Overview"
+DEFAULT_URI=https://rpa-process-overview.local.itkdev.dk/
+
+# https://github.com/itk-dev/openid-connect-bundle
+# "admin" open id connect configuration variables (values provided by the OIDC IdP)
+# For production, these must be overridden in .env.local.
+ADMIN_OIDC_ALLOW_HTTP=true
+ADMIN_OIDC_METADATA_URL=http://idp.rpa-process-overview.local.itkdev.dk/.well-known/openid-configuration
+ADMIN_OIDC_CLIENT_ID=client-id
+ADMIN_OIDC_CLIENT_SECRET=client-secret
+ADMIN_OIDC_REDIRECT_URI=https://rpa-process-overview.local.itkdev.dk/
+ADMIN_OIDC_LEEWAY=30
+ADMIN_OIDC_ROLE_MAP='{
+ "overview-manager": ["ROLE_OVERVIEW_MANAGER"]
+}'
+
+# cli redirect url
+OIDC_CLI_LOGIN_ROUTE=app_default
+###< itk-dev/openid-connect-bundle ###
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cce2bb5..64e9dab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+* [PR-10](https://github.com/itk-dev/rpa-process-overview/pull/10)
+ Added OIDC login
* [PR-9](https://github.com/itk-dev/rpa-process-overview/pull/9)
Tailwind classes in twig and svelte files
* [PR-8](https://github.com/itk-dev/rpa-process-overview/pull/8)
diff --git a/README.md b/README.md
index 5ec8d99..00ae682 100644
--- a/README.md
+++ b/README.md
@@ -73,10 +73,42 @@ curl -H "Origin: http://127.0.0.1:3000/ProcessOverview?page=3" \
## User management
-We have a number of commands for managing users. Run
+[Symfony supports OpenID Connect](https://symfony.com/doc/current/security/access_token.html#using-openid-connect-oidc),
+but our IdP does not play well with that. Therefore, we use our own battle-tested [OpenId Connect
+Bundle](https://github.com/itk-dev/openid-connect-bundle) for OIDC login.
-``` shell
-task console -- list app:user
+The bundle is configured with some environment variables:
+
+``` dotenv
+# .env.local
+ADMIN_OIDC_ALLOW_HTTP=false
+# Get these from your IdP provider
+ADMIN_OIDC_METADATA_URL=https://…/.well-known/openid-configuration
+ADMIN_OIDC_CLIENT_ID=…
+ADMIN_OIDC_CLIENT_SECRET=…
+
+ADMIN_OIDC_REDIRECT_URI=https://rpa-process-overview.example.com/
+
+ADMIN_OIDC_ROLE_MAP='{
+ "overview-manager": ["ROLE_OVERVIEW_MANAGER"]
+}'
```
-to see the list of user related commands.
+For local testing of OIDC login, we use [OpenId Connect Server Mock](https://github.com/Soluto/oidc-server-mock) (cf.
+[`docker-compose.oidc.yml`](docker-compose.oidc.yml)) and the mock is running on
+.
+
+The mock provides these users (cf. [`docker-compose.oidc.yml`](docker-compose.oidc.yml)):
+
+| Username | Password | Roles |
+|------------------|------------------|------------------|
+| admin | admin | administrator |
+| overview-manager | overview-manager | overview-manager |
+| user | user | user |
+
+> [!TIP]
+> Set `DOCKER_OIDC_DISABLE` to a non-empty value in `.env.local` to disable the OIDC service, e.g.
+>
+> ``` dotend
+> # .env.local
+> DOCKER_OIDC_DISABLE=true
diff --git a/composer.json b/composer.json
index ba4bb4d..3253a13 100644
--- a/composer.json
+++ b/composer.json
@@ -11,6 +11,7 @@
"doctrine/doctrine-migrations-bundle": "^3.4.2",
"doctrine/orm": "^3.5.2",
"easycorp/easyadmin-bundle": "^4.25.1",
+ "itk-dev/openid-connect-bundle": "^4.0",
"league/uri-components": "^7.5.1",
"runtime/frankenphp-symfony": "^0.2.0",
"stof/doctrine-extensions-bundle": "^1.14",
diff --git a/composer.lock b/composer.lock
index 6858bdc..7f1d885 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": "c86c9f1ba9f64bd04b3d9ba4896749d0",
+ "content-hash": "9ab9315fdca2ad1a44b0e4087c7b94f1",
"packages": [
{
"name": "composer/semver",
@@ -1301,6 +1301,69 @@
],
"time": "2025-09-10T05:00:12+00:00"
},
+ {
+ "name": "firebase/php-jwt",
+ "version": "v6.11.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/firebase/php-jwt.git",
+ "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
+ "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.0"
+ },
+ "require-dev": {
+ "guzzlehttp/guzzle": "^7.4",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.5",
+ "psr/cache": "^2.0||^3.0",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.0"
+ },
+ "suggest": {
+ "ext-sodium": "Support EdDSA (Ed25519) signatures",
+ "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Firebase\\JWT\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Neuman Vong",
+ "email": "neuman+pear@twilio.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Anant Narayanan",
+ "email": "anant@php.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
+ "homepage": "https://github.com/firebase/php-jwt",
+ "keywords": [
+ "jwt",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/firebase/php-jwt/issues",
+ "source": "https://github.com/firebase/php-jwt/tree/v6.11.1"
+ },
+ "time": "2025-04-09T20:32:01+00:00"
+ },
{
"name": "gedmo/doctrine-extensions",
"version": "v3.21.0",
@@ -1433,6 +1496,457 @@
],
"time": "2025-09-22T17:04:34+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.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "21dc724a0583619cd1652f673303492272778051"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051",
+ "reference": "21dc724a0583619cd1652f673303492272778051",
+ "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.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.8.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": "2025-08-23T21:21:41+00:00"
+ },
+ {
+ "name": "itk-dev/openid-connect",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/itk-dev/openid-connect.git",
+ "reference": "8f3b8c0cc4abc7e91c1ee0f3e978deee6806da6d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/itk-dev/openid-connect/zipball/8f3b8c0cc4abc7e91c1ee0f3e978deee6806da6d",
+ "reference": "8f3b8c0cc4abc7e91c1ee0f3e978deee6806da6d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-openssl": "*",
+ "firebase/php-jwt": "^6.8",
+ "league/oauth2-client": "^2.6",
+ "php": "^8.3",
+ "psr/cache": "^2.0 || ^3.0",
+ "psr/http-client": "^1.0",
+ "robrichards/xmlseclibs": "^3.1"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "escapestudios/symfony2-coding-standard": "^3.12",
+ "mockery/mockery": "^1.4",
+ "phpstan/phpstan": "^2.1",
+ "phpunit/php-code-coverage": "^11.0",
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ItkDev\\OpenIdConnect\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jeppe Kuhlmann Andersen",
+ "email": "jekua@aarhus.dk"
+ },
+ {
+ "name": "Ture Gjørup",
+ "email": "tug@aarhus.dk"
+ },
+ {
+ "name": "Lars Steen Risom",
+ "email": "lats@aarhus.dk"
+ }
+ ],
+ "description": "OpenID connect configuration package",
+ "support": {
+ "issues": "https://github.com/itk-dev/openid-connect/issues",
+ "source": "https://github.com/itk-dev/openid-connect/tree/4.0.1"
+ },
+ "time": "2025-01-13T09:01:13+00:00"
+ },
+ {
+ "name": "itk-dev/openid-connect-bundle",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/itk-dev/openid-connect-bundle.git",
+ "reference": "be2dff91f114ba26926b5d59e9db3632cf809e7d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/itk-dev/openid-connect-bundle/zipball/be2dff91f114ba26926b5d59e9db3632cf809e7d",
+ "reference": "be2dff91f114ba26926b5d59e9db3632cf809e7d",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/orm": "^2.8|^3.0",
+ "ext-json": "*",
+ "ext-openssl": "*",
+ "itk-dev/openid-connect": "^4.0",
+ "php": "^8.3",
+ "symfony/cache": "^6.4|^7.0",
+ "symfony/framework-bundle": "^6.4.13|^7.0",
+ "symfony/security-bundle": "^6.4.13|^7.0",
+ "symfony/uid": "^6.4|^7.0",
+ "symfony/yaml": "^6.4|^7.0"
+ },
+ "require-dev": {
+ "ergebnis/composer-normalize": "^2.28",
+ "escapestudios/symfony2-coding-standard": "^3.12",
+ "friendsofphp/php-cs-fixer": "^3.11",
+ "kubawerlos/php-cs-fixer-custom-fixers": "^3.11",
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^11.0",
+ "rector/rector": "^2.0",
+ "symfony/runtime": "^6.4.13|^7.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "ItkDev\\OpenIdConnectBundle\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jeppe Kuhlmann Andersen",
+ "email": "jekua@aarhus.dk"
+ },
+ {
+ "name": "Ture Gjørup",
+ "email": "tug@aarhus.dk"
+ }
+ ],
+ "description": "Symfony bundle for openid-connect",
+ "support": {
+ "issues": "https://github.com/itk-dev/openid-connect-bundle/issues",
+ "source": "https://github.com/itk-dev/openid-connect-bundle/tree/4.0.1"
+ },
+ "time": "2025-01-16T21:14:01+00:00"
+ },
{
"name": "lcobucci/jwt",
"version": "5.5.0",
@@ -1506,6 +2020,71 @@
],
"time": "2025-01-26T21:29:45+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": "league/uri",
"version": "7.5.1",
@@ -1962,6 +2541,58 @@
},
"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",
@@ -2176,6 +2807,92 @@
},
"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": "robrichards/xmlseclibs",
+ "version": "3.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/robrichards/xmlseclibs.git",
+ "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/2bdfd742624d739dfadbd415f00181b4a77aaf07",
+ "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07",
+ "shasum": ""
+ },
+ "require": {
+ "ext-openssl": "*",
+ "php": ">= 5.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "RobRichards\\XMLSecLibs\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "A PHP library for XML Security",
+ "homepage": "https://github.com/robrichards/xmlseclibs",
+ "keywords": [
+ "security",
+ "signature",
+ "xml",
+ "xmldsig"
+ ],
+ "support": {
+ "issues": "https://github.com/robrichards/xmlseclibs/issues",
+ "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.3"
+ },
+ "time": "2024-11-20T21:13:56+00:00"
+ },
{
"name": "runtime/frankenphp-symfony",
"version": "0.2.0",
diff --git a/config/bundles.php b/config/bundles.php
index 900b6d0..155e49b 100644
--- a/config/bundles.php
+++ b/config/bundles.php
@@ -19,4 +19,5 @@
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
Symfonycasts\TailwindBundle\SymfonycastsTailwindBundle::class => ['all' => true],
Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],
+ ItkDev\OpenIdConnectBundle\ItkDevOpenIdConnectBundle::class => ['all' => true],
];
diff --git a/config/packages/itkdev_openid_connect.yaml b/config/packages/itkdev_openid_connect.yaml
new file mode 100644
index 0000000..f995883
--- /dev/null
+++ b/config/packages/itkdev_openid_connect.yaml
@@ -0,0 +1,20 @@
+itkdev_openid_connect:
+ cache_options:
+ cache_pool: 'cache.app' # Cache item pool for caching discovery document and CLI login tokens
+ cli_login_options:
+ route: '%env(string:OIDC_CLI_LOGIN_ROUTE)%' # Redirect route for CLI login
+ user_provider: ~ #
+ openid_providers:
+ admin:
+ options:
+ metadata_url: '%env(string:ADMIN_OIDC_METADATA_URL)%'
+ client_id: '%env(string:ADMIN_OIDC_CLIENT_ID)%'
+ client_secret: '%env(string:ADMIN_OIDC_CLIENT_SECRET)%'
+ # Specify redirect URI
+ redirect_uri: '%env(string:ADMIN_OIDC_REDIRECT_URI)%'
+ # Optional: Specify leeway (seconds) to account for clock skew between provider and hosting
+ # Defaults to 10
+ leeway: '%env(int:ADMIN_OIDC_LEEWAY)%'
+ # Optional: Allow (non-secure) http requests (used for mocking a IdP). NOT RECOMMENDED FOR PRODUCTION.
+ # Defaults to false
+ allow_http: '%env(bool:ADMIN_OIDC_ALLOW_HTTP)%'
diff --git a/config/packages/routing.yaml b/config/packages/routing.yaml
index 8166181..0f34f87 100644
--- a/config/packages/routing.yaml
+++ b/config/packages/routing.yaml
@@ -2,7 +2,7 @@ framework:
router:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
- #default_uri: http://localhost
+ default_uri: '%env(DEFAULT_URI)%'
when@prod:
framework:
diff --git a/config/packages/security.yaml b/config/packages/security.yaml
index ed0dc77..a72f273 100644
--- a/config/packages/security.yaml
+++ b/config/packages/security.yaml
@@ -1,29 +1,21 @@
security:
- # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
- password_hashers:
- Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
- # used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
+
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
- provider: app_user_provider
- form_login:
- login_path: app_login
- check_path: app_login
- enable_csrf: true
- logout:
- path: app_logout
- # where to redirect after logout
- # target: app_any_route
+ custom_authenticators:
+ - App\Security\OidcAuthenticator
+ - ItkDev\OpenIdConnectBundle\Security\CliLoginTokenAuthenticator
+ entry_point: App\Security\OidcAuthenticator
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
@@ -31,22 +23,16 @@ security:
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
+ role_hierarchy:
+ !php/enum App\Entity\UserRole::Admin->value:
+ - !php/enum App\Entity\UserRole::User->value
+ !php/enum App\Entity\UserRole::OverviewManager->value:
+ - !php/enum App\Entity\UserRole::Admin->value
+
# 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: ^/admin, roles: ROLE_ADMIN }
+ - { path: ^/admin, roles: !php/enum App\Entity\UserRole::Admin->value }
- { path: ^/login, roles: PUBLIC_ACCESS }
- - { path: ^/, roles: ROLE_USER }
-
-when@test:
- security:
- password_hashers:
- # By default, password hashers are resource intensive and take time. This is
- # important to generate secure password hashes. In tests however, secure hashes
- # are not important, waste resources and increase test times. The following
- # reduces the work factor to the lowest possible values.
- Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
- algorithm: auto
- cost: 4 # Lowest possible value for bcrypt
- time_cost: 3 # Lowest possible value for argon
- memory_cost: 10 # Lowest possible value for argon
+ - { path: '^/openidconnect/login(/.+)?$', role: PUBLIC_ACCESS }
+ - { path: ^/, roles: !php/enum App\Entity\UserRole::User->value }
diff --git a/config/routes/itkdev_openid_connect.yaml b/config/routes/itkdev_openid_connect.yaml
new file mode 100644
index 0000000..1c60325
--- /dev/null
+++ b/config/routes/itkdev_openid_connect.yaml
@@ -0,0 +1,3 @@
+itkdev_openid_connect:
+ resource: '@ItkDevOpenIdConnectBundle/src/Resources/config/routes.yaml'
+ prefix: '/openidconnect' # Prefix for bundle routes
diff --git a/config/services.yaml b/config/services.yaml
index 65b6a95..354770e 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -20,7 +20,7 @@ services:
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
- App\Controller\SecurityController:
+ App\Security\OidcAuthenticator:
arguments:
$options:
- page_title: '%site_title%'
+ role_map: '%env(json:ADMIN_OIDC_ROLE_MAP)%'
diff --git a/docker-compose.oidc.yml b/docker-compose.oidc.yml
new file mode 100644
index 0000000..ef39741
--- /dev/null
+++ b/docker-compose.oidc.yml
@@ -0,0 +1,123 @@
+services:
+ # https://github.com/Soluto/oidc-server-mock
+ oidc-idp:
+ image: ghcr.io/soluto/oidc-server-mock:latest
+ profiles:
+ - ${DOCKER_OIDC_DISABLE:-}
+ # Let this container be accessible both internally and externally on the same domain.
+ container_name: idp.${COMPOSE_DOMAIN}
+ networks:
+ - app
+ - frontend
+ ports:
+ - "80"
+ volumes:
+ - .:/tmp/config:ro
+ labels:
+ - "traefik.enable=true"
+ - "traefik.docker.network=frontend"
+ - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-idp.rule=Host(`idp.${COMPOSE_DOMAIN}`)"
+ - "traefik.http.services.${COMPOSE_PROJECT_NAME}-idp.loadbalancer.server.port=443"
+ - "traefik.http.services.${COMPOSE_PROJECT_NAME}-idp.loadbalancer.server.scheme=https"
+ - "traefik.http.routers.${COMPOSE_PROJECT_NAME}-idp.middlewares=redirect-to-https"
+ - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
+ environment:
+ # https://github.com/Soluto/oidc-server-mock?tab=readme-ov-file#https
+ ASPNETCORE_URLS: https://+:443;http://+:80
+ ASPNETCORE_Kestrel__Certificates__Default__Password: mock
+ ASPNETCORE_Kestrel__Certificates__Default__Path: /tmp/config/.docker/oidc-server-mock/cert/docker.pfx
+
+ ASPNETCORE_ENVIRONMENT: Development
+ SERVER_OPTIONS_INLINE: |
+ AccessTokenJwtType: JWT
+ Discovery:
+ ShowKeySet: true
+ Authentication:
+ CookieSameSiteMode: Lax
+ CheckSessionCookieSameSiteMode: Lax
+
+ LOGIN_OPTIONS_INLINE: |
+ {
+ "AllowRememberLogin": false
+ }
+
+ LOGOUT_OPTIONS_INLINE: |
+ {
+ "AutomaticRedirectAfterSignOut": true
+ }
+
+ CLIENTS_CONFIGURATION_INLINE: |
+ - ClientId: client-id
+ ClientSecrets: [client-secret]
+ Description: Mock IdP
+ AllowedGrantTypes:
+ # - client_credentials
+ # - implicit
+ - authorization_code
+ # https://github.com/Soluto/oidc-server-mock/issues/46#issuecomment-704963181
+ RequireClientSecret: false
+ AllowAccessTokensViaBrowser: true
+ # https://github.com/Soluto/oidc-server-mock/issues/26#issuecomment-705022941
+ AlwaysIncludeUserClaimsInIdToken: true
+ AllowedScopes:
+ - openid
+ - profile
+ - email
+ ClientClaimsPrefix: ''
+ RedirectUris:
+ - '*'
+ # https://github.com/Soluto/oidc-server-mock/issues/60
+ PostLogoutRedirectUris:
+ - '*'
+ # https://github.com/Soluto/oidc-server-mock/issues/46#issuecomment-704845375
+ RequirePkce: false
+
+ # Needed to set custom claim types in "profile"
+ # https://github.com/Soluto/oidc-server-mock/issues/123#issuecomment-1427129278
+ # https://github.com/Soluto/oidc-server-mock/blob/master/README.md#simple-configuration
+ # https://docs.docker.com/compose/compose-file/compose-file-v3/#environment
+ OVERRIDE_STANDARD_IDENTITY_RESOURCES: "true"
+ IDENTITY_RESOURCES_INLINE: |
+ # https://auth0.com/docs/get-started/apis/scopes/openid-connect-scopes#standard-claims
+ - Name: openid
+ ClaimTypes:
+ - sub
+ - Name: email
+ ClaimTypes:
+ - email
+ - Name: profile
+ ClaimTypes:
+ # Add your custom claims here
+ - name
+ - groups
+ - upn
+
+ USERS_CONFIGURATION_INLINE: |
+ - SubjectId: 1
+ Username: overview-manager
+ Password: overview-manager
+ Claims:
+ # Claims added here must be defined above in IDENTITY_RESOURCES_INLINE
+ - Type: email
+ Value: 'overview-manager@example.com'
+ ValueType: string
+ - Type: groups
+ Value: '["overview-manager"]'
+ ValueType: json
+ - Type: upn
+ Value: 'overview-manager@example.com'
+ ValueType: string
+
+ - SubjectId: 2
+ Username: user
+ Password: user
+ Claims:
+ - Type: email
+ Value: 'user@example.com'
+ ValueType: string
+ - Type: groups
+ Value: '["user"]'
+ ValueType: json
+ - Type: upn
+ Value: 'user@example.com'
+ ValueType: string
diff --git a/docker-compose.override.yml b/docker-compose.override.yml
index 80f9f65..55481c5 100644
--- a/docker-compose.override.yml
+++ b/docker-compose.override.yml
@@ -1,5 +1,6 @@
include:
- docker-compose.api.yml
+ - docker-compose.oidc.yml
services:
phpfpm:
diff --git a/fixtures/user.yaml b/fixtures/user.yaml
index 430772c..0f85688 100644
--- a/fixtures/user.yaml
+++ b/fixtures/user.yaml
@@ -1,12 +1,10 @@
App\Entity\User:
- admin:
- email: admin@example.com
- password: ''
+ overview-manager:
+ email: overview-manager@example.com
roles:
- - ROLE_ADMIN
+ - ROLE_OVERVIEW_MANAGER
user:
email: user@example.com
- password: ''
roles:
- ROLE_USER
diff --git a/migrations/Version20251003082431.php b/migrations/Version20251003082431.php
new file mode 100644
index 0000000..40aee35
--- /dev/null
+++ b/migrations/Version20251003082431.php
@@ -0,0 +1,36 @@
+addSql('CREATE TEMPORARY TABLE __temp__user AS SELECT id, email, roles, created_by, updated_by, created_at, updated_at FROM user');
+ $this->addSql('DROP TABLE user');
+ $this->addSql('CREATE TABLE user (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, email VARCHAR(180) NOT NULL, roles CLOB NOT NULL, created_by VARCHAR(255) DEFAULT NULL, updated_by VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL)');
+ $this->addSql('INSERT INTO user (id, email, roles, created_by, updated_by, created_at, updated_at) SELECT id, email, roles, created_by, updated_by, created_at, updated_at FROM __temp__user');
+ $this->addSql('DROP TABLE __temp__user');
+ $this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON user (email)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('ALTER TABLE user ADD COLUMN password VARCHAR(255) NOT NULL');
+ }
+}
diff --git a/src/Command/UserCreateCommand.php b/src/Command/UserCreateCommand.php
deleted file mode 100644
index 0f13ad0..0000000
--- a/src/Command/UserCreateCommand.php
+++ /dev/null
@@ -1,65 +0,0 @@
-addArgument('email', InputArgument::OPTIONAL, 'User email')
- ->addOption('password', null, InputOption::VALUE_REQUIRED, 'User password');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int
- {
- $io = new SymfonyStyle($input, $output);
- $email = $input->getArgument('email');
- while (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
- $email = $io->ask('Email');
- }
-
- $user = $this->userRepository->findOneBy(['email' => $email]);
- if (null !== $user) {
- throw new InvalidArgumentException(sprintf('User %s already exists.', $user->getUserIdentifier()));
- }
-
- $password = $input->getOption('password');
- while (empty(trim($password))) {
- $password = $io->ask('Password');
- }
-
- $user = new User();
- $user
- ->setEmail($email)
- ->setPassword($this->passwordHasher->hashPassword($user, $password));
- $this->userRepository->save($user, flush: true);
-
- $io->success(sprintf('User %s created.', $user->getUserIdentifier()));
-
- return Command::SUCCESS;
- }
-}
diff --git a/src/Command/UserRolesCommand.php b/src/Command/UserRolesCommand.php
deleted file mode 100644
index 4f2e7d3..0000000
--- a/src/Command/UserRolesCommand.php
+++ /dev/null
@@ -1,64 +0,0 @@
-addArgument('email', InputArgument::REQUIRED, 'User email')
- ->addOption('add', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Roles to add')
- ->addOption('remove', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Roles to removea');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int
- {
- $io = new SymfonyStyle($input, $output);
- $email = $input->getArgument('email');
-
- $user = $this->userRepository->findOneBy(['email' => $email]);
- if (null === $user) {
- throw new InvalidArgumentException(sprintf('Cannot find user %s.', $email));
- }
-
- $roles = $user->getRoles();
- $roles = array_merge($roles, $input->getOption('add'));
- $roles = array_diff($roles, $input->getOption('remove'));
-
- $invalidRoles = array_filter($roles, static fn (string $role) => null === UserRole::tryFrom($role));
- if (!empty($invalidRoles)) {
- throw new InvalidArgumentException(sprintf('Invalid roles: %s', implode(', ', $invalidRoles)));
- }
-
- $user->setRoles($roles);
- $this->userRepository->save($user, flush: true);
-
- $io->success(sprintf('Roles for ser %s: %s.', $user->getUserIdentifier(), implode(', ', $user->getRoles())));
-
- return Command::SUCCESS;
- }
-}
diff --git a/src/Controller/Admin/DashboardController.php b/src/Controller/Admin/DashboardController.php
index 4db9237..518d7b3 100644
--- a/src/Controller/Admin/DashboardController.php
+++ b/src/Controller/Admin/DashboardController.php
@@ -8,9 +8,11 @@
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminDashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
+use EasyCorp\Bundle\EasyAdminBundle\Config\UserMenu;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Security\Core\User\UserInterface;
use function Symfony\Component\Translation\t;
@@ -30,6 +32,14 @@ public function configureDashboard(): Dashboard
->setTitle($this->getParameter('site_title'));
}
+ public function configureUserMenu(UserInterface $user): UserMenu
+ {
+ return parent::configureUserMenu($user)
+ ->setMenuItems([
+ // Remove the logout link.
+ ]);
+ }
+
public function configureMenuItems(): iterable
{
yield MenuItem::linkToCrud(t('Group'), null, ProcessOverviewGroup::class);
diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php
index 6ac5354..38a1c71 100644
--- a/src/Controller/SecurityController.php
+++ b/src/Controller/SecurityController.php
@@ -5,65 +5,12 @@
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends AbstractController
{
- public function __construct(
- private readonly array $options,
- ) {
- }
-
#[Route(path: '/login', name: 'app_login')]
- public function login(AuthenticationUtils $authenticationUtils): Response
- {
- // https://symfony.com/bundles/EasyAdminBundle/current/dashboards.html#login-form-template
- $error = $authenticationUtils->getLastAuthenticationError();
- $lastUsername = $authenticationUtils->getLastUsername();
-
- return $this->render('@EasyAdmin/page/login.html.twig', [
- // parameters usually defined in Symfony login forms
- 'error' => $error,
- 'last_username' => $lastUsername,
-
- // the translation_domain to use (define this option only if you are
- // rendering the login template in a regular Symfony controller; when
- // rendering it from an EasyAdmin Dashboard this is automatically set to
- // the same domain as the rest of the Dashboard)
- 'translation_domain' => 'admin',
-
- // by default EasyAdmin displays a black square as its default favicon;
- // use this method to display a custom favicon: the given path is passed
- // "as is" to the Twig asset() function:
- //
- 'favicon_path' => '/favicon.svg',
-
- // the title visible above the login form (define this option only if you are
- // rendering the login template in a regular Symfony controller; when rendering
- // it from an EasyAdmin Dashboard this is automatically set as the Dashboard title)
- 'page_title' => $this->options['page_title'] ?? 'RPA Process Overview',
-
- // the string used to generate the CSRF token. If you don't define
- // this parameter, the login form won't include a CSRF token
- 'csrf_token_intention' => 'authenticate',
-
- // the URL users are redirected to after the login (default: '/admin')
- 'target_path' => $this->generateUrl('app_default'),
-
- // the label displayed for the username form field (the |trans filter is applied to it)
- 'username_label' => 'Email',
-
- // the label displayed for the password form field (the |trans filter is applied to it)
- 'password_label' => 'Password',
-
- // the label displayed for the Sign In form button (the |trans filter is applied to it)
- 'sign_in_label' => 'Log in',
- ]);
- }
-
- #[Route(path: '/logout', name: 'app_logout')]
- public function logout(): void
+ public function login(): Response
{
- throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
+ return $this->redirectToRoute('itkdev_openid_connect_login', ['providerKey' => 'admin']);
}
}
diff --git a/src/DataFixtures/Faker/Provider/Provider.php b/src/DataFixtures/Faker/Provider/Provider.php
deleted file mode 100644
index ef2cce9..0000000
--- a/src/DataFixtures/Faker/Provider/Provider.php
+++ /dev/null
@@ -1,35 +0,0 @@
-'
- *
- * @return string
- */
- public function password(PasswordAuthenticatedUserInterface $user, string $plaintextPassword)
- {
- return $this->passwordHasher->hashPassword(
- $user,
- $plaintextPassword
- );
- }
-}
diff --git a/src/Entity/User.php b/src/Entity/User.php
index ce0e8b3..991eef8 100644
--- a/src/Entity/User.php
+++ b/src/Entity/User.php
@@ -6,12 +6,11 @@
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Blameable\Traits\BlameableEntity;
use Gedmo\Timestampable\Traits\TimestampableEntity;
-use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
-class User implements UserInterface, PasswordAuthenticatedUserInterface
+class User implements UserInterface
{
use BlameableEntity;
use TimestampableEntity;
@@ -30,12 +29,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column]
private array $roles = [];
- /**
- * @var string The hashed password
- */
- #[ORM\Column]
- private ?string $password = null;
-
public function getId(): ?int
{
return $this->id;
@@ -85,30 +78,9 @@ public function setRoles(array $roles): static
return $this;
}
- /**
- * @see PasswordAuthenticatedUserInterface
- */
- public function getPassword(): ?string
- {
- return $this->password;
- }
-
- public function setPassword(string $password): static
- {
- $this->password = $password;
-
- return $this;
- }
-
- /**
- * Ensure the session doesn't contain actual password hashes by CRC32C-hashing them, as supported since Symfony 7.3.
- */
public function __serialize(): array
{
- $data = (array) $this;
- $data["\0".self::class."\0password"] = hash('crc32c', $this->password);
-
- return $data;
+ return (array) $this;
}
#[\Deprecated]
diff --git a/src/Entity/UserRole.php b/src/Entity/UserRole.php
index 6560f03..fa9a1f7 100644
--- a/src/Entity/UserRole.php
+++ b/src/Entity/UserRole.php
@@ -4,6 +4,7 @@
enum UserRole: string
{
- case ROLE_USER = 'ROLE_USER';
- case ROLE_ADMIN = 'ROLE_ADMIN';
+ case User = 'ROLE_USER';
+ case Admin = 'ROLE_ADMIN';
+ case OverviewManager = 'ROLE_OVERVIEW_MANAGER';
}
diff --git a/src/Security/OidcAuthenticator.php b/src/Security/OidcAuthenticator.php
new file mode 100644
index 0000000..1f3dd6f
--- /dev/null
+++ b/src/Security/OidcAuthenticator.php
@@ -0,0 +1,75 @@
+validateClaims($request);
+
+ // Extract properties from claims
+ $email = $claims['email'] ?? $claims['upn'] ?? null;
+ $roles = $claims['roles'] ?? $claims['groups'] ?? [];
+
+ // Check if user already exists already or create a new one.
+ $user = $this->userRepository->findOneBy(['email' => $email]) ?? new User();
+
+ if (is_array($roles)) {
+ $map = (array) ($this->options['role_map'] ?? null);
+ $userRoles = array_map(static fn (string $role) => (array) ($map[$role] ?? null), $roles);
+ // Flatten and filter out invalid roles.
+ $userRoles = array_filter(array_merge(...$userRoles), static fn (string $role) => null !== UserRole::tryFrom($role));
+ $user->setRoles($userRoles);
+ }
+
+ $user->setEmail($email);
+ $this->userRepository->save($user, flush: true);
+
+ return new SelfValidatingPassport(new UserBadge($user->getUserIdentifier()));
+ } catch (ItkOpenIdConnectException|InvalidProviderException $exception) {
+ throw new CustomUserMessageAuthenticationException($exception->getMessage());
+ }
+ }
+
+ public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
+ {
+ return new RedirectResponse($this->router->generate('app_default'));
+ }
+
+ public function start(Request $request, ?AuthenticationException $authException = null): Response
+ {
+ return new RedirectResponse($this->router->generate('itkdev_openid_connect_login', [
+ 'providerKey' => 'admin',
+ ]));
+ }
+}
diff --git a/symfony.lock b/symfony.lock
index 46d475c..eb6fa4d 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -72,6 +72,9 @@
"fixtures/.gitignore"
]
},
+ "itk-dev/openid-connect-bundle": {
+ "version": "4.0.1"
+ },
"nelmio/alice": {
"version": "3.14",
"recipe": {
diff --git a/templates/default/index.html.twig b/templates/default/index.html.twig
index 6e13814..5028f3a 100644
--- a/templates/default/index.html.twig
+++ b/templates/default/index.html.twig
@@ -1,8 +1,9 @@
{% extends 'base.html.twig' %}
{% block content %}
+ {{ dump(enum('App\\Entity\\UserRole').Admin) }}
- {% if is_granted('ROLE_ADMIN') %}
+ {% if is_granted(enum('App\\Entity\\UserRole').Admin.value) %}
-
{{ 'Admin'|trans }}