From b01ea8b2abaa29f580e0aa0807963571301ece43 Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Sun, 11 Jan 2026 14:20:16 +0100 Subject: [PATCH] =?UTF-8?q?```=20=E2=9C=A8=20feat(login):=20Ajoute=20l'aut?= =?UTF-8?q?hentification=20SSO=20via=20Keycloak=20et=20Discord?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cette modification implémente l'authentification unique (SSO) via Keycloak et Discord, permettant aux utilisateurs de se connecter facilement. Ajoute les trads FR. ``` --- .env | 9 + composer.json | 6 +- composer.lock | 247 +++++++++++++++++++++++- config/bundles.php | 1 + config/packages/knpu_oauth2_client.yaml | 14 ++ config/packages/security.yaml | 2 + migrations/Version20260111130759.php | 32 +++ src/Controller/KeycloakController.php | 32 +++ src/Entity/Account.php | 2 +- src/Security/DiscordAuthenticator.php | 88 +++++++++ src/Security/KeycloakAuthenticator.php | 87 +++++++++ symfony.lock | 12 ++ templates/security/login.twig | 22 ++- translations/messages.fr.yaml | 5 + 14 files changed, 555 insertions(+), 4 deletions(-) create mode 100644 config/packages/knpu_oauth2_client.yaml create mode 100644 migrations/Version20260111130759.php create mode 100644 src/Controller/KeycloakController.php create mode 100644 src/Security/DiscordAuthenticator.php create mode 100644 src/Security/KeycloakAuthenticator.php diff --git a/.env b/.env index b68a337..3ece1ea 100644 --- a/.env +++ b/.env @@ -81,3 +81,12 @@ STRIPE_SECRET_KEY=sk_test_*** NOTIFUSE_CLIENT_EMAIL=jovann@siteconseil.fr NOTIFUSE_CLIENT_SECRET=d04zCk3Fa45oOjDWHpAvc1AZxnLdGffOnNWK+Jt2yXf37+FTfuMMHb8flcfPMqLluRR3rvhbr555r6j1DEigrA== NOTIFUSE_WORKSPACE_ID=ecosplay + + +KEYCLOAK_URL=https://auth.esy-web.dev +KEYCLOAK_REALM=master +KEYCLOAK_CLIENT_ID=ecosplay +KEYCLOAK_CLIENT_SECRET=Cr2WycUH1Vti6xKYMOQSs0etO4tfHj1Q + +DISCORD_CLIENT_ID=1392734559099158549 +DISCORD_CLIENT_SECRET=upafzLsVMvwX_VC0Jw6Thy7d8lPpQGts diff --git a/composer.json b/composer.json index 6f47784..22ad649 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "ext-iconv": "*", "ext-libxml": "*", "ext-zip": "*", + "adam-paterson/oauth2-stripe": "^2.0", "chillerlan/php-qrcode": ">=5.0.5", "cocur/slugify": ">=4.6", "doctrine/dbal": "^3.10.3", @@ -26,6 +27,7 @@ "imagine/imagine": "^1.5", "io-developer/php-whois": ">=4.1.10", "knplabs/knp-paginator-bundle": "^6.9.1", + "knpuniversity/oauth2-client-bundle": "^2.20", "lasserafn/php-initial-avatar-generator": "^4.5", "league/flysystem-aws-s3-v3": "^3.30.1", "league/flysystem-bundle": "^3.6", @@ -46,6 +48,7 @@ "setasign/fpdi": "^2.6.4", "spatie/mjml-php": "^1.2.5", "stancer/stancer": ">=2.0.1", + "stevenmaguire/oauth2-keycloak": "^5.1", "stripe/stripe-php": "^19.0", "symfony/amazon-mailer": "7.3.*", "symfony/asset": "7.3.*", @@ -82,7 +85,8 @@ "twig/intl-extra": "^3.22.1", "twig/twig": "^3.22", "vich/uploader-bundle": "^2.8.1", - "web-auth/webauthn-lib": ">=5.2.2" + "web-auth/webauthn-lib": ">=5.2.2", + "wohali/oauth2-discord-new": "^1.2" }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index f61df05..8c62410 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,67 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ed26fccb39be0438a7def45a9b987dc9", + "content-hash": "403d821dbd4191efa1335b197d907faf", "packages": [ + { + "name": "adam-paterson/oauth2-stripe", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/adam-paterson/oauth2-stripe.git", + "reference": "52a43a58a51dceac5d91c906c6da13f0994951b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/adam-paterson/oauth2-stripe/zipball/52a43a58a51dceac5d91c906c6da13f0994951b6", + "reference": "52a43a58a51dceac5d91c906c6da13f0994951b6", + "shasum": "" + }, + "require": { + "league/oauth2-client": "^2.0", + "php": ">=5.6.0" + }, + "require-dev": { + "mockery/mockery": "~0.9", + "phpunit/phpunit": "~4.0", + "scrutinizer/ocular": "^1.1", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "AdamPaterson\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Adam Paterson", + "email": "hello@adampaterson.co.uk" + } + ], + "description": "Stripe OAuth 2.0 Client Provider for The PHP League OAuth2-Client", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on", + "stripe", + "stripe api" + ], + "support": { + "issues": "https://github.com/adam-paterson/oauth2-stripe/issues", + "source": "https://github.com/adam-paterson/oauth2-stripe/tree/master" + }, + "time": "2018-02-26T09:35:45+00:00" + }, { "name": "async-aws/core", "version": "1.27.1", @@ -4650,6 +4709,66 @@ }, "time": "2025-07-25T07:53:13+00:00" }, + { + "name": "knpuniversity/oauth2-client-bundle", + "version": "v2.20.1", + "source": { + "type": "git", + "url": "https://github.com/knpuniversity/oauth2-client-bundle.git", + "reference": "d59e4dc61484e777b6f19df2efcf8b1bcc03828a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/knpuniversity/oauth2-client-bundle/zipball/d59e4dc61484e777b6f19df2efcf8b1bcc03828a", + "reference": "d59e4dc61484e777b6f19df2efcf8b1bcc03828a", + "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.1" + }, + "time": "2025-12-04T15:46:43+00:00" + }, { "name": "lasserafn/php-initial-avatar-generator", "version": "4.5", @@ -8437,6 +8556,67 @@ }, "time": "2024-11-15T17:47:59+00:00" }, + { + "name": "stevenmaguire/oauth2-keycloak", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/stevenmaguire/oauth2-keycloak.git", + "reference": "1b690b7377dfe7a23e1590373f37e12cf40a6d75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stevenmaguire/oauth2-keycloak/zipball/1b690b7377dfe7a23e1590373f37e12cf40a6d75", + "reference": "1b690b7377dfe7a23e1590373f37e12cf40a6d75", + "shasum": "" + }, + "require": { + "firebase/php-jwt": "^6.0", + "league/oauth2-client": "^2.0", + "php": "~7.2 || ~8.0" + }, + "require-dev": { + "mockery/mockery": "~1.5.0", + "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/5.1.0" + }, + "time": "2023-10-24T06:10:44+00:00" + }, { "name": "stripe/stripe-php", "version": "v19.0.0", @@ -15230,6 +15410,71 @@ "source": "https://github.com/webmozarts/assert/tree/1.12.1" }, "time": "2025-10-29T15:56:20+00:00" + }, + { + "name": "wohali/oauth2-discord-new", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/wohali/oauth2-discord-new.git", + "reference": "2df4d2a882e04c749880797704e4bde8f00ea1d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wohali/oauth2-discord-new/zipball/2df4d2a882e04c749880797704e4bde8f00ea1d9", + "reference": "2df4d2a882e04c749880797704e4bde8f00ea1d9", + "shasum": "" + }, + "require": { + "ext-json": "*", + "league/oauth2-client": "^2.0", + "php": "^7.2|^8.0" + }, + "conflict": { + "team-reflex/oauth2-discord": ">=1.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.0", + "php-parallel-lint/php-parallel-lint": "~0.9", + "phpunit/phpunit": "~8.0", + "squizlabs/php_codesniffer": "^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Wohali\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joan Touzet", + "email": "code@atypical.net", + "homepage": "https://github.com/wohali" + } + ], + "description": "Discord OAuth 2.0 Client Provider for The PHP League OAuth2-Client", + "keywords": [ + "authorisation", + "authorization", + "client", + "discord", + "oauth", + "oauth2" + ], + "support": { + "issues": "https://github.com/wohali/oauth2-discord-new/issues", + "source": "https://github.com/wohali/oauth2-discord-new/tree/1.2.1" + }, + "time": "2022-12-29T18:45:10+00:00" } ], "packages-dev": [ diff --git a/config/bundles.php b/config/bundles.php index d7bd546..1a18dc7 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -19,4 +19,5 @@ return [ Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true], Sentry\SentryBundle\SentryBundle::class => ['prod' => true], PixelOpen\CloudflareTurnstileBundle\PixelOpenCloudflareTurnstileBundle::class => ['all' => true], + KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true], ]; diff --git a/config/packages/knpu_oauth2_client.yaml b/config/packages/knpu_oauth2_client.yaml new file mode 100644 index 0000000..7a7fd3e --- /dev/null +++ b/config/packages/knpu_oauth2_client.yaml @@ -0,0 +1,14 @@ +knpu_oauth2_client: + clients: + keycloak: + type: keycloak + auth_server_url: '%env(KEYCLOAK_URL)%' + realm: '%env(KEYCLOAK_REALM)%' + client_id: '%env(KEYCLOAK_CLIENT_ID)%' + client_secret: '%env(KEYCLOAK_CLIENT_SECRET)%' + redirect_route: connect_keycloak_check + discord: + type: discord + client_id: '%env(DISCORD_CLIENT_ID)%' + client_secret: '%env(DISCORD_CLIENT_SECRET)%' + redirect_route: connect_discord_check diff --git a/config/packages/security.yaml b/config/packages/security.yaml index c93de53..4893398 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -27,6 +27,8 @@ security: entry_point: App\Security\AuthenticationEntryPoint custom_authenticator: - App\Security\LoginFormAuthenticator + - App\Security\KeycloakAuthenticator + - App\Security\DiscordAuthenticator logout: target: app_logout diff --git a/migrations/Version20260111130759.php b/migrations/Version20260111130759.php new file mode 100644 index 0000000..c82dbc5 --- /dev/null +++ b/migrations/Version20260111130759.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE account ALTER password DROP NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE "account" ALTER password SET NOT NULL'); + } +} diff --git a/src/Controller/KeycloakController.php b/src/Controller/KeycloakController.php new file mode 100644 index 0000000..1626f73 --- /dev/null +++ b/src/Controller/KeycloakController.php @@ -0,0 +1,32 @@ +getClient('keycloak')->redirect(['openid', 'profile', 'email'], []); + } + #[Route('/connect/discord', name: 'connect_discord_start')] + public function connectActionDiscord(ClientRegistry $clientRegistry) + { + // Redirige vers Keycloak + return $clientRegistry->getClient('discord')->redirect(['openid', 'email','identify'], []); + } + + #[Route('/oauth/keycloak', name: 'connect_keycloak_check')] + #[Route('/oauth/discord', name: 'connect_discord_check')] + public function connectCheckAction(Request $request) + { + // Cette méthode reste vide, elle est interceptée par l'Authenticator + } +} diff --git a/src/Entity/Account.php b/src/Entity/Account.php index 51875c2..2a32904 100644 --- a/src/Entity/Account.php +++ b/src/Entity/Account.php @@ -36,7 +36,7 @@ class Account implements UserInterface, PasswordAuthenticatedUserInterface, \Ser #[ORM\Column] private array $roles = []; - #[ORM\Column] + #[ORM\Column(nullable: true)] private ?string $password = null; #[ORM\Column(length: 255)] diff --git a/src/Security/DiscordAuthenticator.php b/src/Security/DiscordAuthenticator.php new file mode 100644 index 0000000..dfd1949 --- /dev/null +++ b/src/Security/DiscordAuthenticator.php @@ -0,0 +1,88 @@ +attributes->get('_route') === 'connect_discord_check'; + } + + public function authenticate(Request $request): SelfValidatingPassport + { + $client = $this->clientRegistry->getClient('discord'); + $accessToken = $this->fetchAccessToken($client); + + return new SelfValidatingPassport( + new UserBadge($accessToken->getToken(), function() use ($accessToken, $client) { + /** @var DiscordResourceOwner $discordUser */ + $discordUser = $client->fetchUserFromToken($accessToken); + $email = $discordUser->getEmail(); + + // 1) Chercher l'utilisateur par son ID Keycloak ou son Email + $user = $this->entityManager->getRepository(Account::class)->findOneBy(['email' => $email]); + + // 2) Créer l'utilisateur s'il n'existe pas (Inscription automatique) + if (!$user) { + $user = new Account(); + $user->setEmail($email); + $user->setUsername($email); + $user->setUuid(Uuid::uuid4()->toString()); + $user->setRoles(['ROLE_USER']); + $user->setIsActif(true); + // On peut mapper d'autres champs : + // $user->setFirstName($keycloakUser->toArray()['given_name']); + $this->entityManager->persist($user); + } + + // 3) Mettre à jour les rôles ou infos si nécessaire + $this->entityManager->flush(); + + return $user; + }) + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + // Redirige vers la page demandée initialement ou l'accueil + if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { + return new RedirectResponse($targetPath); + } + + return new RedirectResponse($this->router->generate('app_home')); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + $message = strtr($exception->getMessageKey(), $exception->getMessageData()); + return new Response($message, Response::HTTP_FORBIDDEN); + } +} diff --git a/src/Security/KeycloakAuthenticator.php b/src/Security/KeycloakAuthenticator.php new file mode 100644 index 0000000..593fd01 --- /dev/null +++ b/src/Security/KeycloakAuthenticator.php @@ -0,0 +1,87 @@ +attributes->get('_route') === 'connect_keycloak_check'; + } + + public function authenticate(Request $request): SelfValidatingPassport + { + $client = $this->clientRegistry->getClient('keycloak'); + $accessToken = $this->fetchAccessToken($client); + + return new SelfValidatingPassport( + new UserBadge($accessToken->getToken(), function() use ($accessToken, $client) { + /** @var \Stevenmaguire\OAuth2\Client\Provider\KeycloakResourceOwner $keycloakUser */ + $keycloakUser = $client->fetchUserFromToken($accessToken); + $email = $keycloakUser->getEmail(); + + // 1) Chercher l'utilisateur par son ID Keycloak ou son Email + $user = $this->entityManager->getRepository(Account::class)->findOneBy(['email' => $email]); + + // 2) Créer l'utilisateur s'il n'existe pas (Inscription automatique) + if (!$user) { + $user = new Account(); + $user->setEmail($email); + $user->setUsername($keycloakUser->toArray()['preferred_username']); + $user->setUuid(Uuid::uuid4()->toString()); + $user->setRoles(['ROLE_USER']); + $user->setIsActif(true); + // On peut mapper d'autres champs : + // $user->setFirstName($keycloakUser->toArray()['given_name']); + $this->entityManager->persist($user); + } + + // 3) Mettre à jour les rôles ou infos si nécessaire + $this->entityManager->flush(); + + return $user; + }) + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + // Redirige vers la page demandée initialement ou l'accueil + if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { + return new RedirectResponse($targetPath); + } + + return new RedirectResponse($this->router->generate('app_home')); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + $message = strtr($exception->getMessageKey(), $exception->getMessageData()); + return new Response($message, Response::HTTP_FORBIDDEN); + } +} diff --git a/symfony.lock b/symfony.lock index e9d5459..f605b87 100644 --- a/symfony.lock +++ b/symfony.lock @@ -50,6 +50,18 @@ "knplabs/knp-paginator-bundle": { "version": "v6.8.1" }, + "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" + ] + }, "league/flysystem-bundle": { "version": "3.4", "recipe": { diff --git a/templates/security/login.twig b/templates/security/login.twig index f51f051..da12478 100644 --- a/templates/security/login.twig +++ b/templates/security/login.twig @@ -60,7 +60,7 @@ {% endfor %} - {# FORMULAIRE #} + {# FORMULAIRE CLASSIQUE #}
@@ -103,7 +103,27 @@
+ {# SÉPARATEUR #} +
+
+
+
+
+ {{ 'common.or'|trans }} +
+
+ {# BOUTON KEYCLOAK (SSO) - Placé en haut pour favoriser le SSO #} + + + {{ 'button.login_keycloak'|trans|default('Connexion Keycloak') }} + + + + {{ 'button.login_discord'|trans|default('Connexion Keycloak') }} + diff --git a/translations/messages.fr.yaml b/translations/messages.fr.yaml index 45c9ad0..d35f7e2 100644 --- a/translations/messages.fr.yaml +++ b/translations/messages.fr.yaml @@ -1225,3 +1225,8 @@ newsletter: message: "Ton inscription a été validée avec succès. Bienvenue dans la communauté !" hint: "Prépare-toi à recevoir le meilleur du Cosplay directement dans ta boîte." back_button: "C'EST PARTI !" +button: + login_keycloak: 'SSO' + login_discord: 'Discord' +common: + or: 'Ou'