Symfony 5.4 with OAuth Keycloak connect Error must implement AuthenticatorInterface - oauth

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(){}
}

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

Related

zf2 - Nested Aggregate Hydrators

Is it possible to nest aggregate hydrators? If i have the following classes:
class Appointment{
public date;
public startTime;
public endTime;
public User; //* #var User */
}
class User{
public Location; //* #var Location*/
}
...being populated with the following AggregateHydrator (created from a factory):
class AppointmentModelHydratorFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator) {
$serviceManager = $serviceLocator->getServiceLocator();
$arrayHydrator = new ArraySerializable();
$arrayHydrator->addStrategy('date', new DateTimeStrategy())
->addStrategy('endTime', new TimeStrategy())
->addStrategy('startTime', new TimeStrategy());
$aggregateHydrator = new AggregateHydrator();
$aggregateHydrator->add($arrayHydrator);
$aggregateHydrator->add($serviceLocator->get('Hydrator\User'));
return $aggregateHydrator;
}
}
With the UserHydratorFactory looking like:
class UserHydratorFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator) {
$sm = $serviceLocator->getServiceLocator();
$userHydrator = new UserHydrator($sm->get('User\Mapper'));
$aggregateHydrator = new AggregateHydrator();
$aggregateHydrator->add($userHydrator );
$aggregateHydrator->add($sm->get('HydratorManager')->get('Hydrator\User\Location'));
return $aggregateHydrator;
}
}
This is throwing an expection as the model is being returned as null, but if i comment out adding the Location hydrator to the User hydrator, it works fine (albeit without location data loaded). So i was wondering if aggregate hydrators are able to be nested?
It is not built-in, but doable.
namespace Hydrator;
use Zend\Stdlib\Hydrator\HydratorInterface;
class NestedHydrator implements HydratorInterface
{
protected $inner_hydrator;
private $empty;
public function __construct ($inner_hydrator, $empty)
{
$this->inner_hydrator = $inner_hydrator;
$this->empty = $empty;
}
public function extract ($object)
{
return [
$this->getPath() => $this->inner_hydrator->extract ($object->{$this->getPath()})
];
}
public function hydrate (array $data, $object)
{
$object->{$this->getPath()} = $this->inner_hydrator->hydrate ($data [$this->getPath()], $this->empty);
return $object;
}
protected function getPath ()
{
return get_class ($this->empty);
}
}
And then:
$u = new User();
$u->Location = "4 Clinton Rd.";
$a = new Appointment();
$a->date = "yesterday";
$a->startTime = "7:00";
$a->endTime = "8:00";
$a->User = $u;
$h = new AggregateHydrator();
$h->add (new ObjectProperty());
$nested = new \Hydrator\NestedHydrator(new ObjectProperty(), new User());
$h->add ($nested);
$data = $h->extract ($a);
$b = $h->hydrate ($data, new Appointment());
$this->assertEquals ($a, $b);

Laravel 5: How to custom error function in validator request?

My custom validation request:
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
class AccountPostRequest extends Request
{
public function authorize() {
return true;
}
public function rules() {
return [
'username' => 'required|alpha_dash'
];
}
public function message() {
return [
'username.required' => 'input your email',
'username.alpha_dash' => 'email format error'
];
}
protected function formatErrors(Validator $validator) {
return $validator->errors()->all();
}
//here is my question, how to invoke this callback when the validation fails,
//or has any other function like the "before" or "after" filters
protected function callback(Validator $validator) {
if ($validator->fails()) {
//do some thing
} else {
//do some thins
}
}
}
?>
My controller:
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\AccountPostRequest;
class AccountController extends Controller
{
public function login(AccountPostRequest $request) {
//...
}
}
As you know, if the request validate failed, it will not continue to execute the login function of AccountController.
My question is how to invoke the callback when the validation fails, or has any other function like the "before" or "after" filters?
All form request that are validated are going through a pipeline in Illuminate\Foundation\Http\FormRequest which uses a trait called Illuminate\Validation\ValidatesWhenResolvedTrait. This class has contains all the classes you may want to modify and overwrite.
For example, you can simply copy and paste the failedValidation method and customize it.
/**
* Handle a failed validation attempt.
*
* #param \Illuminate\Contracts\Validation\Validator $validator
* #return mixed
*/
protected function failedValidation(Validator $validator)
{
// Do something awesome...
}

catching the login event of ZfcUser with Zend framework 2

