Authenticate users via access token in Symfony 3 - php

The problem
I want users to be authenticated via an access token that is supplied as a GET parameter to the first request.
I have never implemented such a thing in Symfony, so I followed the steps outlined in How to Create a custom Authentication Provider, but it 'doesn't work'. The authenticate method of the AuthenticationProviderInterface is not triggered.
What I have tried
Because it is a lot of configuration mostly, I don't even know how to debug this. This is what I have concluded so far: Only the AccessTokenProvider gets constructed, nothing else.
The code
These are the relevant parts of the system:
security.yml
security:
# Snip default (empty) in_memory provider
firewalls:
# Snip dev and main (symfony default)
accesstoken_secured:
pattern: ^/admin/
accesstoken: true
services.yml
services:
accesstoken.security.authentication.provider:
class: AppBundle\Security\Authentication\Provider\AccessTokenProvider
arguments:
- '' # User Provider
- '%kernel.cache_dir%/security/nonces'
public: false
accesstoken.security.authentication.listener:
class: AppBundle\Security\Firewall\AccessTokenListener
arguments: ['#security.token_storage', '#security.authentication.manager']
public: false
AccessTokenFactory
class AccessTokenFactory implements SecurityFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$providerId = 'security.authentication.provider.accesstoken.'.$id;
$container
->setDefinition($providerId, new DefinitionDecorator('accesstoken.security.authentication.provider'))
->replaceArgument(0, new Reference($userProvider))
;
$listenerId = 'security.authentication.listener.accesstoken.'.$id;
$container->setDefinition($listenerId, new DefinitionDecorator('accesstoken.security.authentication.listener'));
return array($providerId, $listenerId, $defaultEntryPoint);
}
public function getPosition()
{
return 'pre_auth';
}
public function getKey()
{
return 'accesstoken';
}
public function addConfiguration(NodeDefinition $node)
{
}
}
AccessTokenProvider
class AccessTokenProvider implements AuthenticationProviderInterface
{
private $userProvider;
public function __construct(UserProviderInterface $userProvider)
{
$this->userProvider = $userProvider;
}
public function authenticate(TokenInterface $token)
{
$user = $this->userProvider->loadUserByAccessToken($token->getAttribute('token'));
if ($this->isTokenValid($token)) {
$authenticatedToken = new AccessToken(['role_user']);
$authenticatedToken->setUser($user);
return $authenticatedToken;
}
throw new AuthenticationException('The WSSE authentication failed.');
}
protected function isTokenValid(AccessToken $token)
{
//TODO: Implement
return (bool)$token->token;
}
public function supports(TokenInterface $token)
{
return $token instanceof AccessToken;
}
}
AccessTokenListener
class AccessTokenListener
{
protected $tokenStorage;
protected $authenticationManager;
/**
* AccessTokenListener constructor.
* #param TokenStorageInterface $tokenStorage
* #param AuthenticationManagerInterface $authenticationManager
*/
public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager)
{
$this->tokenStorage = $tokenStorage;
$this->authenticationManager = $authenticationManager;
}
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
$accesstoken = $request->get('accesstoken');
$token = new AccessToken();
$token->token = $accesstoken;
try {
$authToken = $this->authenticationManager->authenticate($token);
$this->tokenStorage->setToken($authToken);
return;
} catch (AuthenticationException $failed) {
// ... you might log something here
}
// By default deny authorization
$response = new Response();
$response->setStatusCode(Response::HTTP_FORBIDDEN);
$event->setResponse($response);
}
}
AccessToken
class AccessToken extends AbstractToken
{
public $token;
/**
* AccessToken constructor.
* #param array $roles
*/
public function __construct(array $roles = array())
{
parent::__construct($roles);
// If the user has roles, consider it authenticated
$this->setAuthenticated(count($roles) > 0);
}
/**
* Returns the user credentials.
*
* #return mixed The user credentials
*/
public function getCredentials()
{
return '';
}
}

I eventually tried to implement it in another way, using the tutorial at How to Create a Custom Authentication System with Guard.
This uses Symfony's new Guard system. It was actually very easy to setup!

Related

Anon. in Custom User Provider

