- 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>
118 lines
4.0 KiB
PHP
118 lines
4.0 KiB
PHP
<?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'));
|
|
}
|
|
}
|