Add SSO E-Cosplay (Keycloak OIDC) and dynamic navbar active state
- Install knpuniversity/oauth2-client-bundle and stevenmaguire/oauth2-keycloak - Register KnpUOAuth2ClientBundle in bundles.php - Configure Keycloak OIDC client (realm e-cosplay, auth.esy-web.dev) - Add keycloakId field to User entity with migration - Create KeycloakAuthenticator with group-to-role mapping (/superadmin -> ROLE_ROOT) - Create OAuthController with SSO routes (/connection/sso/login, logout, check) - Add custom_authenticator to security firewall with form_login entry point - Add auth.esy-web.dev to nelmio external_redirects whitelist and CSP form-action - Add SSO button and error flash messages to login page - Make navbar active state dynamic based on current route (desktop + mobile) - Add Keycloak env vars to .env, .env.local, and ansible/env.local.j2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
7
.env
7
.env
@@ -47,6 +47,13 @@ STRIPE_WEBHOOK_SECRET=
|
|||||||
STRIPE_MODE=test
|
STRIPE_MODE=test
|
||||||
SMIME_PASSPHRASE='KLreLnyR07x5h#3$AC'
|
SMIME_PASSPHRASE='KLreLnyR07x5h#3$AC'
|
||||||
|
|
||||||
|
###> SSO E-Cosplay (Keycloak OIDC) ###
|
||||||
|
OAUTH_KEYCLOAK_CLIENT_ID=e-ticket
|
||||||
|
OAUTH_KEYCLOAK_CLIENT_SECRET=changeme
|
||||||
|
OAUTH_KEYCLOAK_URL=https://auth.esy-web.dev
|
||||||
|
OAUTH_KEYCLOAK_REALM=e-cosplay
|
||||||
|
###< SSO E-Cosplay (Keycloak OIDC) ###
|
||||||
|
|
||||||
###> s3/minio ###
|
###> s3/minio ###
|
||||||
S3_ENDPOINT=http://minio:9000
|
S3_ENDPOINT=http://minio:9000
|
||||||
S3_ACCESS_KEY=e-ticket
|
S3_ACCESS_KEY=e-ticket
|
||||||
|
|||||||
@@ -19,3 +19,7 @@ STRIPE_MODE=live
|
|||||||
SMIME_PASSPHRASE='{{ smime_passphrase }}'
|
SMIME_PASSPHRASE='{{ smime_passphrase }}'
|
||||||
MEILISEARCH_URL=http://meilisearch:7700
|
MEILISEARCH_URL=http://meilisearch:7700
|
||||||
MEILISEARCH_API_KEY={{ meilisearch_api_key }}
|
MEILISEARCH_API_KEY={{ meilisearch_api_key }}
|
||||||
|
OAUTH_KEYCLOAK_CLIENT_ID=e-ticket
|
||||||
|
OAUTH_KEYCLOAK_CLIENT_SECRET=1oLwbhJDNVmGH8CES1OdQtzR7dECOlII
|
||||||
|
OAUTH_KEYCLOAK_URL=https://auth.esy-web.dev
|
||||||
|
OAUTH_KEYCLOAK_REALM=e-cosplay
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"doctrine/orm": "^3.6",
|
"doctrine/orm": "^3.6",
|
||||||
"dompdf/dompdf": "*",
|
"dompdf/dompdf": "*",
|
||||||
"endroid/qr-code-bundle": "*",
|
"endroid/qr-code-bundle": "*",
|
||||||
|
"knpuniversity/oauth2-client-bundle": "^2.20",
|
||||||
"league/flysystem-aws-s3-v3": "^3.32",
|
"league/flysystem-aws-s3-v3": "^3.32",
|
||||||
"league/flysystem-bundle": "^3.6",
|
"league/flysystem-bundle": "^3.6",
|
||||||
"liip/imagine-bundle": "^2.17",
|
"liip/imagine-bundle": "^2.17",
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
"nelmio/security-bundle": "^3.9",
|
"nelmio/security-bundle": "^3.9",
|
||||||
"phpdocumentor/reflection-docblock": "^6.0",
|
"phpdocumentor/reflection-docblock": "^6.0",
|
||||||
"phpstan/phpdoc-parser": "^2.3",
|
"phpstan/phpdoc-parser": "^2.3",
|
||||||
|
"stevenmaguire/oauth2-keycloak": "^6.1",
|
||||||
"stripe/stripe-php": "*",
|
"stripe/stripe-php": "*",
|
||||||
"symfony/asset": "8.0.*",
|
"symfony/asset": "8.0.*",
|
||||||
"symfony/console": "8.0.*",
|
"symfony/console": "8.0.*",
|
||||||
|
|||||||
252
composer.lock
generated
252
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "a708b901e0ade836caa13855beebd9d1",
|
"content-hash": "ecb55cf346fc28e16c4caec521a016e2",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aws/aws-crt-php",
|
"name": "aws/aws-crt-php",
|
||||||
@@ -1872,6 +1872,69 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-12-01T22:03:15+00:00"
|
"time": "2025-12-01T22:03:15+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "firebase/php-jwt",
|
||||||
|
"version": "v7.0.3",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/firebase/php-jwt.git",
|
||||||
|
"reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/28aa0694bcfdfa5e2959c394d5a1ee7a5083629e",
|
||||||
|
"reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e",
|
||||||
|
"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/v7.0.3"
|
||||||
|
},
|
||||||
|
"time": "2026-02-25T22:16:40+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "guzzlehttp/guzzle",
|
"name": "guzzlehttp/guzzle",
|
||||||
"version": "7.10.0",
|
"version": "7.10.0",
|
||||||
@@ -2324,6 +2387,66 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-11-30T20:12:26+00:00"
|
"time": "2025-11-30T20:12:26+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/flysystem",
|
"name": "league/flysystem",
|
||||||
"version": "3.32.0",
|
"version": "3.32.0",
|
||||||
@@ -2637,6 +2760,71 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-09-21T08:32:55+00:00"
|
"time": "2024-09-21T08:32:55+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": "liip/imagine-bundle",
|
"name": "liip/imagine-bundle",
|
||||||
"version": "2.17.1",
|
"version": "2.17.1",
|
||||||
@@ -4036,6 +4224,68 @@
|
|||||||
},
|
},
|
||||||
"time": "2026-03-03T17:31:43+00:00"
|
"time": "2026-03-03T17:31:43+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "stevenmaguire/oauth2-keycloak",
|
||||||
|
"version": "6.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/stevenmaguire/oauth2-keycloak.git",
|
||||||
|
"reference": "459cc58576d37f5823de2a677a7b17667b85ba7f"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/stevenmaguire/oauth2-keycloak/zipball/459cc58576d37f5823de2a677a7b17667b85ba7f",
|
||||||
|
"reference": "459cc58576d37f5823de2a677a7b17667b85ba7f",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"firebase/php-jwt": "^7.0",
|
||||||
|
"league/oauth2-client": "^2.8",
|
||||||
|
"php": "^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"phpstan/phpstan": "^1.12",
|
||||||
|
"phpunit/phpunit": "~9.6.4",
|
||||||
|
"squizlabs/php_codesniffer": "~3.7.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Stevenmaguire\\OAuth2\\Client\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Steven Maguire",
|
||||||
|
"email": "stevenmaguire@gmail.com",
|
||||||
|
"homepage": "https://github.com/stevenmaguire"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Keycloak OAuth 2.0 Client Provider for The PHP League OAuth2-Client",
|
||||||
|
"keywords": [
|
||||||
|
"authorisation",
|
||||||
|
"authorization",
|
||||||
|
"client",
|
||||||
|
"keycloak",
|
||||||
|
"oauth",
|
||||||
|
"oauth2"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/stevenmaguire/oauth2-keycloak/issues",
|
||||||
|
"source": "https://github.com/stevenmaguire/oauth2-keycloak/tree/6.1.0"
|
||||||
|
},
|
||||||
|
"time": "2026-03-03T11:50:29+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "stripe/stripe-php",
|
"name": "stripe/stripe-php",
|
||||||
"version": "v19.4.1",
|
"version": "v19.4.1",
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ return [
|
|||||||
Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true],
|
Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true],
|
||||||
League\FlysystemBundle\FlysystemBundle::class => ['all' => true],
|
League\FlysystemBundle\FlysystemBundle::class => ['all' => true],
|
||||||
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
|
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
|
||||||
|
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
12
config/packages/knpu_oauth2_client.yaml
Normal file
12
config/packages/knpu_oauth2_client.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
knpu_oauth2_client:
|
||||||
|
clients:
|
||||||
|
keycloak:
|
||||||
|
type: generic
|
||||||
|
provider_class: Stevenmaguire\OAuth2\Client\Provider\Keycloak
|
||||||
|
client_id: '%env(OAUTH_KEYCLOAK_CLIENT_ID)%'
|
||||||
|
client_secret: '%env(OAUTH_KEYCLOAK_CLIENT_SECRET)%'
|
||||||
|
redirect_route: app_oauth_keycloak_check
|
||||||
|
redirect_params: {}
|
||||||
|
provider_options:
|
||||||
|
authServerUrl: '%env(OAUTH_KEYCLOAK_URL)%'
|
||||||
|
realm: '%env(OAUTH_KEYCLOAK_REALM)%'
|
||||||
@@ -51,6 +51,9 @@ nelmio_security:
|
|||||||
- 'https://fonts.gstatic.com'
|
- 'https://fonts.gstatic.com'
|
||||||
object-src:
|
object-src:
|
||||||
- 'none'
|
- 'none'
|
||||||
|
form-action:
|
||||||
|
- 'self'
|
||||||
|
- 'https://auth.esy-web.dev'
|
||||||
block-all-mixed-content: true
|
block-all-mixed-content: true
|
||||||
|
|
||||||
permissions_policy:
|
permissions_policy:
|
||||||
@@ -71,3 +74,4 @@ nelmio_security:
|
|||||||
- stripe.com
|
- stripe.com
|
||||||
- checkout.stripe.com
|
- checkout.stripe.com
|
||||||
- hooks.stripe.com
|
- hooks.stripe.com
|
||||||
|
- auth.esy-web.dev
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ security:
|
|||||||
login_path: app_login
|
login_path: app_login
|
||||||
check_path: app_login
|
check_path: app_login
|
||||||
enable_csrf: true
|
enable_csrf: true
|
||||||
|
custom_authenticators:
|
||||||
|
- App\Security\KeycloakAuthenticator
|
||||||
|
entry_point: form_login
|
||||||
logout:
|
logout:
|
||||||
path: app_logout
|
path: app_logout
|
||||||
target: app_home
|
target: app_home
|
||||||
|
|||||||
37
migrations/Version20260319093607.php
Normal file
37
migrations/Version20260319093607.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?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 Version20260319093607 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 messenger_log (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, message_class VARCHAR(255) NOT NULL, message_body TEXT DEFAULT NULL, status VARCHAR(20) NOT NULL, error_message TEXT DEFAULT NULL, stack_trace TEXT DEFAULT NULL, transport_name VARCHAR(255) DEFAULT NULL, retry_count INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, failed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY (id))');
|
||||||
|
$this->addSql('CREATE INDEX idx_messenger_log_status ON messenger_log (status)');
|
||||||
|
$this->addSql('CREATE INDEX idx_messenger_log_created_at ON messenger_log (created_at)');
|
||||||
|
$this->addSql('ALTER TABLE "user" ADD keycloak_id VARCHAR(255) DEFAULT NULL');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649491914B1 ON "user" (keycloak_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('DROP TABLE messenger_log');
|
||||||
|
$this->addSql('DROP INDEX UNIQ_8D93D649491914B1');
|
||||||
|
$this->addSql('ALTER TABLE "user" DROP keycloak_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/Controller/OAuthController.php
Normal file
29
src/Controller/OAuthController.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
class OAuthController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/connection/sso/login', name: 'app_oauth_keycloak')]
|
||||||
|
public function connect(ClientRegistry $clientRegistry): RedirectResponse
|
||||||
|
{
|
||||||
|
return $clientRegistry->getClient('keycloak')->redirect(['openid', 'email', 'profile']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/connection/sso/logout', name: 'app_oauth_keycloak_logout')]
|
||||||
|
public function logout(): RedirectResponse
|
||||||
|
{
|
||||||
|
return $this->redirectToRoute('app_home');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/connection/sso/check', name: 'app_oauth_keycloak_check')]
|
||||||
|
public function check(): void
|
||||||
|
{
|
||||||
|
// Handled by KeycloakAuthenticator
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private ?string $password = null;
|
private ?string $password = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, unique: true, nullable: true)]
|
||||||
|
private ?string $keycloakId = null;
|
||||||
|
|
||||||
#[ORM\Column]
|
#[ORM\Column]
|
||||||
private \DateTimeImmutable $createdAt;
|
private \DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
@@ -122,6 +125,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
return $this->createdAt;
|
return $this->createdAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getKeycloakId(): ?string
|
||||||
|
{
|
||||||
|
return $this->keycloakId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setKeycloakId(?string $keycloakId): static
|
||||||
|
{
|
||||||
|
$this->keycloakId = $keycloakId;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function eraseCredentials(): void
|
public function eraseCredentials(): void
|
||||||
{
|
{
|
||||||
// Required by UserInterface — no temporary credentials to clear
|
// Required by UserInterface — no temporary credentials to clear
|
||||||
|
|||||||
117
src/Security/KeycloakAuthenticator.php
Normal file
117
src/Security/KeycloakAuthenticator.php
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Security;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
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;
|
||||||
|
|
||||||
|
class KeycloakAuthenticator extends OAuth2Authenticator
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ClientRegistry $clientRegistry,
|
||||||
|
private EntityManagerInterface $em,
|
||||||
|
private RouterInterface $router,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(Request $request): ?bool
|
||||||
|
{
|
||||||
|
return 'app_oauth_keycloak_check' === $request->attributes->get('_route');
|
||||||
|
}
|
||||||
|
|
||||||
|
private const GROUP_ROLE_MAP = [
|
||||||
|
'/superadmin' => 'ROLE_ROOT',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function authenticate(Request $request): Passport
|
||||||
|
{
|
||||||
|
$client = $this->clientRegistry->getClient('keycloak');
|
||||||
|
$accessToken = $this->fetchAccessToken($client);
|
||||||
|
|
||||||
|
return new SelfValidatingPassport(
|
||||||
|
new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) {
|
||||||
|
$keycloakUser = $client->fetchUserFromToken($accessToken);
|
||||||
|
$keycloakId = $keycloakUser->getId();
|
||||||
|
$email = $keycloakUser->getEmail();
|
||||||
|
$data = $keycloakUser->toArray();
|
||||||
|
$roles = $this->mapGroupsToRoles($data['groups'] ?? []);
|
||||||
|
|
||||||
|
$user = $this->em->getRepository(User::class)->findOneBy(['keycloakId' => $keycloakId]);
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
$user->setEmail($email);
|
||||||
|
$user->setFirstName($data['given_name'] ?? $user->getFirstName());
|
||||||
|
$user->setLastName($data['family_name'] ?? $user->getLastName());
|
||||||
|
$user->setRoles($roles);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingUser = $this->em->getRepository(User::class)->findOneBy(['email' => $email]);
|
||||||
|
|
||||||
|
if ($existingUser) {
|
||||||
|
$existingUser->setKeycloakId($keycloakId);
|
||||||
|
$existingUser->setRoles($roles);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return $existingUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
$newUser = new User();
|
||||||
|
$newUser->setKeycloakId($keycloakId);
|
||||||
|
$newUser->setEmail($email);
|
||||||
|
$newUser->setFirstName($data['given_name'] ?? '');
|
||||||
|
$newUser->setLastName($data['family_name'] ?? '');
|
||||||
|
$newUser->setPassword('');
|
||||||
|
$newUser->setRoles($roles);
|
||||||
|
|
||||||
|
$this->em->persist($newUser);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return $newUser;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $groups
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function mapGroupsToRoles(array $groups): array
|
||||||
|
{
|
||||||
|
$roles = [];
|
||||||
|
|
||||||
|
foreach ($groups as $group) {
|
||||||
|
if (isset(self::GROUP_ROLE_MAP[$group])) {
|
||||||
|
$roles[] = self::GROUP_ROLE_MAP[$group];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||||
|
{
|
||||||
|
return new RedirectResponse($this->router->generate('app_account'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||||
|
{
|
||||||
|
$request->getSession()->getFlashBag()->add('error', 'Echec de la connexion SSO E-Cosplay.');
|
||||||
|
|
||||||
|
return new RedirectResponse($this->router->generate('app_login'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,15 @@
|
|||||||
".php-cs-fixer.dist.php"
|
".php-cs-fixer.dist.php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"knpuniversity/oauth2-client-bundle": {
|
||||||
|
"version": "2.20",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes-contrib",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.20",
|
||||||
|
"ref": "1ff300d8c030f55c99219cc55050b97a695af3f6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"league/flysystem-bundle": {
|
"league/flysystem-bundle": {
|
||||||
"version": "3.6",
|
"version": "3.6",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|||||||
@@ -82,9 +82,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hidden lg:flex items-center space-x-1">
|
<div class="hidden lg:flex items-center space-x-1">
|
||||||
<a href="{{ path('app_home') }}" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all bg-yellow-400 border-2 border-gray-900 shadow-[2px_2px_0px_rgba(0,0,0,1)]"><span itemprop="name">Accueil</span></a>
|
{% set current_route = app.request.attributes.get('_route') %}
|
||||||
|
<a href="{{ path('app_home') }}" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all {{ current_route == 'app_home' ? 'bg-yellow-400 border-2 border-gray-900 shadow-[2px_2px_0px_rgba(0,0,0,1)]' : 'hover:text-indigo-600' }}"><span itemprop="name">Accueil</span></a>
|
||||||
<a href="#" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all hover:text-indigo-600"><span itemprop="name">Evenements</span></a>
|
<a href="#" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all hover:text-indigo-600"><span itemprop="name">Evenements</span></a>
|
||||||
<a href="{{ path('app_contact') }}" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all hover:text-indigo-600"><span itemprop="name">Contact</span></a>
|
<a href="{{ path('app_contact') }}" itemprop="url" class="px-3 py-2 text-xs font-black uppercase tracking-widest transition-all {{ current_route == 'app_contact' ? 'bg-yellow-400 border-2 border-gray-900 shadow-[2px_2px_0px_rgba(0,0,0,1)]' : 'hover:text-indigo-600' }}"><span itemprop="name">Contact</span></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center space-x-4 border-l-4 border-gray-900 pl-6 h-full">
|
<div class="flex items-center space-x-4 border-l-4 border-gray-900 pl-6 h-full">
|
||||||
@@ -110,9 +111,9 @@
|
|||||||
|
|
||||||
<div id="mobile-menu" class="hidden lg:hidden border-t-4 border-gray-900 bg-white" role="menu">
|
<div id="mobile-menu" class="hidden lg:hidden border-t-4 border-gray-900 bg-white" role="menu">
|
||||||
<div class="p-4 space-y-2 uppercase font-black italic">
|
<div class="p-4 space-y-2 uppercase font-black italic">
|
||||||
<a href="{{ path('app_home') }}" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Accueil</a>
|
<a href="{{ path('app_home') }}" class="block p-3 border-2 {{ current_route == 'app_home' ? 'border-gray-900 bg-yellow-400' : 'border-transparent hover:border-gray-900 hover:bg-gray-50' }}" role="menuitem">Accueil</a>
|
||||||
<a href="#" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Evenements</a>
|
<a href="#" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Evenements</a>
|
||||||
<a href="{{ path('app_contact') }}" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Contact</a>
|
<a href="{{ path('app_contact') }}" class="block p-3 border-2 {{ current_route == 'app_contact' ? 'border-gray-900 bg-yellow-400' : 'border-transparent hover:border-gray-900 hover:bg-gray-50' }}" role="menuitem">Contact</a>
|
||||||
{% if app.user %}
|
{% if app.user %}
|
||||||
<a href="{{ path('app_account') }}" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Mon espace</a>
|
<a href="{{ path('app_account') }}" class="block p-3 border-2 border-transparent hover:border-gray-900 hover:bg-gray-50" role="menuitem">Mon espace</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -1,7 +1,68 @@
|
|||||||
{% extends 'base.html.twig' %}
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
{% block title %}Connexion - E-Ticket{% endblock %}
|
{% block title %}Connexion - E-Ticket{% endblock %}
|
||||||
|
{% block description %}Connectez-vous a votre compte E-Ticket pour gerer vos evenements et billets{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
<div style="max-width:28rem;margin:0 auto;padding:3rem 1rem;">
|
||||||
|
<h1 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;margin-bottom:0.5rem;">Connexion</h1>
|
||||||
|
<p class="font-bold text-gray-600 italic" style="margin-bottom:2rem;">Accedez a votre espace.</p>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div style="border:4px solid #111827;padding:1rem 1.5rem;margin-bottom:2rem;background:#fee2e2;box-shadow:4px 4px 0 rgba(0,0,0,1);">
|
||||||
|
<p class="font-black text-sm">{{ error.messageKey|trans(error.messageData, 'security') }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for message in app.flashes('error') %}
|
||||||
|
<div style="border:4px solid #111827;padding:1rem 1.5rem;margin-bottom:2rem;background:#fee2e2;box-shadow:4px 4px 0 rgba(0,0,0,1);">
|
||||||
|
<p class="font-black text-sm">{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<form method="post" action="{{ path('app_login') }}" style="display:flex;flex-direction:column;gap:1.5rem;">
|
||||||
|
<div>
|
||||||
|
<label for="login_email" class="text-xs font-black uppercase tracking-widest" style="display:block;margin-bottom:0.5rem;">Email</label>
|
||||||
|
<input type="email" id="login_email" name="_username" required autofocus
|
||||||
|
value="{{ last_username }}"
|
||||||
|
style="width:100%;padding:0.75rem 1rem;border:3px solid #111827;font-weight:700;outline:none;"
|
||||||
|
class="focus:border-indigo-600"
|
||||||
|
placeholder="jean.dupont@exemple.fr">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="login_password" class="text-xs font-black uppercase tracking-widest" style="display:block;margin-bottom:0.5rem;">Mot de passe</label>
|
||||||
|
<input type="password" id="login_password" name="_password" required
|
||||||
|
style="width:100%;padding:0.75rem 1rem;border:3px solid #111827;font-weight:700;outline:none;"
|
||||||
|
class="focus:border-indigo-600"
|
||||||
|
placeholder="••••••••">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit"
|
||||||
|
style="width:100%;padding:0.75rem 2rem;border:3px solid #111827;box-shadow:4px 4px 0 rgba(0,0,0,1);cursor:pointer;"
|
||||||
|
class="bg-yellow-400 font-black uppercase text-sm tracking-widest hover:bg-indigo-600 hover:text-white transition-all">
|
||||||
|
Se connecter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style="display:flex;align-items:center;gap:1rem;margin-top:2rem;">
|
||||||
|
<div style="flex:1;height:3px;background:#111827;"></div>
|
||||||
|
<span class="text-xs font-black uppercase tracking-widest text-gray-500">ou</span>
|
||||||
|
<div style="flex:1;height:3px;background:#111827;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ path('app_oauth_keycloak') }}"
|
||||||
|
style="display:block;width:100%;padding:0.75rem 2rem;border:3px solid #111827;box-shadow:4px 4px 0 rgba(0,0,0,1);text-align:center;text-decoration:none;margin-top:1.5rem;"
|
||||||
|
class="bg-gray-900 text-white font-black uppercase text-sm tracking-widest hover:bg-indigo-600 transition-all">
|
||||||
|
Se connecter avec E-Cosplay
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="margin-top:2rem;text-align:center;">
|
||||||
|
<p class="text-sm font-bold text-gray-600">Pas encore de compte ? <a href="{{ path('app_register') }}" class="text-indigo-600 hover:underline font-black">Inscription</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user