I want to manage my users without Database, and check their JWT
I use Guard and a Custom Service to Validate my JWT.
I Want to use even a custom user provider, And If user doesn't have JWT I want to redirect to a new page or call an exception
this is my code
class WebserviceUserProvider implements UserProviderInterface {
/**
* #var \Symfony\Component\Routing\RouterInterface
*/
private $router;
private $requestStack;
/**
* WebserviceUserProvider constructor.
* #param RequestStack $router
*/
public function __construct($router,RequestStack $requestStack) {
$this->router = $router;
$this->requestStack = $requestStack;
}
public function refreshUser(UserInterface $user) {
if (!$user instanceof User) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', get_class($user))
);
}
return $this->loadUserByUsername($user->getUsername());
}
public function loadUserByUsername($username) {
if (!$token = $this->requestStack->getCurrentRequest()->headers->get('Authorization') ?: $this->requestStack->getCurrentRequest()->query->get('token')) {
throw new UsernameNotFoundException('Could not find user. Sorry!');
}
$user = new \AppBundle\Entity\User('MyUsername');
$user->setJwtToken($token);
return $user;
}
public function supportsClass($class) {
return $class === 'AppBundle\Security\User\WebserviceUser';
}
}
And this is my service.yml
app.webservice_user_provider:
class: AppBundle\Security\WebserviceUserProvider
arguments: ['#router','#request_stack']
And My security.yml
user-providers
providers:
webservice:
id: app.webservice_user_provider
When I visit a url with token in queryString
http://127.0.0.1:8000/app_dev.php/?token=my.token.jwt
My User is logged.
When visit
http://127.0.0.1:8000/app_dev.php/
I have an user anon. ( My token is set to Anon. ) and I don't have an exception
If I change loadUserByUsername with
public function loadUserByUsername($username) {
if (!$token = $this->requestStack->getCurrentRequest()->headers->get('Authorization') ?: $this->requestStack->getCurrentRequest()->query->get('token')) {
$url = $this->router->generate('errorPage');
return new RedirectResponse($url);
}
$user = new \AppBundle\Entity\User('MyUsername');
$user->setJwtToken($token);
return $user;
}
I have this error
Attempted to call an undefined method named "getUsername" of class
"Symfony\Component\HttpFoundation\RedirectResponse". 500 Internal
Server Error - UndefinedMethodException
How can I redirect to a page if I dont'have a JWT ( or is not valid ), or have an exception ?

symfony2 access denied exception

i'm working on symfony2 project and i get this exception. anybody have an idea on what is causing it ?
Uncaught exception 'Symfony\Component\Security\Core\Exception\AccessDeniedException' with message 'Access Denied' in /data/apache/www/emploipublic-sf/vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall/AccessListener.php:70\n
class AccessListener implements ListenerInterface
{
private $context;
private $accessDecisionManager;
private $map;
private $authManager;
private $logger;
public function __construct(SecurityContextInterface $context, AccessDecisionManagerInterface $accessDecisionManager, AccessMapInterface $map, AuthenticationManagerInterface $authManager, LoggerInterface $logger = null)
{
$this->context = $context;
$this->accessDecisionManager = $accessDecisionManager;
$this->map = $map;
$this->authManager = $authManager;
$this->logger = $logger;
}
/**
* Handles access authorization.
*
* #param GetResponseEvent $event A GetResponseEvent instance
*/
public function handle(GetResponseEvent $event)
{
if (null === $token = $this->context->getToken()) {
throw new AuthenticationCredentialsNotFoundException('A Token was not found in the SecurityContext.');
}
$request = $event->getRequest();
list($attributes, $channel) = $this->map->getPatterns($request);
if (null === $attributes) {
return;
}
if (!$token->isAuthenticated()) {
$token = $this->authManager->authenticate($token);
$this->context->setToken($token);
}
if (!$this->accessDecisionManager->decide($token, $attributes, $request)) {
throw new AccessDeniedException(); // this is line 70
}
}
}
Look at your security.yml file (app/config/security.yml).
You may have some secure path which you do not have access to. Check out
security -> access_control
section.

Symfony2 custom Authenticator do something when not authenticated

