I use FriendsOfSymfony UserBundle. It works when I set everything in firewall as form_login, but if I set it to simple_form to use Custom Authenticator then it let's me login even if the account is locked or disabled. I want to check if the user comes from the correct IP, that's why I created the custom authenticator, but it seems that some authentication coming from FOS is not processed this way. How can I use simple_form with custom authenticator while still keeping full functionality of FOS UserBundle?
Is there some other way I can achieve some other authentication than just the standard? Maybe I'm doing something wrong? I know I can correct this code of my authenticator to check for locked/enabled etc, but I figured - since it's actually already done in FOS - why should I?
EDIT: Also, I noticed that when I use simple_form the methods of class Symfony\Component\Security\Core\User\UserChecker aren't being called.
Below is my code of authenticator and security.yml:
config.yml
services:
login_authenticator:
class: Forex\AlchemyBundle\Security\LoginAuthenticator
arguments: ["#security.encoder_factory"]
security.yml
security:
encoders:
FOS\UserBundle\Model\UserInterface: sha512
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
firewalls:
main:
pattern: ^/
simple_form:
authenticator: login_authenticator
provider: fos_userbundle
csrf_provider: form.csrf_provider
logout: true
anonymous: true
access_control:
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY } # To be removed
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/, role: ROLE_ADMIN }
- { path: ^/.*, roles: ROLE_USER }
LoginAuthenticator
<?php
namespace Forex\AlchemyBundle\Security;
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\EncoderFactoryInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class LoginAuthenticator implements SimpleFormAuthenticatorInterface
{
private $encoderFactory;
public function __construct(EncoderFactoryInterface $encoderFactory)
{
$this->encoderFactory = $encoderFactory;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
try {
$user = $userProvider->loadUserByUsername($token->getUsername());
} catch (UsernameNotFoundException $e) {
throw new AuthenticationException('Invalid username or password');
}
$encoder = $this->encoderFactory->getEncoder($user);
$passwordValid = $encoder->isPasswordValid(
$user->getPassword(),
$token->getCredentials(),
$user->getSalt()
);
if ($passwordValid) {
$request = Request::createFromGlobals();
$current_ip = $request->server->get('REMOTE_ADDR');
$user->setLoggedIP($current_ip);
if (!$user->isValidIP()) {
throw new AuthenticationException(
"You cannot login from your location.",
100
);
}
return new UsernamePasswordToken(
$user,
$user->getPassword(),
$providerKey,
$user->getRoles()
);
} else {
// TODO: Check if there weren't too many tries to login
}
throw new AuthenticationException('Invalid username or password');
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof UsernamePasswordToken
&& $token->getProviderKey() === $providerKey;
}
public function createToken(Request $request, $username, $password, $providerKey)
{
return new UsernamePasswordToken($username, $password, $providerKey);
}
}
I was facing the same issue. Here is how I solved it.
You must call checkPreAuth() and checkPostAuth() methods of your UserChecker class from within the authenticateToken() method in your Authenticator.
That is done this way:
1) Configure the user_checker service:
services:
app.user_checker:
class: AppBundle\Security\UserChecker
2) Configure the autenticator as a service and inject the user_checker service:
services:
app.my_authenticator:
class: AppBundle\Security\MyAuthenticator
arguments: ["#app.user_checker", "#security.password_encoder"]
3) Now you can call checkPreAuth() and checkPostAuth() on authenticateToken()
Anyway I think symfony approach is the right one, because in my case y needed to perform different checks in simple_form than in login_form.
Related
I'm using LexikJWTAuthenticationBundle to manage the token creation and all that jazz.
My security.yaml file looks like this(removed not relevant stuff):
security:
encoders:
App\Entity\User:
algorithm: auto
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
login:
pattern: ^/api/login
stateless: true
anonymous: true
json_login:
check_path: /api/login_check
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api:
pattern: ^/api
stateless: true
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: lazy
guard:
authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: logout
target: /
invalidate_session: true
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: /
always_remember_me: true
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/profile, roles: ROLE_USER }
- { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/user, roles: ROLE_USER }
- { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
As far as JWT goes it works fine
Doing a simple curl
$ curl -X POST -H "Content-Type: application/json" http://localhost/api/\login_check -d '{"username":"someusername","password":"somepassword!"}'
Returns a token:
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 910 100 852 100 58 376 25 0:00:02 0:00:02 --:--:-- 376
{"token":"6wvL6E..."}
Problem is, every other route responds with a 302 the immediately redirects to /
Request URL: http://localhost/register
Request Method: GET
Status Code: 302 Found
Remote Address: [::1]:80
Referrer Policy: no-referrer-when-downgrade
It's pretty clear that I've missconfigured something somewhere. But I can't seem to find. As far as I'm concerned the configs seem to be good.
Perhaps somebody with a better eye(and better experience) than myself can give me a hand.
I can only assume JWT expects X route to have a token, but, I'm clearly whitelisting certain paths in access_control.
Removing the bundle and reverting any changes will make all routes behave as expected.
Upon further investigation it seems to collide with a custom LoginFormAuthenticator
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
use TargetPathTrait;
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $passwordEncoder;
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request)
{
return 'login_user' === $request->attributes->get('_route') && $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Email could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
/**
* Used to upgrade (rehash) the user's password automatically over time.
*/
public function getPassword($credentials): ?string
{
return $credentials['password'];
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('index', [], RouterInterface::ABSOLUTE_URL));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate('login_user');
}
}
Why exactly this happens I'm not sure since they guards are in separate firewalls with clearly distinct routes. Not even the same prefix as far as I can tell.
So in your config you have the main firewall with the custom LoginFormAuthenticator, in which I do not see the whole point when you do not need an authenticator for the login when you are using LexikJWTAuthenticationBundle.
Also I noticed is that if you make a request for example to http://localhost/register it would use your LoginFormAuthenticator, which will not support the request because in its supports() function you declared to accept certain route.
return 'login_user' === $request->attributes->get('_route') ....
Another thing you can troubleshoot is your onAuthenticationSuccess function, especially I have suspicion about this line.
return new RedirectResponse($this->urlGenerator->generate('index', [], RouterInterface::ABSOLUTE_URL));
I would assume that you have a route,which is named index and has path "/" and if you use routes.yaml it would look something like this. (You have the same principle in the other types too)
index:
path: /
controller: App\Controller\ACMEController::fooFunction
So basically you are telling your authenticator to redirect to "/". That would be my other guess.
I hope I was helpful.
I have a Symfony 3.3.13 system with various forms.
To achieve "deep-linking" in these forms, ie. being able to click on an email link, login and then be redirected to the form I have added the following changes:
config.yml
framework:
secret: "%secret%"
router:
resource: "%kernel.root_dir%/config/routing.yml"
strict_requirements: ~
form: ~
csrf_protection: ~
...
more
...
security.yml
security:
providers:
zog:
id: app.zog_user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
logout:
path: /logout
target: /
guard:
authenticators:
- app.legacy_token_authenticator
- app.token_authenticator
entry_point: app.legacy_token_authenticator
form_login: <--this line alone breaks CSRF
use_referer: true <--I tried partial combinations, none seems to make CSRF work
login_path: /security/login
use_forward: true
success_handler: login_handler
csrf_token_generator: security.csrf.token_manager <--added based on answer, doesn't help
src/AppBundle/Resources/config/services.yml
login_handler:
class: AppBundle\Service\LoginHandler
arguments: ['#router', '#doctrine.orm.entity_manager', '#service_container']
src/AppBundle/Service/Loginhandler.php
<?php
/**
* Created by PhpStorm.
* User: jochen
* Date: 11/12/17
* Time: 12:31 PM
*/
namespace AppBundle\Service;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Routing\RouterInterface;
use Doctrine\ORM\EntityManager;
class LoginHandler implements AuthenticationSuccessHandlerInterface
{
private $router;
private $container;
private static $key;
public function __construct(RouterInterface $router, EntityManager $em, $container) {
self::$key = '_security.main.target_path';
$this->router = $router;
$this->em = $em;
$this->session = $container->get('session');
}
public function onAuthenticationSuccess( Request $request, TokenInterface $token ) {
//check if the referer session key has been set
if ($this->session->has( self::$key )) {
//set the url based on the link they were trying to access before being authenticated
$route = $this->session->get( self::$key );
//remove the session key
$this->session->remove( self::$key );
//if the referer key was never set, redirect to a default route
return new RedirectResponse($route);
} else{
$url = $this->generateUrl('portal_job_index');
return new RedirectResponse($url);
}
}
}
I have also made sure that csrf is enabled on the login form like this:
src/AppBundle/resources/views/security/login.html.twig
<form action="{{ path('app_security_login') }}" method="post" autocomplete="off">
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}"
>
app/config/services.yml
app.legacy_token_authenticator:
class: AppBundle\Security\LegacyTokenAuthenticator
arguments: ["#router", "#session", "%kernel.environment%", "#security.csrf.token_manager"]
src/AppBundle/Security\legacyTokenAuthenticator
class LegacyTokenAuthenticator extends AbstractGuardAuthenticator
{
private $session;
private $router;
private $csrfTokenManager;
public function __construct(
RouterInterface $router,
SessionInterface $session,
$environment,
CsrfTokenManagerInterface $csrfTokenManager
) {
if ($environment != 'test'){
session_start();
}
$session->start();
$this->setSession($session);
$this->csrfTokenManager = $csrfTokenManager;
$this->router = $router;
}
/**
* #return mixed
*/
public function getSession()
{
return $this->session;
}
/**
* #param mixed $session
*/
public function setSession($session)
{
$this->session = $session;
}
/**
* Called on every request. Return whatever credentials you want,
* or null to stop authentication.
*/
public function getCredentials(Request $request)
{
$csrfToken = $request->request->get('_csrf_token');
if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken('authenticate', $csrfToken))) {
throw new InvalidCsrfTokenException('Invalid CSRF token.');
}
$session = $this->getSession();
if (isset($_SESSION['ADMIN_logged_in']) && intval($_SESSION['ADMIN_logged_in'])){
return $_SESSION['ADMIN_logged_in'];
}
return;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
return $userProvider->loadUserByUserId($credentials);
}
public function checkCredentials($credentials, UserInterface $user)
{
return $user->getUsername() == $credentials;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return null;
}
/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$url = $this->router->generate('app_security_login');
return new RedirectResponse($url);
}
public function supportsRememberMe()
{
return false;
}
}
All CSRF checks - including on the login form - fail always when I add the 5 lines in security.yml starting with form_login. The error I get is:
The CSRF token is invalid. Please try to resubmit the form. portalbundle_portal_job
Caused by:
When I remove these 5 lines, all CSRF tokens work.
Here is a security.yml file I have from one of my projects which has csrf protection enabled. I do use the FOS UserBundle, which looks to be different from yours, but you might be able to see something here that helps. Specifically, a csrf generator has to be specified to use FOS UserBundle (under firewalls: main: form_login). I also have access_control patterns setup so that some endpoints are only accessible if a user is authenticated with a specific role -- but I don't think this will affect csrf. See below:
security:
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
providers:
fos_userbundle:
id: fos_user.user_provider.username
firewalls:
main:
pattern: ^/
form_login:
provider: fos_userbundle
csrf_token_generator: security.csrf.token_manager
logout: true
anonymous: true
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/, role: ROLE_ADMIN }
- { path: ^/event, role: ROLE_USER }
Also in my main config.yml I've enabled csrf under framework. Here's a snip of the whole thing:
framework:
#esi: ~
translator: { fallbacks: ["%locale%"] }
secret: "%secret%"
router:
resource: "%kernel.root_dir%/config/routing.yml"
strict_requirements: ~
form: ~
csrf_protection: ~
For me, manually dealing with Symfony CSRF tokens is a major headcache. Short of doing it just to learn how to do it, I think there is almost always a simpler solution.
My solution for CSRF protection in a login does not run into this issue.
I create the form login using the Form component.
function loginAction()
{
$login = $this->createForm(LoginType::class);
$authenticationUtils = $this->get('security.authentication_utils');
$error = $authenticationUtils->getLastAuthenticationError();
return $this->render('Path/to/login.html.twig', [
'form' => $login->createView(),
'error' => $error,
]);
}
LoginType.php:
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
//...
class LoginType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('username', TextType::class)
->add('password', PasswordType::class)
;
}
}
login.html.twig:
{# template information #}
{{ form_start(form) }}
{{ form_row(form.username, {
'full_name': '_username'
} ) }}
{{ form_row(form.password, {
'full_name': '_password'
} ) }}
{{ form_end(form) }}
{# template information #}
security.yml:
security:
providers:
zog:
id: app.zog_user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
logout:
path: /logout
target: /
form_login:
use_referer: true
login_path: /security/login
success_handler: login_handler
always_use_default_target_path: false
default_target_path: /
If you have CSRF enabled on forms, then your login form will be CSRF protected without any custom Guard authenticator.
First a short explanation of my task. I'm using Symfony 2.8 and have an application with REST API and SonataAdminBundle. Visitors of the website can post certain data over the REST API that is persisted to the database. A certain group of employees should manage those data through the admin area.
The access to the admin area should be protected with username and password. There is entity Employee with a property username, but no password. The authentication should be done against the LDAP server, but the access to admin area should be restricted only to those employees that are present in the entity Employee i.e. the referring database table.
For the LDAP authentication I am using the new LDAP component in Symfony 2.8.
Beyond that there should be an admin account as in_memory user.
This is what I have now:
app/config/services.yml
services:
app.ldap:
class: Symfony\Component\Ldap\LdapClient
arguments: ["ldaps://ldap.uni-rostock.de"]
app.db_user_provider:
class: AppBundle\Security\DbUserProvider
arguments: ["#doctrine.orm.entity_manager"]
app/config/security.yml
security:
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
chain_provider:
chain:
providers: [db_user, app_users]
in_memory:
memory:
users:
admin: { password: adminpass, roles: 'ROLE_ADMIN' }
app_users:
ldap:
service: app.ldap
base_dn: ou=people,o=uni-rostock,c=de
search_dn: uid=testuser,ou=people,o=uni-rostock,c=de
search_password: testpass
filter: (uid={username})
default_roles: ROLE_USER
db_user:
id: app.db_user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
admin:
anonymous: true
pattern: ^/
form_login_ldap:
provider: chain_provider
service: app.ldap
dn_string: "uid={username},ou=people,o=uni-rostock,c=de"
check_path: /login_check
login_path: /login
form_login:
provider: in_memory
check_path: /login_check
login_path: /login
logout:
path: /logout
target: /
access_control:
- { path: ^/admin, roles: ROLE_USER }
encoders:
Symfony\Component\Security\Core\User\User: plaintext
AppBundle\Entity\Employee: bcrypt
src/AppBundle/Entity/Employee.php
namespace AppBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Doctrine\ORM\Mapping as ORM;
class Employee implements UserInterface, EquatableInterface
{
// other properties
private $username;
// getters and setters for the other properties
public function getUsername()
{
return $this->username;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function getPassword()
{
return null;
}
public function getSalt()
{
return null;
}
public function eraseCredentials()
{
}
public function isEqualTo(UserInterface $user)
{
if (!$user instanceof Employee) {
return false;
}
if ($this->username !== $user->getUsername()) {
return false;
}
return true;
}
}
src/AppBundle/Security/DbUserProvider.php
<?php
namespace AppBundle\Security;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Doctrine\ORM\EntityManager;
use AppBundle\Entity\Employee;
class DbUserProvider implements UserProviderInterface
{
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function loadUserByUsername($username)
{
$repository = $this->em->getRepository('AppBundle:Employee');
$user = $repository->findOneByUsername($username);
if ($user) {
return new Employee();
}
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof Employee) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', get_class($user))
);
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return $class === 'AppBundle\Entity\Employee';
}
}
The authentication against LDAP works like a charm. When an employee from the database is trying to login, he/she is redirected to the homepage('/') and the login is failed. All other users, who are not in the database, can login without a problem.
That is exactly the opposite of what I want!
If I chain the providers like this:
chain_provider:
chain:
providers: [app_users, db_user]
then the method loadUserByUsername is not even called and all users can login, those who are in the database and those who are not.
The in_memory user admin can login without a problem in any case.
I appreciate any help. If someone thinks that my entire approach is bad and knows a better way, please don't spare with critics.
I know there is FOSUserBundle and SonataUserBundle, but I would prefer a custom user provider as I don't want to bloat the entity Employee, since I really don't need all those properties like password, salt, isLocked, etc. Also I don't think configuring SonataUserBundle in my particular case would be much simpler. Should you still think there is a more elegant way to fulfill my task with these two bundles, I'll be grateful to get a good advice.
you can configure user-checker per firewall, your db user provider isn't really a user-provider because it doesn't have all the information that's needed to authenticate a user (e.g password)
so what i would do is , i would remove the db user provider and add a user checker instead, the main idea of the user checker is to add additional checks during the authentication process , in your case we need to check if the user is in the employee table or not
you need to do three things to implement this
implement UserCheckerInterface Symfony\Component\Security\Core\User\UserCheckerInterface
check if the user is in the employee table or not in checkPostAuth() method
expose your new user-checker as a service
services:
app.employee_user_checker:
class: Path\To\Class\EmployeeUserChecker
change your firewall config to use the new user-checker
security:
firewalls:
admin:
pattern: ^/admin
user_checker: app.employee_user_checker
#...
Read more
I have looked everywhere, and tried everything, but my redirect still doesn't want to work.
I have done option two of this question's answer but the wanted results doesn't appear.
This is my overwritten login function (inside /src/Acme/UserBundle/Controller/SecurityController.php);
class SecurityController extends BaseController
{
public function loginAction(Request $request)
{
/** #var $session \Symfony\Component\HttpFoundation\Session\Session */
$session = $request->getSession();
$authChecker = $this->container->get('security.authorization_checker');
$router = $this->container->get('router');
if ($authChecker->isGranted('ROLE_ADMIN')) {
return new RedirectResponse($router->generate('admin_home'), 307);
}
if ($authChecker->isGranted('ROLE_USER')) {
return new RedirectResponse($router->generate('user_home'), 307);
}
...
...
}
}
I have my AcmeUserBundle.php inside /src/Acme/UserBundle/ containing the following code;
namespace Acme\UserBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AcmeUserBundle extends Bundle
{
public function getParent()
{
return 'FOSUserBundle';
}
}
Then I have my main application bundle AppBundle next to Acme inside /src/ with my DefaultController.php inside /Controller/ and my admin controller as DefaultController.php inside /Controller/Admin/
When I log in with a user with role ROLE_USER, it goes to the correct location. But when I log in with role ROLE_ADMIN he goes to the same location as ROLE_USER. It is suppose to go to the admin page.
This is my security.yml file;
security:
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
providers:
fos_userbundle:
id: fos_user.user_provider.username
firewalls:
main:
pattern: ^/
form_login:
provider: fos_userbundle
csrf_token_generator: security.csrf.token_manager
logout: true
anonymous: true
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, role: ROLE_USER }
- { path: ^/admin, role: ROLE_ADMIN }
In my project's profiler the user role for a normally registered user state ROLE_USER. I modified a user's role in my database to be ROLE_ADMIN and when I log in with him both roles shows to be assigned to him. Under the role column in my database this is how it looks for a user with ROLE_USER assigned to him :
a:0:{}
and this is how I modified the other user to have ROLE_ADMIN:
a:1:{i:0;s:10:"ROLE_ADMIN";}
Is this right? Can somebody please help me to fix this problem?
NOTE: When I sign in with a ROLE_USER user and I modify the url to go to /admin/ I get a "Expression "has_role('ROLE_ADMIN')" denied access." error. I get that, that's fine because I don't want a normal user to access that area. But when I login with a ROLE_ADMIN user and modify the url to /admin/ it does go to that page, so I can see by that that my ROLES are working fine and the routing as well, but the redirect on login not.
Clearing my cache also didn't do the trick.
This is how I did it...
// AppBundle\Security\LoginSuccessHandler.php
namespace AppBundle\Security;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Router;
class LoginSuccessHandler implements AuthenticationSuccessHandlerInterface {
protected $router;
protected $authorizationChecker;
public function __construct(Router $router, AuthorizationChecker $authorizationChecker) {
$this->router = $router;
$this->authorizationChecker = $authorizationChecker;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token) {
$response = null;
if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
$response = new RedirectResponse($this->router->generate('admin'));
}
return $response;
}
}
Then add this to your services.yml file:
authentication.handler.login_success_handler:
class: AppBundle\Security\LoginSuccessHandler
arguments: ['#router', '#security.authorization_checker']
I can't remember if there were other steps.. Pretty sure the services file entry takes care of making sure that class gets called.
There is a little known success_handler option in form_login (at least), here you can find a gist with example usage suiting your needs just perfectly.
I'm actually trying to create a login form matching with my database.
The form works well but I've a problem using a UserRepository. Symfony gives me the following error:
The user provider must return a UserInterface object.
exception 'Symfony\Component\Security\Core\Exception\AuthenticationServiceException' with message 'The user provider must return a UserInterface object.' in C:\wamp\www\php\Promocast\Symfony\vendor\symfony\src\Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider.php:101
But as you can see below, my user entity is implemented by UserInterface so I really don't understand what's the mistake here.
My user entity:
<?php
namespace Promocast\UtilisateurBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;
/**
* Promocast\UtilisateurBundle\Entity\ImUser
*
* #ORM\Entity(repositoryClass="Promocast\UtilisateurBundle\Entity\ImUserRepository")
*/
class ImUser implements UserInterface
{
/**
* Here all my var and getter/setter
* ...
*/
/**
* Fonctions UserInterface
*
*/
public function eraseCredentials()
{
}
public function getRoles()
{
return $this->idrole;
}
public function equals(UserInterface $user)
{
return $user->getUsername() === $this->login;
}
public function getUsername()
{
return $this->login;
}
public function getSalt()
{
return '';
}
}
My user repository:
<?php
namespace Promocast\UtilisateurBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NoResultException;
class ImUserRepository extends EntityRepository implements UserProviderInterface
{
public function loadUserByUsername($login)
{
$user = $this->findBy(array("login" => $login));
if (!$user) {
throw new UsernameNotFoundException(sprintf('No user with name "%s" was found.', $login));
}
return $user;
}
public function refreshUser(UserInterface $user)
{
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return $class === 'Promocast\UtilisateurBundle\Entity\ImUser';
}
}
And my security.yml:
security:
encoders:
Promocast\UtilisateurBundle\Entity\ImUser: plaintext
#Promocast\UtilisateurBundle\Security\Provider\LoginWebService: plaintext
role_hierarchy:
ROLE_USER_SUP: ROLE_USER
ROLE_ADMIN: ROLE_USER_SUP
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_USER_SUP, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
main:
entity: { class: PromocastUtilisateurBundle:ImUser }
#entity: { class: Promocast\UtilisateurBundle\Entity\ImUser, property: login }
#id: webservice_user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/(login$|register|resetting)
anonymous: true
main:
pattern: ^/
form_login:
login_path: /login
check_path: /login_check
username_parameter: _login
password_parameter: _password
remember_me:
key: %secret%
anonymous: true
provider: main
logout: true
logout:
path: /logout
target: /
access_control:
#- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
#- { path: ^/_internal, roles: IS_AUTHENTICATED_ANONYMOUSLY, ip: 127.0.0.1 }
#- { path: /.*, role: ROLE_USER }
#- { path: /login, role: IS_AUTHENTICATED_ANONYMOUSLY }
Thanks a lot!
It looks like your problem is here:
public function loadUserByUsername($login)
{
$user = $this->findBy(array("login" => $login));
if (!$user) {
throw new UsernameNotFoundException(sprintf('No user with name "%s" was found.', $login));
}
return $user;
}
$this->findBy() is probably returning a cursor/resultset rather than one row as you intended (even if the resultset contains only one row).
Try just using $this->findOneBy() instead.
Based on your code, it looks like you are following the cookbook tutorial on How to load Security Users from the Database (the Entity Provider) - but if you're not, that is a helpful resource in this case.
Caveat: I'm far from an expert on the S2 security system. I'm just posting what works for me.
There is nothing in your config file that tells the system to use ImUserRepository as a user provider. So the system is basically trying to use a default provider. Furthermore, using a repository is problematical at best. Doctrine repos can only come from Doctrine entity managers so you asking the system to say hey my user class is a doctrine entity therefore I should use a repository and I guess I'll use this entity manager. Not going to happen.
Make your user provider a service and then inject the entity manager into it.
security:
providers:
my_provider:
id: zayso_core.user.provider
<service id="zayso_core.user.provider" class="Zayso\CoreBundle\Component\User\UserProvider" public="true">
<argument type="service" id="doctrine.orm.accounts_entity_manager" />
<argument type="string">%zayso_core.user.class%</argument>
</service>
You will probably also want to encode your passwords eventually so:
security:
encoders:
Zayso\AreaBundle\Component\User\User:
id: zayso_core.user.encoder
<service id="zayso_core.user.encoder" class="Zayso\CoreBundle\Component\User\Encoder" public="true">
</service>
And that should get you going.