i am using zfcUser with Zend Framework 2.
i want to capture the login event so that i can do something with it and also redirect the user to a different page.
i know how to catch the register event. ie i place a onBootstrap in my module.php file and then do the following
$em->attach('ZfcUser\Form\RegisterFilter','init',function($e)
the entire function is like this;
public function onBootstrap(MVCEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$em = $eventManager->getSharedManager();
$em->attach(
'ZfcUser\Form\RegisterFilter',
'init',
function($e)
{ }
}
However, when i tried to catch the login event i did not have any results. i.e
$zfcServiceEvents->attach('login.post', function($e) {
echo "loggedn"; die();
});
would really appreciate some help.
thank you.
edit:
this is how i called the registered event ( i know how to called the event for a user who has just registered. but i want to call the event when a user has just succesfully logged in
public function onBootstrap(MVCEvent $e)
{
$zfcServiceEvents = $e->getApplication()->getServiceManager()->get('zfcuser_user_service')->getEventManager();
$zfcServiceEvents->attach('register', function($e) {
$form = $e->getParam('form');
$user = $e->getParam('user');
RESPONSE TO cptnk CODE
in response to the answer given by cptnk i tried the following code but it did not work;
public function onBootstrap(MVCEvent $e)
{
$sharedManager = $e->getApplication()->getEventManager()->getSharedManager();
$serviceManager = $e->getApplication()->getServiceManager();
$loginFunction = function ($e) use ($serviceManager) {
echo "eventCaptured" ; die();
};
$sharedManager->attach('ZfcUser\Service\User', 'login.post', $loginFunction);
}
i am still not able to capture the login event.Any ideas?
cptnik had a good suggestion; he asked whether i had overridden the ZFcUser-service. in responce i clarfiy that i had overridden the zfcuser. below is the code i used;
'zfcuser' => array(
// telling ZfcUser to use our own class
'user_entity_class' => 'BaseModel\Entity\User',
// telling ZfcUserDoctrineORM to skip the entities it defines
'enable_default_entities' => false,
),
/**
* Listen to the bootstrap event
*
* #return array
*/
public function onBootstrap(MvcEvent $e)
{
$serviceManager = $e->getApplication()->getServiceManager()
$loginFunction = function ($e) use ($serviceManager) {
// do something
};
$sharedManager->attach('ZfcUser\Service\User', 'login.post', $loginFunction);
}
Your code does not show what $zfcServiceEvents is or where it came from I assume it is a event manager referencing the zfcuser events. My approach is a little different since I attach a event to the zf2 shared manager. Maybe you have problems regarding that piece of code?
You can grab the sharedEventManager like so:
$sharedManager = $e->getApplication()->getEventManager()->getSharedManager();
EDIT: I was a little unclear on how to get the $serviceManager and the application within the Module.php onBootstrap function.
Try this
$events = $e->getApplication()->getEventManager()->getSharedManager();
// Handle login
$events->attach('ZfcUser\Authentication\Adapter\AdapterChain', 'authenticate.success', function($e) {
$userId = $e->getIdentity();
// do some stuff
});
In your Module.php add this:
public function onBootstrap(MvcEvent $e) {
$em = $e->getApplication()->getEventManager();
$UserListener = $e->getApplication()->getServiceManager()->get('UserListener');
$em->attachAggregate($UserListener);
}
and create src/User/Listener/UserListener.php with below code:
namespace LcUser\Listener;
use Zend\EventManager\AbstractListenerAggregate;
use Zend\EventManager\EventManagerInterface;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class LcUserListener extends AbstractListenerAggregate implements ServiceLocatorAwareInterface {
/**
* #var ServiceLocatorInterface
*/
protected $serviceManager;
/**
*
* #param ServiceLocatorInterface $serviceLocator
*/
public function setServiceLocator(ServiceLocatorInterface $serviceLocator) {
$this->serviceManager = $serviceLocator;
}
/**
*
* #return \Zend\ServiceManager\ServiceLocatorInterface
*/
public function getServiceLocator() {
return $this->serviceManager;
}
public function attach(EventManagerInterface $events) {
$sharedManager = $events->getSharedManager();
$this->listeners[] = $sharedManager->attach('ZfcUser\Authentication\Adapter\AdapterChain', 'authenticate.success', array($this, 'userLog'));
}
public function userLog(Event $e) {
$em = $this->getServiceLocator()->get('doctrine.entitymanager.orm_default');
$userlog = new UserLogin(); // User Login is my User Log table to record login details as below
$user = $em->getRepository('MyUser\Entity\User')
->findOneBy(array('id' => $e->getIdentity()));
$userlog->setGroupId($user->getId());
$userlog->setUserId($e->getIdentity());
$userlog->setEmail($_POST['identity']);
$userlog->setIpAddresses($_SERVER['SERVER_ADDR']);
$em->persist($userlog);
$em->flush();
}
}

How to create a service layer in zend framwork two?

I need to create a service layer for Zend framework two controller functions in order to decouple the services from controllers.
You're going to need to use the ServiceManager (SM) in order to make this work properly.
This is just an example of how I have done it:
In your ModuleName/src/ModuleName/ create a folder named Service and create your ExampleService.php, Example:
namespace ModuleName\Service;
class ExampleService
{
public function SomeFunctionNameHere()
{
echo 'Hello World';
}
}
Now edit your Module.php and add the Service Layer to your invokables, IE:
public function getServiceConfig()
{
return array(
'invokables' => array(
'ModuleName\Service\ExampleService' => 'ModuleName\Service\ExampleService',
),
);
}
Now edit your ModuleNameController.php
protected $service_example;
public function indexAction()
{
$service = $this->getServiceExample()->SomeFunctionNameHere();
}
private function getServiceExample()
{
if (!$this->service_example) {
$this->service_example = $this->getServiceLocator()->get('ModuleName\Service\ExampleService');
}
return $this->service_example;
}
This should get you started.
Depending on the functionality you are looking for from your service, you might be able to create a custom Controller Plugin. For example, here's a custom controller plugin I wrote to get a user's access level.
Application/Controller/Plugin/GetAccessLevel.php
namespace Application\Controller\Plugin;
use Zend\Mvc\Controller\Plugin\AbstractPlugin;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
class GetAccessLevel extends AbstractPlugin implements ServiceLocatorAwareInterface
{
/**
* Set the service locator.
*
* #param ServiceLocatorInterface $serviceLocator
* #return GetAccessLevel
*/
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
return $this;
}
/**
* Get the service locator.
*
* #return \Zend\ServiceManager\ServiceLocatorInterface
*/
public function getServiceLocator()
{
return $this->serviceLocator;
}
/**
* Takes an array of role objects and returns access level
*
* #param array of MyModule\Entity\Role objects
* #return int Access Level
*/
public function __invoke(array $roles)
{
// Default access level
$accesslevel = 0;
// Get Service Locator for view helpers
$controllerPluginManager = $this->getServiceLocator();
// Get application service manager
$serviceManager = $controllerPluginManager->getServiceLocator();
// Get application config
$config = $serviceManager->get('Config');
// Get the role associated with full access from config
$fullAccessRole = $config['appSettings']['full_access_role'];
// Does user have the role for full access?
foreach ($roles as $roleObject) {
if($roleObject->getName() == $fullAccessRole) {
$accesslevel = 1;
break;
}
}
// Return access level
return $accesslevel;
}
}
Then add the plugin to the configuration.
./module/Application/config/module.config.php
'controller_plugins' => array(
'invokables' => array(
'getAccessLevel' => 'Application\Controller\Plugin\GetAccessLevel'
)
),
Now every controller will have access to this plugin.
Some Controller
public function someAction() {
$accessLevel = $this->getAccesslevel(array('User Role Entities Go Here'));
}

How to inject ServiceManager into a user defined class

In the doc it's said:"By default, the Zend Framework MVC registers an initializer that will inject the ServiceManager instance, which is an implementation of Zend\ServiceManager\ServiceLocatorInterface, into any class implementing Zend\ServiceManager\ServiceLocatorAwareInterface."
so I tried this:
interface ModelResourceInterface extends ServiceLocatorAwareInterface
{
}
interface ServiceModelResourceInterface extends ModelResourceInterface
{
public function fetch($uri, $method, $parameters, $options, $encodeType);
}
namespace Ssports\Model\Resource\Service\Http;
use Ssports\Model\Resource\Service\ServiceModelResourceInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\Http\Client;
use Zend\Http\Request;
use Ssports\Model\Resource\Service\ConnectionException;
abstract class AbstractHttpServiceModelResource implements ServiceModelResourceInterface
{
/**
*
* #var Zend\ServiceManager\ServiceLocatorInterface;
*/
protected $serviceLocator;
/**
* Constructor
*/
function __construct()
{
$this->init();
}
/**
* Extend Constructor
*/
public function init()
{}
/**
* (non-PHPdoc)
*
* #see \Zend\ServiceManager\ServiceLocatorAwareInterface::setServiceLocator()
*
*/
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
/**
* (non-PHPdoc)
*
* #see \Ssports\Model\Resource\Service\ServiceModelResourceInterface::fetch()
*
*/
public function fetch($uri, $method, $parameters = null, $options = null, $encodeType = null)
{
try {
//something raise \RuntimeException
} catch (\RuntimeException $e) {
$this->getServiceLocator()->get('Log\Web');
throw new ConnectionException();
}
}
/**
* (non-PHPdoc)
*
* #see \Zend\ServiceManager\ServiceLocatorAwareInterface::getServiceLocator()
*
*/
public function getServiceLocator()
{
return $this->serviceLocator;
}
}
I extend this abstract class with some model resource class, and run it, and an exception throw to say that I'm calling get on a non-object.
Seem that the service manager is not being injected to my abstract class, and the return of getServiceLocator is NULL.
Any thing I missed to make it right?
Have you tried to use the service locator trait?
It can be found in \Zend\ServiceManager\ServiceLocatorAwareTrait
However this requires PHP 5.4 to work...
To use a trait do the following
class Class1
{
use Zend\ServiceManager\ServiceLocatorAwareTrait;
}
You can access the service locator then, or at least that is how i had to do it when i needed to load the service locator.

Resources