How to manage Full authentication is required to access this resource.?
I want to redirect user when he is not authenticated.
I have custom uthenticater which authenticate user depending on session data, and i want to redirect user when hes not authenticatet.
My authenticator class:
/**
* #Service("sso_authenticator")
*/
class SsoAuthenticator implements SimplePreAuthenticatorInterface
{
/**
* #var SsoUserProvider
*/
protected $userProvider;
/**
* #InjectParams({
* "userProvider" = #Inject("sso_user_provider")
* })
*/
public function __construct(SsoUserProvider $userProvider)
{
$this->userProvider = $userProvider;
}
public function createToken(Request $request, $providerKey)
{
$user = $request->getSession()->get('sso_user');
if (!$user) {
throw new BadCredentialsException('No user found');
}
return new PreAuthenticatedToken(
'anon.', $user, $providerKey
);
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
$user = $token->getCredentials();
if (!is_array($user)) {
$user = $token->getUser();
}
if (!$user) {
throw new AuthenticationException('User does not exist.');
}
$ssoUser = $this->userProvider->loadUser($user);
return new PreAuthenticatedToken(
$ssoUser, $user, $providerKey, $ssoUser->getRoles()
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
}
i set the login path to logout path like this:
secured_area:
form_login:
login_path : main_user_logout
then i wrote custom logout handler:
/**
* #Service("sso_authentication_handler")
*/
class SsoAuthenticationHandler implements LogoutSuccessHandlerInterface
{
/**
* #var Router
*/
private $router;
/**
* #var array
*/
protected $ssoUrls;
/**
* #InjectParams({
* "ssoUrls" = #Inject("%wordpress_sso%"),
* "router" = #Inject("router")
* })
*/
public function __construct(array $ssoUrls, Router $router)
{
$this->ssoUrls = $ssoUrls;
$this->router = $router;
}
public function onLogoutSuccess(Request $request)
{
$locale = $request->getLocale();
if ($locale === 'pl') {
$url = $this->ssoUrls[$locale];
} else {
$url = $this->ssoUrls['en'];
}
$url .= '?returnUrl=' . $this->router->generate('main');
return new RedirectResponse($url);
}
}
so with this combination i achive behavior like when youser is not authenticated or when he logout i will redirect him to other site to login, in my example to wordpress.

getting authenticated user onAuthenticationSuccess

I have the following authentication handler:
class LoginAuthSuccessHandler implements AuthenticationSuccessHandlerInterface, AuthenticationFailureHandlerInterface
{
private $router;
private $container;
/**
* Constructor
* #param RouterInterface $router
*/
public function __construct(RouterInterface $router, $container)
{
$this->router = $router;
$this->container = $container;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
if ($request->isXmlHttpRequest()) {
$user = $this->container->get('security.context')->getToken()->getUser();
$result = array('success' => true, 'user' => $user);
return new Response(json_encode($result));
} else {
$route = $this->router->generate('ShopiousMainBundle_profile');
$referrer_url = $request->server->get('HTTP_REFERER');
if (strstr($referrer_url, '/items/')) {
$route = $referrer_url;
}
return new RedirectResponse($route);
}
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
if ($request->isXmlHttpRequest()) {
$result = array('success' => false, 'message' => $exception->getMessage());
return new Response(json_encode($result));
} else {
// Handle non XmlHttp request here
}
}
}
why is it that:
$user = $this->container->get('security.context')->getToken()->getUser();
returns null? how do i get the authenticated user at this point?
You should use the $token variable you are receiving as an argument instead of $this->container->get('security.context')->getToken().
$user = $token->getUser();
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
$user = $token->getUser();
}
that's all ;)
maibe you don't set your provider in security.yml.
security:
provider:
example:
entity: {class Acme\AuctionBundle\Entity\User, property: username}
Replace the Bundle and the entity by yours.
The only way I found was to inject the entityManager,
calls:
- [setEntityManager,[#doctrine.orm.entity_manager]]
get the username from the request and query for the user, using that username.
$userRepo->findByEmail($request->get('_username'));

How to redirect to different url based on roles in symfony 2

I have one login page on site.
I have 4 different tye of users and i want that when they login they go to different page based on their role assigned.
Is there any way?
One way to solve this is to use an event listener on the security.interactive_login event. In this case I simply attach another listener in that event listener so it will fire on the response. This lets the authentication still happen but still perform a redirect once complete.
<service id="sotb_core.listener.login" class="SOTB\CoreBundle\EventListener\SecurityListener" scope="request">
<tag name="kernel.event_listener" event="security.interactive_login" method="onSecurityInteractiveLogin"/>
<argument type="service" id="router"/>
<argument type="service" id="security.context"/>
<argument type="service" id="event_dispatcher"/>
</service>
And the class...
class SecurityListener
{
protected $router;
protected $security;
protected $dispatcher;
public function __construct(Router $router, SecurityContext $security, EventDispatcher $dispatcher)
{
$this->router = $router;
$this->security = $security;
$this->dispatcher = $dispatcher;
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
$this->dispatcher->addListener(KernelEvents::RESPONSE, array($this, 'onKernelResponse'));
}
public function onKernelResponse(FilterResponseEvent $event)
{
if ($this->security->isGranted('ROLE_TEAM')) {
$response = new RedirectResponse($this->router->generate('team_homepage'));
} elseif ($this->security->isGranted('ROLE_VENDOR')) {
$response = new RedirectResponse($this->router->generate('vendor_homepage'));
} else {
$response = new RedirectResponse($this->router->generate('homepage'));
}
$event->setResponse($response);
}
}
For Symfony >= 2.6 now would be:
<?php
namespace CommonBundle\Listener;
use Monolog\Logger;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Router;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
class LoginListener
{
/** #var Router */
protected $router;
/** #var TokenStorage */
protected $token;
/** #var EventDispatcherInterface */
protected $dispatcher;
/** #var Logger */
protected $logger;
/**
* #param Router $router
* #param TokenStorage $token
* #param EventDispatcherInterface $dispatcher
* #param Logger $logger
*/
public function __construct(Router $router, TokenStorage $token, EventDispatcherInterface $dispatcher, Logger $logger)
{
$this->router = $router;
$this->token = $token;
$this->dispatcher = $dispatcher;
$this->logger = $logger;
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
$this->dispatcher->addListener(KernelEvents::RESPONSE, [$this, 'onKernelResponse']);
}
public function onKernelResponse(FilterResponseEvent $event)
{
$roles = $this->token->getToken()->getRoles();
$rolesTab = array_map(function($role){
return $role->getRole();
}, $roles);
$this->logger->info(var_export($rolesTab, true));
if (in_array('ROLE_ADMIN', $rolesTab) || in_array('ROLE_SUPER_ADMIN', $rolesTab)) {
$route = $this->router->generate('backend_homepage');
} elseif (in_array('ROLE_CLIENT', $rolesTab)) {
$route = $this->router->generate('frontend_homepage');
} else {
$route = $this->router->generate('portal_homepage');
}
$event->getResponse()->headers->set('Location', $route);
}
}
And services.yml
services:
common.listener.login:
class: CommonBundle\Listener\LoginListener
arguments: [#router, #security.token_storage, #event_dispatcher, #logger]
scope: request
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: onSecurityInteractiveLogin }
Tested in Symfony 3.1
You could also set default path after user login successfully for all users in security.yml file like so:
[config/security.yml]
...
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: /.*
form_login:
login_path: /login
check_path: /login_check
default_target_path: /login/redirect <<<<<<<<<<<<<<<<<<<<<<<<<
logout:
path: /logout
target: /
security: true
anonymous: ~
...
and then in default_target_path method make simple redirection based on user role. Very straight forward. Some say that the easiest way is always the best way. You decide :)
[SomeBundle/Controller/SomeController.php]
/**
* Redirect users after login based on the granted ROLE
* #Route("/login/redirect", name="_login_redirect")
*/
public function loginRedirectAction(Request $request)
{
if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY'))
{
return $this->redirectToRoute('_login');
// throw $this->createAccessDeniedException();
}
if($this->get('security.authorization_checker')->isGranted('ROLE_ADMIN'))
{
return $this->redirectToRoute('_admin_panel');
}
else if($this->get('security.authorization_checker')->isGranted('ROLE_USER'))
{
return $this->redirectToRoute('_user_panel');
}
else
{
return $this->redirectToRoute('_login');
}
}
Works like a charm but keep in mind to always check for most restricted roles downwards in case your ROLE_ADMIN also has privileges of ROLE_USER and so on...
I used Mdrollette answer but this solution has a big drawback, you completely override the symfony original response and by doing this remove the remember me cookie that was set in the header by symfony.
my solution was to change the OnKernelResponse this way :
public function onKernelResponse(FilterResponseEvent $event)
{
if ($this->security->isGranted('ROLE_TEAM')) {
$event->getResponse()->headers->set('Location', $this->router->generate('team_homepage'));
} elseif ($this->security->isGranted('ROLE_VENDOR')) {
$event->getResponse()->headers->set('Location', $this->router->generate('vendor_homepage'));
} else {
$event->getResponse()->headers->set('Location', $this->router->generate('homepage'));
}
}
This way you remain the remember me cookie intact.
If you are looking for a simpler answer than #MDrollette, you could put a similar redirect block into the controller of your login success page.
For the sake of testing, if you're wanting to to preserve the original response you could also just copy the headers. The clone method on the Redirect object only copies the headers.
public function onKernelResponse(FilterResponseEvent $event)
{
if ($this->security->isGranted('ROLE_TEAM')) {
$response = new RedirectResponse($this->router->generate('team_homepage'));
} elseif ($this->security->isGranted('ROLE_VENDOR')) {
$response = new RedirectResponse($this->router->generate('vendor_homepage'));
} else {
$response = new RedirectResponse($this->router->generate('homepage'));
}
$response->headers = $response->headers + $event->getResponse()->headers;
$event->setResponse($response);
}
I used this in the login Form authenticator to redirect user based on role (symfony :
4.26.8) :
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
private $urlGenerator;
/**
* #var Security
*/
private $security;
public function __construct(UrlGeneratorInterface $urlGenerator ,Security $security)
{
$this->urlGenerator = $urlGenerator;
$this->security = $security;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// redirecting user by role :
$user = $this->security->getUser();
$roles = $user->getRoles();
$rolesTab = array_map(function($role){
return $role;
}, $roles);
if (in_array('ROLE_ADMIN', $rolesTab) || in_array('ROLE_SUPER_ADMIN', $rolesTab)) {
return new RedirectResponse($this->urlGenerator->generate('admin'));
}
else{
return new RedirectResponse($this->urlGenerator->generate('home'));
}
}

Categories