Skip to content

Instantly share code, notes, and snippets.

@HiroKX
Last active July 12, 2024 08:28
Show Gist options
  • Save HiroKX/244ff784ed23cb2c545324ac9a58e339 to your computer and use it in GitHub Desktop.
Save HiroKX/244ff784ed23cb2c545324ac9a58e339 to your computer and use it in GitHub Desktop.
Add Keycloak to a Symfony Project
# You need both bundles :
# composer require knpuniversity/oauth2-client-bundle
# composer require stevenmaguire/oauth2-keycloak
OAUTH_KEYCLOAK_CLIENT_ID=""
OAUTH_KEYCLOAK_CLIENT_SECRET=""
OAUTH_KEYCLOAK_URL="(Example : https://my.keycloack:922)"
OAUTH_KEYCLOAK_REALM="Realm"
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\Provider\KeycloakClient;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Stevenmaguire\OAuth2\Client\Provider\KeycloakResourceOwner;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
class KeycloakAuthenticator extends OAuth2Authenticator implements AuthenticatorInterface
{
use TargetPathTrait;
public function __construct(private readonly ClientRegistry $clientRegistry, private readonly EntityManagerInterface $entityManager, private readonly RouterInterface $router, private readonly KeycloakUserProviderInterface $userProvider)
{}
public function supports(Request $request): ?bool
{
return $request->attributes->get('_route') === 'connect_keycloak_check'; //The route name must be the name as the one that redirect
}
public function authenticate(Request $request): Passport
{
/** @var KeycloakClient $client */
$client = $this->getKeycloakClient();
$accessToken = $this->fetchAccessToken($client);
if (null === $accessToken) {
throw new CustomUserMessageAuthenticationException('No access token provided');
}
return new SelfValidatingPassport(
new UserBadge($accessToken->getToken(), function() use ($accessToken, $client) {
return $this->userProvider->loadUserByIdentifier($accessToken);
}
));
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
$url = $this->router->generate('index'); //IMPORTANT : URI AVAILABLE TO REDIRECT WHEN SUCCESS
return new RedirectResponse($url);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
return new Response($message, Response::HTTP_FORBIDDEN);
}
private function getKeycloakClient()
{
return $this->clientRegistry
->getClient('keycloak');
}
}
<?php
namespace App\Security;
use App\Entity\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Client\OAuth2ClientInterface;
use KnpU\OAuth2ClientBundle\Security\User\OAuthUserProvider;
use Stevenmaguire\OAuth2\Client\Provider\KeycloakResourceOwner;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
class KeycloakUserProvider extends OAuthUserProvider implements KeycloakUserProviderInterface
{
public function __construct(private readonly UserRepository $userRepository, private readonly ClientRegistry $clientRegistry, private readonly EntityManagerInterface $entityManager)
{
}
public function loadUserByIdentifier($accessToken): UserInterface
{
try {
/** @var KeycloakResourceOwner $keycloakUser */
$keycloakUser = $this->getKeycloakClient()->fetchUserFromToken($accessToken);
} catch (\UnexpectedValueException $e) {
throw new UserNotFoundException();
}
$email = $keycloakUser->getEmail();
$existingUser = $this->userRepository->findOneBy(['username' => $email]);
if ($existingUser) {
$existingUser->setRoles($keycloakUser->toArray()['groups']);
$existingUser->setAccessToken($accessToken);
$this->entityManager->persist($existingUser);
$this->entityManager->flush();
return $existingUser;
}
$user = new User();
$user->setUsername($keycloakUser->getEmail());
$user->setRoles($keycloakUser->toArray()['groups']);
$user->setAccessToken($accessToken);
$this->entityManager->persist($user);
$this->entityManager->flush();
return $user;
}
public function refreshUser(UserInterface $user): UserInterface
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
$accessToken = $user->getAccessToken();
if ($accessToken->hasExpired()) {
$accessToken = $this->getKeycloakClient()->getOAuth2Provider()->getAccessToken(
'refresh_token',
[
'refresh_token' => $accessToken->getRefreshToken(),
]
);
}
return $this->loadUserByIdentifier($accessToken);
}
protected function getKeycloakClient(): OAuth2ClientInterface
{
return $this->clientRegistry->getClient('keycloak');
}
public function supportsClass($class): bool
{
return User::class === $class;
}
}
<?php
namespace App\Security;
use Symfony\Component\Security\Core\User\UserInterface;
interface KeycloakUserProviderInterface
{
public function loadUserByIdentifier($accessToken):UserInterface;
}
#config/packages/
knpu_oauth2_client:
clients:
keycloak:
type: keycloak
client_id: '%env(OAUTH_KEYCLOAK_CLIENT_ID)%'
client_secret: '%env(OAUTH_KEYCLOAK_CLIENT_SECRET)%'
redirect_route: connect_keycloak_check #Route name to verify
redirect_params: {}
auth_server_url: '%env(OAUTH_KEYCLOAK_URL)%'
realm: '%env(OAUTH_KEYCLOAK_REALM)%'
use_state: true
#config/packages/
security:
providers:
keycloak:
id: App\Security\KeycloakUserProvider
firewalls:
main:
lazy: true
form_login:
provider: keycloak
login_path: connect_keycloak_login
custom_authenticators:
- App\Security\KeycloakAuthenticator
logout:
path: /logout
target: /
access_control:
- { path: ^/connect/keycloak, roles: PUBLIC_ACCESS }
- { path: ^/connect/keycloak/check, roles: IS_AUTHENTICATED }
<?php
namespace App\Controller;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Attribute\Route;
class SecurityController extends AbstractController
{
#[Route('/connect/keycloak', name: 'connect_keycloak_login')]
public function connect(ClientRegistry $clientRegistry)
{
return $clientRegistry
->getClient('keycloak')
->redirect(['openid']);
}
#[Route('/connect/keycloak/check', name: 'connect_keycloak_check')]
public function check()
{
return $this->render('base.html.twig');
}
}
<?php
//All fields are mandatory
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use League\OAuth2\Client\Token\AccessToken;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
class User implements UserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $username = null;
#[ORM\Column]
private array $roles = [];
private ?AccessToken $accessToken = null;
public function getAccessToken(): ?AccessToken
{
return $this->accessToken;
}
public function setAccessToken(?AccessToken $accessToken): void
{
$this->accessToken = $accessToken;
}
public function getId(): ?int
{
return $this->id;
}
public function getUsername(): ?string
{
return $this->username;
}
public function setUsername(string $username): static
{
$this->username = $username;
return $this;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
public function eraseCredentials(): void
{
}
public function getUserIdentifier(): string
{
return (string)$this->username;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment