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 ?
Related
The admin user in my Symfony 4.2 application should be able to log out another (non-admin) user. I created a user login system depending on the Symfony security-bundle (https://symfony.com/doc/current/security/form_login_setup.html).
Now I am building an admin dashboard where all user have to be listed with their online status (last activity).
Is there a recommended way to list active users and kill their session if needed?
I've read some posts like this: Symfony how to return all logged in Active Users. But the answers are a little bit older and are just about listing the active users.
The correct way is to store the user session in the database.
https://symfony.com/doc/current/doctrine/pdo_session_storage.html (in here is the create syntax of the database table. Also add a user_id to the table)
in framework.yml add the Pdo Session Handler.
session:
handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler
cookie_secure: auto
cookie_samesite: lax
In service.yml add a listener and register the session handler
# Handlers
Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
arguments:
- !service { class: PDO, factory: 'database_connection:getWrappedConnection' }
- { lock_mode: 1 }
# Listeners
App\Listener\SessionListener:
tags:
- {name: kernel.event_listener, event: kernel.request, method: onRequestListener}
create a new listener in
class SessionListener
{
/**
* #var TokenStorageInterface
*/
private $tokenStorage;
/**
* #var EntityManagerInterface
*/
private $em;
/**
* #var SessionInterface
*/
private $session;
public function __construct(
TokenStorageInterface $tokenStorage,
EntityManagerInterface $em,
SessionInterface $session
) {
$this->tokenStorage = $tokenStorage;
$this->em = $em;
$this->session = $session;
}
public function onRequestListener(GetResponseEvent $event): void
{
// If its not te master request or token is null
if (!$event->isMasterRequest() || $this->tokenStorage->getToken() === null) {
return;
}
/** #var User $user */
$user = $this->tokenStorage->getToken()->getUser();
// Check if user is logged in
if (!$user instanceof User) {
return;
}
$connection = $this->em->getConnection();
try {
$stmt = $connection->prepare('UPDATE `sessions` SET `user_id` = :userId WHERE `sess_id` = :sessionId');
$stmt->execute([
'userId' => $user->getId(),
'sessionId' => $this->session->getId(),
]);
} catch (DBALException $e) {
}
}
}
Now just delete the sessions from this user.
/**
* #var EntityManagerInterface
*/
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function delete(User $user): void
{
$sessions = $this->em->getRepository(Session::class)->findBy([
'user' => $user,
]);
foreach ($sessions as $session) {
$this->em->remove($session);
}
$this->em->flush();
}
Here's a good way to kill user sessions:
use an EventListener with an onKernelRequest event. In your main code: public function onKernelRequest(KernelEvent $event)
$request = $event->getRequest();
$token = $this->container->get('security.token_storage')->getToken();
if ($token === null) { // somehow
return;
}
if ($token->getUser()->isLocked() === true) {
// you must implement a boolean flag on your user Entities, which the admins can set to false
$this->container->get('security.token_storage')->setToken(); // default is null, therefore null
$request->getSession()->invalidate(); // these lines will invalidate user session on next request
return;
}
Now, on to your other question: How to list users with their online status? Easy, your user Entities should implement another boolean flag, such as isOnline (with a getter and setter).
Next, you should create a LoginListener (no need to implement any interface). And in your main code:
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event) {
$user = $event->getAuthenticationToken()->getUser();
if ($user instanceof UserInterface) {
// set isOnline flag === true
// you will need to fetch the $user with the EntityManager ($this->em)
// make sure it exists, set the flag and then
$this->em->flush();
}
}
Your third event should be a LogoutListener, where you will set the isOnline flag === false
Symfony calls a LogoutListener (as a handler) when a user requests logout.
But you can write your own:
class LogoutListener implements LogoutHandlerInterface {
public function logout(Request $request, Response $response, TokenInterface $token): void
{
$user = $token->getUser();
if (!$user instanceof UserInterface) { /** return if user is somehow anonymous
* this should not happen here, unless... reasons */
return;
}
// else
$username = $user->getUsername(); // each user class must implement getUsername()
// get the entity Manager ($this->em, injected in your constructor)
// get your User repository
$repository = $this->em->getRepository(MyUser::class);
$user = $repository->findOneBy(['username' => $username]); // find one by username
$user->setIsOnline(false);
$this->em->flush(); // done, you've recorded a logout
}
}
Hope this helps. With a bit of luck, it will. Cheers! :-)
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!
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.
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.
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'));