How to extend authentication in OroCRM - symfony

Good day, everyone.
I need to extend authentication mechanism for my needs.
To do this i created Custom Form Password Authenticator
1) I changed firewall settings
main:
...
#organization-form-login:
simple_form:
authenticator: my_authenticator
csrf_provider: form.csrf_provider
check_path: oro_user_security_check
login_path: oro_user_security_login
...
2) I created service for my_authenticator
services:
...
my_authenticator:
class: OQ\SecurityBundle\Security\MyAuthenticator
arguments:
- #oro_organization.organization_manager
...
3) And here is the code of MyAuthenticator
namespace OQ\SecurityBundle\Security;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\SimpleFormAuthenticatorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
Use Oro\Bundle\SecurityBundle\Authentication\Token\UsernamePasswordOrganizationToken;
use Oro\Bundle\OrganizationBundle\Entity\Manager\OrganizationManager;
class MyAuthenticator implements SimpleFormAuthenticatorInterface
{
/** #var OrganizationManager */
protected $manager;
public function __construct(OrganizationManager $manager)
{
$this->manager = $manager;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
// Here will be my special checks
//Here i try to get username and force authentication
try {
$user = $userProvider->loadUserByUsername($token->getUsername());
} catch (UsernameNotFoundException $e) {
throw new AuthenticationException('This user not allowed');
}
// If everythin' is ok - create a token
if ($user) {
return new UsernamePasswordOrganizationToken(
$user,
$user->getPassword(),
$providerKey,
$this->manager->getOrganizationById(1)
);
} else {
throw new AuthenticationException('Invalid username or password');
}
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof UsernamePasswordOrganizationToken
&& $token->getProviderKey() === $providerKey;
}
public function createToken(Request $request, $username, $password, $providerKey)
{
//UsernamePasswordOrganizationToken
return new UsernamePasswordOrganizationToken($username, $password, $providerKey, $this->manager->getOrganizationById(1));
}
}
When i try to authenticate user - i succesfully log in, but i dont see anything except black header and profiler. Profiler says me, that i'm logged as USER_NAME (yellow color), and not authenticated (red color).
Can you give me an advice - how to make t work?
And one more question - how can i retrieve user's organization in this authenticator class?

If you check UsernamePasswordToken constructor you'll see it requires you to pass $roles in order to make it authenticated
parent::setAuthenticated(count($roles) > 0);
And it's impossible to change authenticate flag after in setAuthenticated (see the code why).
Check also UserAuthenticationProvider class to get an idea what's happening.
I hope this helps.

Related

Symfony 3.4 : How to force de-authentication

My security user is fully authenticated with some roles got from some system. I want to check if one of the Roles exists and if it does not, I want to force de-authentication of the user.
In my event listener on the login I do this :
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class SecurityEventSubscriber implements EventSubscriberInterface {
private $token;
private $checker;
private $container;
private $session;
public function __construct(TokenStorageInterface $token, AuthorizationCheckerInterface $checker, ContainerInterface $container, SessionInterface $session) {
$this->token = $token;
$this->checker = $checker;
$this->container = $container;
$this->session = $session;
}
public function login() {
if(!$this->checker->isGranted('IS_AUTHENTICATED_FULLY')) {
$this->session->invalidate();
$this->token->setToken(null);
throw new AccessDeniedException();
} else {
$user = $this->token->getToken()->getUser();
$roles = $user->getRoles();
$found = false;
foreach ($roles as $role) {
if($role->getRole() === $this->container->getParameter('role_expected')) {
$found = true;
break;
}
}
if(!$found) {
$this->session->invalidate();
$this->token->setToken(null);
throw new AccessDeniedException();
} else {
$user->removeAllRoles();
}
}
}
}
As you can see I tried to use the setToken to null but it does not work (Exception).
HGow should I ask to de-authenticate the user ?
Instead of checking the user permissions in your controller, you could create a custom User Checker that could deny the authentication based on your custom logic.
Sample User Checker
namespace AppBundle\Security;
use AppBundle\Security\User as AppUser;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user)
{
if (!$user instanceof AppUser) {
return;
}
if (!in_array('SOME_ROLE', $user->getRoles())) {
// throw an AccountStatusException exception here
}
}
}
If you also want to run a check against the user roles after user has been logged in (if his roles could change during his session) you can use the checkPostAuth() method.
You also have to mention the use of your custom User Checker in your app/config/security.yml file.
security:
firewalls:
main:
pattern: ^/
user_checker: AppBundle\Security\UserChecker
More informations here
The easiest way is to redirect your user to your logout route.
Sadly there does not seem to be a dedicated method that will simply handle the whole logout process for you.
Except maybe if you manage to call this method in a valid instance of Symfony\Component\Security\Http\Firewall\LogoutListener.
Symfony logout works like that:
It is handled by this listener
Listener is called on each request
Listener checks if requested route === logout route
If requested route === logout route, listener logs out the user

