0

I'm trying to implement an OAuth connection with Keycloak on Symfony 5.4, when I display a page of my application, all works fine, I have the keycloak login page, but after validate, I have this error :

Argument 1 passed to Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticator::__construct() must implement interface Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface, instance of App\Security\KeycloakAuthenticator given, called in /var/www/oauth-symfony/vendor/symfony/security-http/Authenticator/Debug/TraceableAuthenticatorManagerListener.php on line 60

Obviously I tried to add implements AuthenticatorInterface I hadd to add authenticate() and createToken() methods, but even with that the implementations still not works.

KeycloakAuthenticator.php

<?php

namespace App\Security;

use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;

/**
 * Class KeycloakAuthenticator
 */
class KeycloakAuthenticator extends SocialAuthenticator
{
    /**
     * ClientRegistry: the OAuth client manager
     * EntityManagerInterface: to read/write in database
     * RouterInterface: read a route/URL
     */
    private $clientRegistry;
    private $em;
    private $router;

    public function __construct(
        ClientRegistry $clientRegistry,
        EntityManagerInterface $em,
        RouterInterface $router
        )
    {
        $this->clientRegistry = $clientRegistry;
        $this->em = $em;
        $this->router = $router;   
    }

    public function start(Request $request, \Symfony\Component\Security\Core\Exception\AuthenticationException $authenticationException = null): RedirectResponse
    {
        return new RedirectResponse(
            '/oauth/login', // might be the site, where users choose their oauth provider
            Response::HTTP_TEMPORARY_REDIRECT
        );
    }

    public function supports(Request $request): ?bool
    {
        return $request->attributes->get('_route') === 'oauth_check';
    }

    public function getCredentials(Request $request)
    {
        return $this->fetchAccessToken($this->getKeycloakClient());
    }

    public function getUser($credentials, \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider)
    {
        $keycloakUser = $this->getKeycloakClient()->fetchUserFromToken($credentials);
        //existing user ?
        $existingUser = $this
                            ->em
                            ->getRepository(User::class)
                            ->findOneBy(['keycloakId' => $keycloakUser->getId()]);
        if ($existingUser) {
            return $existingUser;
        }
        // if user exist but never connected with keycloak
        $email = $keycloakUser->getEmail();
        /** @var User $userInDatabase */
        $userInDatabase = $this->em->getRepository(User::class)
            ->findOneBy(['email' => $email]);
        if($userInDatabase) {
            $userInDatabase->setKeycloakId($keycloakUser->getId());
            $this->em->persist($userInDatabase);
            $this->em->flush();
            return $userInDatabase;
        }
        //user not exist in database
        $user = new User();
        $user->setKeycloakId($keycloakUser->getId());
        $user->setEmail($keycloakUser->getEmail());
        $user->setRoles(['ROLE_USER']);
        $this->em->persist($user);
        $this->em->flush();
        return $user;
    }

    public function onAuthenticationFailure(Request $request, \Symfony\Component\Security\Core\Exception\AuthenticationException $exception)
    {
        $message = strtr($exception->getMessageKey(), $exception->getMessageData());

        return new Response($message, Response::HTTP_FORBIDDEN);
    }

    public function onAuthenticationSuccess(Request $request, \Symfony\Component\Security\Core\Authentication\Token\TokenInterface $token, string $providerKey)
    {
        // change "app_homepage" to some route in your app
        $targetUrl = $this->router->generate('dashboard');

        return new RedirectResponse($targetUrl);
    }

    /**
     * @return \KnpU\OAuth2ClientBundle\Client\Provider\KeycloakClient
     */
    private function getKeycloakClient()
    {
        return $this->clientRegistry->getClient('keycloak');
    }
}

security.yml

security:
    enable_authenticator_manager: true

    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'

    providers:

        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
            entry_point: form_login
            form_login:
                login_path: oauth_login
            custom_authenticator: App\Security\KeycloakAuthenticator


    access_control:
        # - { path: ^/admin, roles: ROLE_ADMIN }
        # - { path: ^/profile, roles: ROLE_USER }
        - { path: ^/dashboard, roles: ROLE_USER }

OAuthController.php

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Annotation\Route;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Client\Provider\KeycloakClient;

class OAuthController extends AbstractController
{
    /**
     * @Route("/oauth/login", name="oauth_login")
     */
    public function index(ClientRegistry $registry): RedirectResponse
    {
        /**@var KeycloakClient $client */
        $client = $registry->getClient('keycloak');

        return $client->redirect();
    }

    /**
     * @Route("/oauth/callback", name="oauth_check")
     */
    public function check(){}
}

1 Answers1

0

You need replace the class to extend by OAuth2Authenticator 1.

Add the typing ": ?Response" on onAuthenticationSuccess and onAuthenticationFailure

And create the function "authenticate" to return PassportInterface

Bento2
  • 1