I'm trying to write a SSO authentication bundle for Symfony 5.4+
When I try to access pages with security attribute set to true in security.yaml, no problem, my authenticator class automatically launches and runs well. I can authenticated myself and then get a filled user instance in Symfony.
But now, what I'd like to do is a simple login page that would play the authentication process and then redirect the user to a page defined by the application admin in config files (for eventually add an authenticate button in app with public access).
My question is: how can I launch this authentication process from a controller as my login route has a public access (and then authentication system is never launched) ?
Something like that :
class AuthenticationController extends AbstractController
{
public function login(Request $request, Security $security)
{
if (!$this->getUser())
$security->pleaseLaunchTheAuthenticationProcess()
return new RedirectResponse('/my_path');
}
}
You can achieve that using the UserAuthenticatorInterface:
/**
* #Route("/some-route", name="some_route")
*
* #param Request $request
* #param UserAuthenticatorInterface $userAuthenticator
* #param AuthenticatorInterface $authenticator
* #return Response
*/
public function book(
Request $request,
UserAuthenticatorInterface $userAuthenticator,
AuthenticatorInterface $authenticator
): Response
{
if ($this->getUser() instanceof User) {
$userAuthenticator->authenticateUser($this->getUser(), $authenticator, $request);
}
return $this->redirectToRoute('/my_path');
}
You will have an error about AuthenticatorInterface which cannot be launched. You have to declare it manually in your service.yaml:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
bind:
$authenticator: '#security.authenticator.form_login.main'
Related
I'm having some issues understanding how the Law of Demeter should be applied in some cases with Symfony's DI system.
I have some factory that requires to access current logged in user in the application. To do that I need to require #security.token_storage to inject it as a constructor argument.
But in my factory, to access the user I will need to do : $tokenStorage->getToken()->getUser(), and worst, if I want to access some property of my user, I will need to dive one level deeper.
How would you fix this issue according to the law of demeter ?
Here is a sample of my code :
class SomeFactory
{
/**
* #var User
*/
private $currentUser;
/**
* #param TokenStorageInterface $tokenStorage
*/
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->currentUser = $this->setCurrentUser($tokenStorage);
}
/**
* #param TokenStorageInterface $tokenStorage
*/
protected function setCurrentUser(TokenStorageInterface $tokenStorage)
{
if ($tokenStorage->getToken()
&& $tokenStorage->getToken()->getUser()
&& in_array('ADMIN_ROLE', $tokenStorage->getToken()->getUser()->getRoles())
) {
$this->currentUser = $tokenStorage->getToken()->getUser();
}
}
}
I hope i am being clear.
Thank you very much :)
It seems that in DI factories the session has not been initialized, which makes the current user token unavailable, at this point.
What I did to make it work:
Register a new event listener for kernel.event:
services:
before_request_listener:
class: App\EventListener\MyRequestListener
tags:
-
name: kernel.event_listener
event: kernel.request
method: onKernelRequest
In MyRequestListener I've added my service as dependency (which invokes the factory) and provides the service, with incomplete data. Also I require Security:
public function __construct(\Symfony\Component\Security\Core\Security $security, MyService $service)
{
$this->security = $security;
$this->service = $service;
}
Now, in MyRequestListener::onKernelRequest I can access the user's data and add/change the incomplete properties of my service.
public function onKernelRequest(): void
{
if ($user = $this->security->getUser()) {
$this->service->whatever = $user->whatever;
}
}
Because Symfony uses the same instance of MyService, those modification will be available in all further services.
But keep in mind, that your service, also needs to deal with the incomplete data, when no active user session is existing (e.g. because no user is logged in, or on CLI).
I have a Symfony 3.2 application which exposes a REST API and uses Json Web Tokens (JWT) for authentication. I recently switched to using Symfony's Guard component. Now my security.yml contains a firewall config section as follows (I'm using the Lexik JWT bundle 2.4.0, but this shouldn't matter):
firewalls:
# ...
api:
pattern: ^/api
stateless: true
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
Since I did this switch, I notice that every request is handled as if the user just logged in, i.e. a security.interactive_login event is fired.
In the docs (http://symfony.com/doc/current/components/security/authentication.html#authentication-events) it states:
The security.interactive_login event is triggered after a user has actively
logged into your website. It is important to distinguish this action from
non-interactive authentication methods, such as:
authentication based on a "remember me" cookie,
authentication based on your session,
authentication using a HTTP basic or HTTP digest header.
You could listen on the security.interactive_login event, for example, in
order to give your user a welcome flash message every time they log in.
So I definitely don't expect this event for every request - I'd rather expect to get the security.authentication.success event on every request, as pointed out in the docs.
However, Symfony's GuardAuthenticatorHandler class dispatches the security.interactive_login event in its authenticateWithToken method, and this method is called by the GuardAuthenticationListener on every request.
Is that a bug in Symfony, a misunderstanding on my side, or due to incorrect configuration?
(This is not a philosophical question - in my case it leads to the concrete problem that the last login time of the user is updated on every request, which does not make sense.)
I've come across your issue, because I've exactly the same problem. My workaround is to add a attribute in the request object, right before return true in the supports method of the guard.
Example:
public function supports(Request $request)
{
...
$request->attributes->set('is_interactive_login', true);
return true;
}
With this information you can check if it was a interactive login in the event listener
Example:
public function onLoginSuccess(InteractiveLoginEvent $event)
{
$request = $event->getRequest();
if ($request->attributes->get('is_interactive_login', false)) {
// do whatever you need todo on interactive login
}
}
Better to subscribe to Events::JWT_CREATED event, as it's fired after authentication with credentials has been passed.
Example:
<?php
namespace App\Event\Subscriber;
use App\Entity\User\User;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class AuthenticationSuccessSubscriber implements EventSubscriberInterface
{
/**
* #var EntityManager
*/
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public static function getSubscribedEvents()
{
return [
Events::JWT_CREATED => 'onInteractiveLogin',
];
}
/**
* #param JWTCreatedEvent $event
*
* #throws \Doctrine\ORM\ORMException
* #throws \Doctrine\ORM\OptimisticLockException
*/
public function onInteractiveLogin(JWTCreatedEvent $event)
{
/** #var User $user */
$user = $event->getUser();
$user->setLastLoginAt(new \DateTime());
$user->resetFailedLogins();
$this->em->flush($user);
}
}
You should change this
stateless: false
I've faced with same trouble. In my case I use Guard authentication for API requests. So I definitely don't like updating user's last_login after any API request.
INTERACTIVE_LOGIN event is dispatched from here.
So my dirty hack is to add this definition to the services section of app's config:
security.authentication.guard_handler:
class: Symfony\Component\Security\Guard\GuardAuthenticatorHandler
arguments:
$eventDispatcher: ~
Warning
Obvious drawback of this approach is that you broke change handler for all your app's guards.
How to redirect the user to last page visited after login in Symfony with fosuserbundle?
In my controller, I check first if user is logged in. Then if he isn't, I redirect him to the login page. Here is the short code I used at the beginning of my controller.
$autenthicated = $this->checkAuth();
if($autenthicated==true){
return $this->render('MainBundle:Default:home.html.twig');
}else{
return $this->redirect($this->generateUrl('login_connect'));
}
The problem is that after the user has logged in, he is redirected to the main page instead the last page visited.
How should I redirect him to my custom login page? apparently this is not working:
return $this->redirect($this->generateUrl('login_connect'));
You don't have to handle the redirection to the login route by yourself.
If you throw an AccessDeniedException in your controller, the security component will redirect you to the login page (with the page you tried to access as parameter).
After login, the native LoginSuccessHandler will redirect you to the desired page.
You can also use the Controller shortcut :
$this->denyAccessUnlessGranted(['ROLE_USER']);// check your needed roles here
If you need to override the redirection logic after login, you can define a service and tell the security component to use this one to handle the redirection.
Here is a dummy example :
Your php service with redirection logic :
AppBundle/Security/LoginSuccessHandler.php
class LoginSuccessHandler implements AuthenticationSuccessHandlerInterface
{
// …
/**
* #param Request $request
* #param TokenInterface $token
* #return RedirectResponse
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
// your stuff
return new RedirectResponse($this->router->generate(‘custom_route’));
}
}
Service configuration :
# services.yml
app.login_success_handler:
public: false
class: AppBundle\Security\LoginSuccessHandler
arguments: ["#security.token_storage", "#router"]
Configure the security component to use your custom handler :
# security.yml
security:
firewalls:
main:
form_login:
success_handler: app.login_success_handler
Some users of my application will have an allowedIPs array attached. There was a guide for an authentication Voter for blacklisting IPs which I could adapt to whitelist a user's IP based on who is authenticating.
The problem I see here is a scenario where a user authenticates while in one allowed network, then switches to another network where the user is not allowed to connect from.
I think the solution would be to have a subscriber to the kernel.request event, where I deauthorize the user if the IP is not allowed.
Is this sort of IP checking on each request a stupid approach? If it's not, how do I get the authenticated user in the event subscriber? The GetResponseEvent (api docs) doesn't seem to give any method from which to get the authenticated user if one exists.
EDIT: As Cerad suggested, I did this with a voter.
Voter class
<?php
namespace My\UserBundle\Security;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Psr\Log\LoggerInterface;
use My\UserBundle\Entity\User;
class ValidClientIpVoter extends AuthenticatedVoter
{
private $container;
private $logger;
public function __construct(ContainerInterface $container, LoggerInterface $logger)
{
$this->container = $container;
$this->logger = $logger;
}
public function vote(TokenInterface $token, $object, array $attributes)
{
$request = $this->container->get('request');
$user = $token->getUser();
// vote on instances of our User class
if($user instanceof User) {
$allowed_ips = $user->getAllowedIps();
// only vote if there actually are limitations
if(is_array($allowed_ips) && count($allowed_ips)) {
$this->logger->debug(sprintf('ValidClientIpVoter: Validating allowed IPs for user #%d', $user->getId()));
// deny access if current request's IP is not allowed for the user
if(!in_array($request->getClientIp(), $allowed_ips)) {
$this->logger->notice(sprintf('ValidClientIpVoter: Invalid client IP for user #%d', $user->getId()));
return VoterInterface::ACCESS_DENIED;
}
}
}
return VoterInterface::ACCESS_ABSTAIN;
}
}
changes to security.yml to make voting unanimous
security:
access_decision_manager:
strategy: unanimous
and finally the service definition
services:
valid_client_ip_voter:
class: My\UserBundle\Security\ValidClientIpVoter
arguments: [#service_container, #monolog.logger]
public: false
tags:
- { name: security.voter }
To get the current user, inject the security.token_storage service into your listener: http://symfony.com/doc/current/book/security.html#retrieving-the-user-object
I would actually listen to kernel.controller just to make sure the user is available.
On the other hand, voters get called on each access to a resource so I am unclear why your existing approach would not work even if the ip changes.
I'm using a YAML configuration to wire my dependencies, and I need to provide some runtime information to get a useful object back. I was going to run a setter method from my code once the object has been injected, but I Was wondering if there was a better way of doing it (or if there's something I'm missing).
This is the gist of my configuration:
services:
example_object : "myObject"
arguments : ["%object_parameter1%"]
parameters:
object_parameter1 : Some Static Data
object_parameter2 : #Rutime info required
For retrieving the current logged in user in any service, inject the security.context. In this case I use setter injection to simply user mock injection.
namespace Acme\ExampleBundle\Foo;
use Symfony\Component\Security\Core\SecurityContextInterface;
class MyService
{
private $param;
private $user;
public function __construct($param)
{
$this->param = $param;
}
/**
* Retrieve the current logged in user from the security context.
*/
public function setUserFromContext(SecurityContextInterface $context)
{
$this->user = $context->getToken()->getUser();
}
/**
* Set any user object.
*
* Usefull for testing, to inject a simple user mock.
*/
public function setUser($user)
{
$this->user = $user;
}
public function doSomething()
{
// do something with the user object
}
}
Define the service:
services:
my_service:
class: Acme\ExampleBundle\Foo\MyService
arguments: ["%object_parameter1%"]
calls:
- [ setUserFromContext, [#security.context] ]
You should not try to add dynamic values directly into the DI configuration. Symfony services configuration is reflected by compiled DI container and recompilation is very heavy operation.
If you do not want to couple your service with Symfony's security system directly, you can add your custom "user provider" service as a dependency. Then you will need to rewrite this service if the source of information will change. It may be also easily mocked.
You can also use a factory to inject a user object instead of user provider service.