Symfony route access check

I have a website made with Symfony 3.4 and within my actions I must check if the current user can edit the target product, something like this:
/**
* #Route("/products/{id}/edit")
*/
public function editAction(Request $request, Product $product)
{
// security
$user = $this->getUser();
if ($user != $product->getUser()) {
throw $this->createAccessDeniedException();
}
// ...
}
How can I avoid making the same check on every action (bonus points if using annotations and expressions)?
I am already using security.yml with access_control to deny access based on roles.
You can use Voters for this exact purpose. No magic involved. After creating and registering the Voter authentication will be done automatically in the security layer.
You just have to create the Voter class and then register it as a service. But if you're using the default services.yaml configuration, registering it as a service is done automatically for you!
Here is an example you can use. You may have to change a few items but this is basically it.
To read more visit: https://symfony.com/doc/current/security/voters.html
<?php
namespace AppBundle\Security;
use AppBundle\Entity\Product;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use AppBundle\Entity\User;
class ProductVoter extends Voter
{
const EDIT = 'EDIT_USER_PRODUCT';
protected function supports($attribute, $subject)
{
if($attribute !== self::EDIT) {
return false;
}
if(!$subject instanceof Product) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
/** #var Product $product */
$product= $subject;
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
return $this->belongsToUser($product, $user);
}
private function belongsToUser(Product $product, User $user)
{
return $user->getId() === $product->getUser()->getId();
}
}
You could try with a listener:
Check the action name,for example, if it is "edit_product", them continue.
Get the current logged User.
Get the user of the product entity.
Check if current user is different to Product user, if it is true, throw CreateAccessDeniedException.
services.yml
app.user.listener:
class: AppBundle\EventListener\ValidateUserListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
arguments: ["#service_container", "#doctrine.orm.entity_manager"]
Edit Action:
Added name "edit_product" to the action.
/**
*
* #Route("/products/{id}/edit",name="edit_product")
*/
public function editAction()
{
...
src\AppBundle\EventListener\ValidateUserListener.php
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class ValidateUserListener
{
private $container;
private $entityManager;
public function __construct($container, $entityManager)
{
$this->container = $container;
$this->entityManager = $entityManager;
}
public function onKernelRequest(GetResponseEvent $event)
{
$currentRoute = $event->getRequest()->attributes->get('_route');
if($currentRoute=='edit_product' || $currentRoute=='edit_item' )
{
$array_user = $this->getCurrentUser();
if($array_user['is_auth'])
{
$current_user = $array_user['current_user'];
$product = $this->entityManager->getRepository('AppBundle:User')->findOneByUsername($current_user);
$product_user = $product->getUsername();
if ($current_user !==$product_user)
{
throw $this->createAccessDeniedException();
}
}
}
}
private function getCurrentUser()
{
//Get the current logged User
$user = $this->container->get('security.token_storage')->getToken()->getUser();
if(null!=$user)
{
//If user is authenticated
$isauth = $this->container->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY');
return array('is_auth'=>$isauth, 'current_user'=>$user);
}
return array('is_auth'=>false, 'current_user'=>$user);
}
}
Tested in Symfony 3.3

symfony3 guard login form doesn't authenticate [duplicate]

This question already has an answer here:
Symfony & Guard: "The security token was removed due to an AccountStatusException"
(1 answer)
Closed 5 years ago.
I try to make a form login authentication with guard (symfony 3.2) but it doesn't work.
The authentication is working, but when I'm redirected to the home page (accueil), I'm redirected to the login page without anthentication.
If I put in the controler of my home page
$user = $this->get('security.token_storage')->getToken();
dump($user); die;
I can see my user, the role but he is not authenticated.
DashboardController.php on line 23:
PostAuthenticationGuardToken {#133 ▼
-providerKey: "main"
-user: User {#457 ▶}
-roles: array:1 [▼
0 => Role {#120 ▼
-role: "ROLE_SUPERADMIN"
}
]
-authenticated: false
-attributes: []
}
What I've missed ?
Security.ym
security:
encoders:
EntBundle\Entity\User\User:
algorithm: bcrypt
providers:
database:
entity:
class: EntBundle:User\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: ~
logout: ~
guard:
authenticators:
- ent.login_authenticator
TestAuthenticator.php
namespace EntBundle\Security;
use Doctrine\ORM\EntityManager;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
class TestAuthenticator extends AbstractGuardAuthenticator
{
private $em;
private $router;
public function __construct(EntityManager $em, RouterInterface $router)
{
$this->em = $em;
$this->router = $router;
}
public function getCredentials(Request $request)
{
if ($request->getPathInfo() != '/login' || !$request->isMethod('POST')) {
return;
}
return [
'username' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
];
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials['username'];
return $this->em->getRepository('EntBundle:User\User')->findOneBy(['username' => $username]);
}
public function checkCredentials($credentials, UserInterface $user)
{
// this is just for test
return true;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
$url = $this->router->generate('login');
return new RedirectResponse($url);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$url = $this->router->generate('accueil');
return new RedirectResponse($url);
}
public function start(Request $request, AuthenticationException $authException = null)
{
$url = $this->router->generate('login');
return new RedirectResponse($url);
}
public function supportsRememberMe()
{
return false;
}
}
DashboardController.php
namespace EntBundle\Controller\Dashboard;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class DashboardController extends Controller
{
/**
* #Route("/accueil", name="accueil")
*/
public function indexAction()
{
$user = $this->get('security.token_storage')->getToken();
dump($user); die;
return $this->render('EntBundle:dashboard:dashboard_structure.html.twig');
}
/**
* #Route("/login", name="login")
*/
public function loginAction()
{
$authenticationUtils = $this->get('security.authentication_utils');
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('EntBundle::login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
/**
* #Route("/logout", name="logout")
*/
public function logoutAction()
{
}
}
EDIT:
Thanks leo_ap for your help but the problem doesnt come from there.
The config session is like this :
session:
handler_id: session.handler.native_file
save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%"
and if I check in the save path folder I have session file created but not authenticated.
_sf2_attributes|a:1:{s:26:"_security.main.target_path";s:29:"http://localhost:8000/accueil";}_sf2_flashes|a:0:{}_sf2_meta|a:3:{s:1:"u";i:1488245179;s:1:"c";i:1488244922;s:1:"l";s:1:"0";}
If I try the normal login_form with security.yml it's working fine...
I've try with handler_id and save_path at null with no success.
EDIT2:
I've found why I'm always redirected to the login page, because I'm logged out!
[2017-02-28 09:16:34] security.INFO: The security token was removed due to an AccountStatusException. {"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\AuthenticationExpiredException(code: 0): at /home/philippe/Documents/symfony/vendor/symfony/symfony/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php:86)"}
and in GuardAuthenticationProvider.php (86)
The listener *only* passes PreAuthenticationGuardToken instances.
This means that an authenticated token (e.g.PostAuthenticationGuardToken)
is being passed here, which happens if that token becomes "not authenticated" (e.g. happens if the user changes between requests).
In this case, the user should be logged out, so we will return an AnonymousToken to accomplish that.
But Why ???
May be your Session that isn't persisting the token. Check your Session configuration, inside: config.yml. in the framework option, there is session. See how the handler_id and save_path are configured. It may be that your php instalation is unable to handle the sessions on the configured path. Try to put null to handler_id and save_path to force php use its own build in configurations to handle sessions.
config.yml file:
framework:
{ .. Other configurations ..}
session:
handler_id: null
save_path: null
{ .. More configurations ..}

Symfony2. Simple preauth not working after deploying on hosting, because it try to authenticate one more time after success authentication

In my Symfony application I have two firewalls:
main:
pattern: ^/(?!login).+
stateless: true
simple_preauth:
authenticator: app_bundle.api_key_authenticator
provider: api_key_user_provider
anonymous: ~
logout: ~
login:
pattern: ^/login$
stateless: false
simple_preauth:
authenticator: app_bundle.email_password_authenticator
provider: email_user_provider
anonymous: ~
logout: ~
After authenticating through /login User getting apiKey, which allow get access to another part of app.
On my local server all works fine. But after deploying on hosting authentication not working.
After debugging I see, that email authenticator try to authenticate twice.
So first time it returns correct Token, but second time it returns NULL, because loadUserByUsername() method get username instead of email (authentication is possible only through email).
This is my email authenticator:
<?php
namespace AppBundle\Security\Authentication;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\SimplePreAuthenticatorInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
class EmailPasswordAuthenticator implements SimplePreAuthenticatorInterface
{
private $encoder;
public function __construct(UserPasswordEncoderInterface $encoder)
{
$this->encoder = $encoder;
}
public function createToken(Request $request, $providerKey)
{
if ($request->get('email') && $request->get('password')) {
return new UsernamePasswordToken(
$request->get('email'),
$request->get('password'),
$providerKey
);
}
throw new AuthenticationException('Authentication failed');
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
try {
$user = $userProvider->loadUserByUsername($token->getUsername());
} catch (UsernameNotFoundException $e) {
throw new AuthenticationException('Invalid username or password');
}
$passwordValid = $this->encoder->isPasswordValid($user, $token->getCredentials());
if ($passwordValid) {
return new UsernamePasswordToken(
$user,
$user->getPassword(),
$providerKey,
$user->getRoles()
);
}
throw new AuthenticationException('Password incorrect');
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof UsernamePasswordToken
&& $token->getProviderKey() === $providerKey;
}
}
This is my User provider:
<?php
namespace AppBundle\Security\User;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use AppBundle\Repository\UserRepository;
use Symfony\Component\Security\Core\User\UserInterface;
class EmailUserProvider implements UserProviderInterface
{
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function loadUserByUsername($email)
{
$user = $this->userRepository->findOneBy(array('email' => $email));
if (null === $user) {
throw new UsernameNotFoundException(
sprintf(
'User with email "%s" not found',
$email
)
);
}
return $user;
}
public function refreshUser(UserInterface $user)
{
return $this->loadUserByUsername($user->getEmail());
}
public function supportsClass($class)
{
return $class === 'AppBundle\Entity\User';
}
}
Please, help me to understand, what's going on. What the reason of this weird behavior it can be?
Thanks a lot for any help!
UPD. As I can see, authenticate() method calls first time by Symfony\Component\Security\Http\Firewall\SimplePreAuthenticationListener, second time - by Symfony\Component\Security\Http\Firewall\AccessListener.
Problem in this part of code:
// AccessListener
if (!$token->isAuthenticated()) {
$token = $this->authManager->authenticate($token);
$this->tokenStorage->setToken($token);
}
Solved. Problem was in my groups and roles tables, them was empty and without data from them User cannot be authenticated.

allow only one connection on the same login with FOSUserBundle

I'm creating a website thanks to Symfony2 with FOSUserBundle.
I'm triyng to deny multiple connections on the same login (but from different computers for example).
I've 2 solutions :
Create an event listner on authentification but I didn't manage to make it. (even with the cookbook).
override the login_check method but my FOSUserBundle doesn't work if I do it.
Do you have any better options?
Or any solutions?
Got it finaly. There is just one last update to make to solve it all.
You need to add an other field to the User entity. sessionId (string).
Then update your LoginListener class like that :
// YourSite\UserBundle\Listener\YourSiteLoginListener.php
//...
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
$request = $event->getRequest();
$session = $request->getSession();
$user = $event->getAuthenticationToken()->getUser();
$has_session = is_file ( '/path_to_your_php_session_file/'.'sess_'.$user->getSessionId() );
if($user->getLogged() && $has_session){
throw new AuthenticationException('this user is already logged');
}else{
$user->setLogged(true);
$user->setSessionId($session->getId());
$this->userManager->updateUser($user);
}
}
Maybe this will help people to solve this problem.
It's kind of a solution but there is still a problem :
If the user session is killed by php (after too mush time without action for example), you will have to go into your database to reset the "logged" value to 0.
So my solution is :
-add the field "logged" (boolean) to you User entity.
-in YourSite\UserBundle\Listener create a : YourSiteLoginListener.php with this code
namespace YourSite\UserBundle\Listener;
use FOS\UserBundle\Model\UserManagerInterface;
use FOS\UserBundle\Model\UserInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\SecurityContext;
class YourSiteLoginListener
{
private $userManager;
public function __construct(UserManagerInterface $userManager)
{
$this->userManager = $userManager;
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
if($user->getLogged()){
throw new AuthenticationException('this user is already logged');
}else{
$user->setLogged(true);
$this->userManager->updateUser($user);
}
}
}
-then in the same directory, create a logout handler : YourSiteLogoutHandler.php
namespace YourSite\UserBundle\Listener;
use FOS\UserBundle\Model\UserManagerInterface;
use FOS\UserBundle\Model\UserInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
class YourSiteLogoutHandler implements LogoutHandlerInterface
{
private $userManager;
public function __construct(UserManagerInterface $userManager)
{
$this->userManager = $userManager;
}
public function logout (Request $request, Response $response, TokenInterface $token){
$user = $token->getUser();
if($user->getLogged()){
$user->setLogged(false);
$this->userManager->updateUser($user);
}
}
}
-finaly declare those services in your app/config.yml for example:
services:
yoursite_login_listener:
class: YourSite\UserBundle\Listener\YourSiteLoginListener
arguments: [#fos_user.user_manager]
tags:
- { name: kernel.event_listener, event: security.interactive_login, method :onSecurityInteractiveLogin }
yoursite_logout_handler:
class: YourSite\UserBundle\Listener\YourSiteLogoutHandler
arguments: [#fos_user.user_manager]
In Symfony3, the logout handler was not trigged by the code above.
I rebuild the code so the system is updated when the user is logging out.
namespace YourSite\UserBundle\Listener;
use FOS\UserBundle\Model\UserManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface;
class LogoutSuccessHandler implements LogoutSuccessHandlerInterface
{
private $userManager;
public function __construct(UserManagerInterface $userManager)
{
$this->userManager = $userManager;
}
public function onLogoutSuccess(Request $request){
global $kernel;
$user = $kernel->getContainer()->get('security.token_storage')->getToken()->getUser();
if($user->getLogged()){
$user->setLogged(false);
$this->userManager->updateUser($user);
}
$referer = $request->headers->get('referer');
return new RedirectResponse($referer);
}
}

Resources