I am trying to configure my Symfony2.4 application to use a custom authenticator to check a database table to protect against brute force login attempts and I am running into a problem where when a user gives the correct credentials, they are re-directed back to the login screen instead of to their given URL. Here is my security.yml file:
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
Acme\FakeBundle\Entity\User: sha512
Acme\FakeBundle\Entity\User: sha512
role_hierarchy:
ROLE_VENDOR: ROLE_USER
ROLE_STANDARD: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_STANDARD, ROLE_ALLOWED_TO_SWITCH]
providers:
users:
id: my_custom_user_provider
firewalls:
assets_firewall:
pattern: ^/(_(profiler|wdt)|css|images|js|media|img)/
security: false
registration_area:
pattern: ^(/register|/register/details|/register/success)$
security: false
unsecured_area:
pattern: ^(/login(?!_check$))|^(?!support).privacy|^(?!support).terms_and_conditions
security: false
secured_area:
pattern: ^/
simple_form:
authenticator: my_custom_authenticator
check_path: /login_check
login_path: /login
username_parameter: form[_username]
password_parameter: form[_password]
csrf_parameter: form[_token]
logout:
path: /logout
target: /login
access_control:
- { path: ^/, roles: IS_AUTHENTICATED_FULLY, requires_channel: %force_channel% }
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel:%force_channel%}
Here is my custom User Provider:
<?php
namespace Acme\FakeBundle\Services;
use Doctrine\ORM\NoResultException;
use Acme\FakeBundle\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
class AcmeFakeUserProvider implements UserProviderInterface
{
/**
* Holds the Doctrine entity manager for database interaction
* #var EntityManager
*/
protected $em;
/**
* Fake bundle User entity repository
* #var EntityRepository
*/
protected $user_repo;
/**
* Fake bundle FloodTableEntry repository
* #var EntityRepository
*/
protected $flood_table_repo;
protected $container;
/**
* #var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
public function __construct(EntityManager $em, ContainerInterface $container)
{
$this->em = $em;
$this->user_repo = $this->em->getRepository("AcmeFakeBundle:User");
$this->flood_table_repo = $this->em->getRepository('AcmeFakeBundle:FloodTableEntry');
$this->container = $container;
$this->request = $this->container->get('request');
}
/**
* #return User
*/
public function loadUserByUsername($username)
{
$q = $this->user_repo
->createQueryBuilder('u')
->where('LOWER(u.username) = :username OR u.email = :email')
->setParameter('username', strtolower($username))
->setParameter('email', $username)
->getQuery();
try {
/*
* Verify that the user has not tried to log in more than 5 times in the last 5 minutes for
* the same username or from the same IP Address. If so, block them from logging in and notify
* them that they must wait a few minutes before trying again.
*/
$qb2 = $this->flood_table_repo->createQueryBuilder('f');
$entries = $qb2
->where($qb2->expr()->eq('f.ipAddress', ':ipAddress'))
->andWhere($qb2->expr()->gte('f.attemptTime', ':fiveMinsAgo'))
->setParameters(
array(
'fiveMinsAgo' => date('o-m-d H:i:s',time() - 5 * 60),
'ipAddress' => $this->request->getClientIp(),
)
)->getQuery()
->getResult();
if (count($entries) >= 10) {
throw new AuthenticationException("Too many unsuccessful login attempts. Try again in a few minutes.");
}
// The Query::getSingleResult() method throws an exception
// if there is no record matching the criteria.
$user = $q->getSingleResult();
} catch (NoResultException $e) {
$message = sprintf(
'Unable to find an active admin AcmeFakeBundle:User object identified by "%s".',
$username
);
throw new UsernameNotFoundException($message, 0, $e);
}
return $user;
}
/**
* #return User
*/
public function refreshUser(UserInterface $user)
{
$class = get_class($user);
if (!$this->supportsClass($class)) {
throw new UnsupportedUserException(
sprintf(
'Instances of "%s" are not supported.',
$class
)
);
}
return $this->user_repo->find($user->getId());
}
public function supportsClass($class)
{
return 'Acme\FakeBundle\Entity\User' === $class
|| is_subclass_of($class, 'Acme\FakeBundle\Entity\User');
}
}
And finally, here is the custom authenticator:
<?php
namespace Acme\FakeBundle\Services;
use Acme\FakeBundle\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Doctrine\ORM\EntityManager;
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 AcmeFakeAuthenticator implements SimpleFormAuthenticatorInterface
{
private $container;
private $encoderFactory;
/**
* #var \Acme\FakeBundle\Services\FloodTableManager
*/
protected $floodManager;
/**
* Holds the Doctrine entity manager for database interaction
* #var EntityManager
*/
protected $em;
/**
* #var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
public function __construct(ContainerInterface $container, EncoderFactoryInterface $encoderFactory)
{
$this->container = $container;
$this->encoderFactory = $encoderFactory;
$this->floodManager = $this->container->get('acme.fakebundle.floodtable');
$this->em = $this->container->get('doctrine.orm.fakebundle_entity_manager');
$this->request = $this->container->get('request');
}
public function createToken(Request $request, $username, $password, $providerKey)
{
return new UsernamePasswordToken($username, $password, $providerKey);
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
try {
$user = $userProvider->loadUserByUsername($token->getUsername());
} catch (UsernameNotFoundException $e) {
$this->floodManager->addLoginFailureToFloodTable($token->getUsername(), $this->request->getClientIp());
$this->floodManager->trimFloodTable();
throw new AuthenticationException('Invalid username or password');
}
$passwordValid = $this->encoderFactory
->getEncoder($user)
->isPasswordValid(
$user->getPassword(),
$token->getCredentials(),
$user->getSalt()
);
if ($passwordValid) {
// If User is not active, throw appropriate exception
$status = $user->getStatus();
if (!$status == User::USER_ACTIVE) {
// If User's account is waiting on available seats, print this message:
if ($status == User::USER_PENDING_SEAT) {
throw new AuthenticationException("Account pending activation");
} else {
// Otherwise, User's account is inactive, print this error message.
throw new AuthenticationException("Account inactive");
}
}
return new UsernamePasswordToken(
$user,
$user->getPassword(),
$providerKey,
$user->getRoles()
);
}
$this->floodManager->addLoginFailureToFloodTable($user->getUsername(), $this->request->getClientIp());
$this->floodManager->trimFloodTable();
throw new AuthenticationException('Invalid username or password');
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof UsernamePasswordToken && $token->getProviderKey() === $providerKey;
}
}
When a user gives incorrect login credentials it is handled correctly (i.e. the correct AuthenticationException is thrown with the correct message). However, as mentioned above, if the correct credentials are given then the user simply stays on the login page with no error message being shown.
I think I found the answer the problem is your regex in unsecured_area.^/login_(?!check$)
does match "login_check".the dollar sign should be after the parantheses as in (?!_check)$.What's currently happening is the login_check path falls under the unsecured_area firewall and the token isn't set for the context of Secured_area.Actually I don't think it's kept anywhere since security: false for unsecured_area.read up on the firewall context in http://symfony.com/doc/current/book/security.html#book-security-common-pitfalls
Related
I'm trying to grant roles to a user according to their LDAP dn when they log in through the LDAP.
To do that, I'd like to override the loadUser method from Symfony\Component\Security\Core\User\LdapUserProvider, but I don't really know how to proceed as, if I understand correctly (I'm quite new at using Symfony :p), it's not a service, but part of one?
So, is there a way to override that method easily, or do I need to redefine the whole Ldap service?
What I've tried is:
// app/config/services.yml
[...]
Symfony\Component\Ldap\Ldap:
arguments: ['#Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
Symfony\Component\Security\Core\User\LdapUserProvider:
class: AppBundle\Services\LdapUserProvider
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
arguments:
- host: '%ldap_host%'
port: '%ldap_port%'
// src/Services/LdapUserProvider.php
namespace AppBundle\Services;
use Symfony\Component\Ldap\Entry;
class LdapUserProvider extends \Symfony\Component\Security\Core\User\LdapUserProvider {
protected function loadUser($username, Entry $entry)
{
$password = null;
if (null !== $this->passwordAttribute) {
$password = $this->getAttributeValue($entry, $this->passwordAttribute);
}
return new User($username, $password, array('ROLE_TEST'));
}
}
But of course it doesn't work and I don't get the ROLE_TEST role.
Thanks by advance!
You can use this tutorial:
https://medium.com/#devstan/extended-ldap-with-symfony-3-30be6f1a36b1
and you should create a LdapUser class that implements UserInterface
1.LDAP user provider
<?php
namespace YourAppBundle\Security\User;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Security\Core\User\LdapUserProvider as BaseLdapUserProvider;
class LdapUserProvider extends BaseLdapUserProvider
{
/**
* {#inheritdoc}
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof LdapUser) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
return new LdapUser($user->getUsername(), null, $user->getRoles(), $user->getLdapEntry());
}
/**
* {#inheritdoc}
*/
public function supportsClass($class)
{
return $class === 'YourAppBundle\Security\User\LdapUser';
}
/**
* {#inheritdoc}
*/
protected function loadUser($username, Entry $entry)
{
$user = parent::loadUser($username, $entry);
return new LdapUser($username, $user->getPassword(), $user->getRoles(), $entry);
}
}
2.LDAP user
<?php
namespace YourAppBundle\Security\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Ldap\Entry;
class LdapUser implements UserInterface
{
const LDAP_KEY_DISPLAY_NAME = 'displayName';
const LDAP_KEY_MAIL = 'mail';
protected $username;
protected $password;
protected $roles;
protected $ldapEntry;
protected $displayName;
protected $eMail;
public function __construct($username, $password, array $roles, Entry $ldapEntry)
{
if ('' === $username || null === $username) {
throw new \InvalidArgumentException('The username cannot be empty.');
}
$this->username = $username;
$this->password = $password;
$this->roles = $roles;
$this->ldapEntry = $ldapEntry;
$this->displayName = $this->extractSingleValueByKeyFromEntry(
$ldapEntry,
self::LDAP_KEY_DISPLAY_NAME,
$username
);
$this->eMail = $this->extractSingleValueByKeyFromEntry($ldapEntry, self::LDAP_KEY_MAIL);
}
public function __toString()
{
return (string) $this->getUsername();
}
public function getDisplayName()
{
return $this->displayName;
}
/**
* #return Entry
*/
public function getLdapEntry()
{
return $this->ldapEntry;
}
/**
* {#inheritdoc}
*/
public function getRoles()
{
return $this->roles;
}
/**
* {#inheritdoc}
*/
public function getPassword()
{
return $this->password;
}
/**
* {#inheritdoc}
*/
public function getSalt()
{
}
/**
* {#inheritdoc}
*/
public function getUsername()
{
return $this->username;
}
/**
* {#inheritdoc}
*/
public function eraseCredentials()
{
}
/**
* Extracts single value from entry's array value by key.
*
* #param Entry $entry Ldap entry
* #param string $key Key
* #param null|string $defaultValue Default value
*
* #return string|null
*/
protected function extractSingleValueByKeyFromEntry(Entry $entry, $key, $defaultValue = null)
{
$value = $this->extractFromLdapEntry($entry, $key, $defaultValue);
return is_array($value) && isset($value[0]) ? $value[0] : $defaultValue;
}
/**
* Extracts value from entry by key.
*
* #param Entry $entry Ldap entry
* #param string $key Key
* #param mixed $defaultValue Default value
*
* #return array|mixed
*/
protected function extractFromLdapEntry(Entry $entry, $key, $defaultValue = null)
{
if (!$entry->hasAttribute($key)) {
return $defaultValue;
}
return $entry->getAttribute($key);
}
}
3.Services.yml
we need to define our newly created ldap user provider service
services:
...
app.ldap:
class: Symfony\Component\Ldap\Ldap
factory: ['Symfony\Component\Ldap\Ldap', 'create']
arguments:
- 'ext_ldap'
- host: '%ldap_host%'
app.ext_ldap_user_provider:
class: YourAppBundle\Security\User\LdapUserProvider
arguments:
- '#app.ldap' # LDAP component instance
- '%ldap_base_dn%' # Base dn
- '%ldap_search_dn%' # Search dn
- '%ldap_search_password%' # Search user password
- ['ROLE_SUPER_ADMIN'] # Roles
- '%ldap_uid_key%' # LDAP uid key
- '%ldap_filter%' # filter
4.Security.yml
Here is just an example usage of our provider — the “/ui” will be secured with LDAP.
security:
encoders:
...
YourAppBundle\Security\User\LdapUser:
algorithm: bcrypt
cost: 12
providers:
...
ldap_users:
id: app.ext_ldap_user_provider
...
firewalls:
frontend:
anonymous: ~
provider: ldap_users
form_login_ldap:
service: app.ldap
dn_string: '%ldap_dn_string%'
login_path: front-login
check_path: front-login
default_target_path: /ui
logout:
path: front-logout
target: front-login
access_control:
...
- { path: ^/ui/logout, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/ui/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/ui, roles: IS_AUTHENTICATED_FULLY }
I'm in trouble with authentication with symfony 4 on my first REST API.
The fact is my authentication succeed, and then my redirect URL is called, but the authentication token is lost during this redirection. I've also noticed that my serialize method is never called on my User Entity.
What I want is : When my Authentication is succeeded, then my profile page is called.
But with that code, all I get is a 302 redirection from profile, means that my authentication works, but the token was lost (if it exist, never seen it)
My only hints are :
Serialize method in User never called (is this important ?) EDIT : no because i need to be stateless, so remove those methods.
My Authentication works because if I make a mistake in credential I got a correct error.
Here is the code :
My Provider
<?php
declare(strict_types = 1);
namespace App\Api\Auth\Provider;
use App\Api\User\Entity\User;
use App\Api\User\Repository\UserRepository;
use App\Domain\User\ValueObject\Email;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class AuthProvider implements UserProviderInterface
{
/**
* #var \App\Api\User\Repository\UserRepository
*/
private $userRepository;
/**
* AuthProvider constructor.
* #param \App\Api\User\Repository\UserRepository $repository
*/
public function __construct(UserRepository $repository)
{
$this->userRepository = $repository;
}
/**
* #param string $email
* #return mixed
*/
public function loadUserByUsername($email)
{
try {
$user = $this->userRepository->getUser($email);
} catch (UnsupportedUserException $e) {
throw new UsernameNotFoundException('User not found', 1001, $e);
}
return $user;
}
/**
* #param \Symfony\Component\Security\Core\User\UserInterface | User $user
* #return mixed
*/
public function refreshUser(UserInterface $user)
{
return $this->loadUserByUsername($user->getEmail());
}
/**
* Qualify the supported class for this provider
* #param string $class
* #return string
*/
public function supportsClass($class)
{
if (!$class instanceof User) {
throw new UnsupportedUserException(
sprintf('Entity given is not supported, expected User got %s', $class),
1000
);
}
return $class;
}
}
My Guard :
<?php
declare(strict_types = 1);
namespace App\Api\Auth\Guard;
use App\Api\User\Repository\UserRepository;
use App\Domain\User\Exception\InvalidCredentialsException;
use App\Domain\User\ValueObject\Credentials;
use App\Domain\User\ValueObject\Email;
use App\Domain\User\ValueObject\HashedPassword;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
/**
* Allow the authentication by giving credential, when login process achieved and valid, profile page show up
* Class LoginAuthenticator
* #package App\Api\Auth\Guard
*/
final class LoginAuthenticator extends AbstractFormLoginAuthenticator
{
const LOGIN = 'login';
const SUCCESS_REDIRECT = 'profile';
/**
* #var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
*/
private $router;
/**
* #var \App\Api\User\Repository\UserRepository
*/
private $repository;
public function __construct(UrlGeneratorInterface $router, UserRepository $userRepository)
{
$this->router = $router;
$this->repository = $userRepository;
}
/**
* This method will pass the returning array to getUser and getCredential methods automatically
* #param \Symfony\Component\HttpFoundation\Request $request
* #return array
*/
public function getCredentials(Request $request)
{
return [
'email' => $request->get('email'),
'password' => $request->get('password')
];
}
/**
* In the case or the Guard and the Authenticator is the same, this method is called just after getCredentials
* #param mixed $credentials
* #param \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider
* #return null|\Symfony\Component\Security\Core\User\UserInterface|void
*/
public function getUser($credentials, UserProviderInterface $userProvider): UserInterface
{
try {
$email = $credentials['email'];
$mail = Email::fromString($email);
$user = $userProvider->loadUserByUsername($mail->toString());
if ($user instanceof UserInterface) {
$this->checkCredentials($credentials, $user);
}
} catch (InvalidCredentialsException $exception) {
throw new AuthenticationException();
}
return $user;
}
/**
* The ùail has been found, because a user has been identified, we take the has password we have to compare
* #param mixed $credentials
* #param \Symfony\Component\Security\Core\User\UserInterface $user
* #return bool
*/
public function checkCredentials($credentials, UserInterface $user)
{
$mail = Email::fromString($credentials['email']);
$userCredentials = new Credentials($mail, HashedPassword::fromHash($user->getPassword()));
// Plain password compared
$match = $userCredentials->password->match($credentials['password']);
if (!$match) {
throw new InvalidCredentialsException();
}
return true;
}
/**
* Called when authentication executed and was successful!
*
* This should return the Response sent back to the user, like a
* RedirectResponse to the last page they visited.
*
* If you return null, the current request will continue, and the user
* will be authenticated. This makes sense, for example, with an API.
*
* #param \Symfony\Component\HttpFoundation\Request $request
* #param \Symfony\Component\Security\Core\Authentication\Token\TokenInterface $token
* #param string $providerKey
*
* #return RedirectResponse
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return new RedirectResponse($this->router->generate(self::SUCCESS_REDIRECT));
}
protected function getLoginUrl(): string
{
return $this->router->generate(self::LOGIN);
}
/**
* Does the authenticator support the given Request?
*
* If this returns false, the authenticator will be skipped.
*
* #param Request $request
*
* #return bool
*/
public function supports(Request $request)
{
return $request->getPathInfo() === $this->router->generate(self::LOGIN) && $request->isMethod('POST');
}
}
My Security.yml
security:
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
users:
id: 'App\Api\Auth\Provider\AuthProvider'
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
stateless: true
anonymous: true
provider: users
guard:
entry_point: 'App\User\Auth\Guard\LoginAuthenticator'
authenticators:
- 'App\Api\Auth\Guard\LoginAuthenticator'
form_login:
login_path: /sign-in
check_path: sign-in
logout:
path: /logout
target: /
api:
pattern: ^/(/user/*|/api|)
stateless: true
guard:
authenticators:
- 'App\Api\Auth\Guard\LoginAuthenticator'
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/api, roles: USER }
- { path: ^/user/*, roles: USER }
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
My User entity
<?php
declare(strict_types = 1);
namespace App\Api\User\Entity;
use App\Domain\User\Repository\Interfaces\CRUDInterface;
use App\Shared\Entity\Traits\CreatedTrait;
use App\Shared\Entity\Traits\DeletedTrait;
use App\Shared\Entity\Traits\EntityNSTrait;
use App\Shared\Entity\Traits\IdTrait;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Table(name="app_users")
* #ORM\Entity(repositoryClass="App\Api\User\Repository\UserRepository")
*/
class User implements UserInterface, CRUDInterface, \Serializable, EncoderAwareInterface
{
use IdTrait;
use CreatedTrait;
use DeletedTrait;
use EntityNSTrait;
/**
* #ORM\Column(type="string", length=25, unique=false, nullable=true)
*/
private $username;
/**
* #ORM\Column(type="string", length=64)
*/
private $password;
/**
* #ORM\Column(type="string", length=254, unique=true)
*/
private $email;
/**
* #return mixed
*/
public function getEmail()
{
return $this->email;
}
/**
* #param mixed $email
* #return User
*/
public function setEmail($email)
{
$this->email = $email;
return $this;
}
public function __construct()
{
}
public function getUsername()
{
return $this->username;
}
public function getSalt()
{
// you *may* need a real salt depending on your encoder
// see section on salt below
return null;
}
public function getPassword()
{
return $this->password;
}
public function getRoles()
{
return array('USER');
}
/**
* From UserInterface
*/
public function eraseCredentials()
{
// Never used ?‡
}
/** #see \Serializable::serialize() */
public function serialize()
{
var_dump('need it'); // never called
return serialize([
$this->id,
$this->username,
$this->email,
$this->password,
// see section on salt below
// $this->salt,
]);
}
/** #see \Serializable::unserialize() */
public function unserialize($serialized)
{
list (
$this->id,
$this->username,
$this->email,
$this->password,
// see section on salt below
// $this->salt
) = unserialize($serialized, ['allowed_classes' => false]);
}
/**
* #param mixed $password
* #return User
*/
public function setPassword($password)
{
$this->password = $password;
return $this;
}
/**
* Gets the name of the encoder used to encode the password.
*
* If the method returns null, the standard way to retrieve the encoder
* will be used instead.
*
* #return string
*/
public function getEncoderName()
{
return 'bcrypt';
}
}
It's my really first project on SF4, it's maybe a dumb mistake but can't find it.
EDIT : I tried to pass in security config the attribute stateless to false, my serialize method were called but then I have an access denied error on profile page.
I need to stay "stateless" but it may help you to find a solution.
A stateless firewall will never store the token in the session, so you have to pass the credentials for every request you make to the API.
Currently your guard class returns a redirect, so your authentication is lost due to symfony not storing the token for stateless firewalls. To solve this, you should return null in the method onAuthenticationSuccess instead of doing a redirect. This also means, that you should create a separate guard class for the API firewall.
You can also find a good guard example for APIs in the symfony docs: https://symfony.com/doc/current/security/guard_authentication.html#step-1-create-the-authenticator-class
Edit:
I slightly misunderstood what you are trying to achieve. So it seems that you want to have a pure REST application with symfony and authenticate the user once where you then get back a token which can be used for future requests.
Some time ago I had the same issue and I stumbled over a very good bundle called LexikJWTAuthenticationBundle. This bundle gives you the necessary feature you need out of the box.
If you install it by following the Getting started documentation, you should have the basics for this.
Your configuration should then look something like this:
security:
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
users:
id: 'App\Api\Auth\Provider\AuthProvider'
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/api/sign-in
stateless: true
anonymous: true
form_login:
check_path: /api/login_check
username_parameter: email
password_parameter: password
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
require_previous_session: false
api:
pattern: ^/(/user/*|/api|)
stateless: true
anonymous: true
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/api, roles: USER }
- { path: ^/user/*, roles: USER }
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
But don't forget to add the login_check route to your routes.yml
api_login_check:
path: /api/login_check
If everything is setup correctly, you should now be able to retrieve a new token with the following request:
curl -X POST http://localhost/api/login_check -d _username=yourUsername -d _password=yourPassword
The token you received with this call should then be used for all future request to the API. You can pass it via the Authorization header
curl -H "Authorization: Bearer $YOUR_TOKEN" http://localhost/api/some-protected-route`
If you want to pass it differently (e.g. via query param) you have to change the configuration of this bundle:
lexik_jwt_authentication:
token_extractors:
query_parameter:
enabled: true
name: auth
Now you could use https://localhost/api/some-protecte-route?auth=$YOUR_TOKEN instead.
For more information about this, take a look at the configuration reference of this bundle
I hope this helps a little bit to get you started with.
I'm using symfony 3.4 with DoctrineMongoDBBundle and LexikJWTAuthenticationBundle . I'm trying to create a user login which return JWT token. If i specify the username and password under in_memory provider, it returns the token but if i use a entity provider, it returns {"code":401,"message":"bad credentials"}.
Here is my security.yml
# To get started with security, check out the documentation:
# https://symfony.com/doc/current/security.html
security:
# https://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded
encoders:
AppBundle\Document\User:
algorithm: bcrypt
cost: 12
providers:
webprovider:
entity:
class: AppBundle\Document\User
property: username
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/api/login
stateless: true
anonymous: true
provider: webprovider
form_login:
username_parameter: username
password_parameter: password
check_path: /api/login_check
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
require_previous_session: false
api:
pattern: ^/api
stateless: true
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
main:
anonymous: ~
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate
#http_basic: ~
# https://symfony.com/doc/current/security/form_login_setup.html
#form_login: ~
access_control:
- { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
Here is my User class
<?php
// /AppBundle/Document/User.php
namespace AppBundle\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\Bundle\MongoDBBundle\Validator\Constraints\Unique as MongoDBUnique;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #MongoDB\Document(collection="users")
* #MongoDBUnique(fields="email")
*/
class User implements UserInterface
{
/**
* #MongoDB\Id
*/
protected $id;
/**
* #MongoDB\Field(type="string")
* #Assert\Email()
*/
protected $email;
/**
* #MongoDB\Field(type="string")
* #Assert\NotBlank()
*/
protected $username;
/**
* #MongoDB\Field(type="string")
* #Assert\NotBlank()
*/
protected $password;
/**
* #MongoDB\Field(type="boolean")
*/
private $isActive;
public function __construct()
{
var_dump("1");
$this->isActive = true;
// may not be needed, see section on salt below
// $this->salt = md5(uniqid('', true));
}
public function getId()
{
return $this->id;
}
public function getEmail()
{
return $this->email;
}
public function setEmail($email)
{
$this->email = $email;
}
public function setUsername($username)
{
$this->username = $username;
}
public function getUsername()
{
var_dump($this->username);
return $this->username;
}
public function getSalt()
{
return null;
}
public function getPassword()
{
return $this->password;
}
public function setPassword($password)
{
$this->password = $password;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function eraseCredentials()
{
}
}
Would really appreciate if someone could help, Thanks.
You should use FOS bundle components by extending Base model User calss in entity, better than using directly Implementing UserInterface, In your case problem could be your password is not getting encoded correct. It's better to encode using security.password_encoder. To more better understanding i am sharing an example to setup login and generating token.
Your security.yml should look like this
`# Symfony 3.4 security.yml
security:
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
Symfony\Component\Security\Core\User\User: plaintext
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
ROLE_API: ROLE_USER, ROLE_MEDIC, ROLE_STUDENT
# https://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
in_memory:
memory: ~
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api_login:
pattern: ^/api/login
stateless: true
anonymous: true
form_login:
check_path: /api/login_check
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
require_previous_session: false
api:
pattern: ^/(api)
stateless: true #false (not assign cookies)
anonymous: ~
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
provider: fos_userbundle
access_control: .................`
Your User class should be like this:
// /AppBundle/Document/User.php
namespace AppBundle\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
use FOS\UserBundle\Model\User as BaseUser;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\Bundle\MongoDBBundle\Validator\Constraints\Unique as MongoDBUnique;
/**
* #MongoDB\Document(collection="users")
* #MongoDBUnique(fields="email")
*/
class User implements BaseUser
{
/**
* #MongoDB\Id
*/
protected $id;
/**
* #MongoDB\Field(type="string")
* #Assert\Email()
*/
protected $email;
/**
* #MongoDB\Field(type="string")
* #Assert\NotBlank()
*/
protected $username;
/**
* #MongoDB\Field(type="string")
* #Assert\NotBlank()
*/
protected $password;
/**
* #MongoDB\Field(type="boolean")
*/
private $isActive;
public function __construct()
{
var_dump("1");
$this->isActive = true;
// may not be needed, see section on salt below
// $this->salt = md5(uniqid('', true));
}
public function getId()
{
return $this->id;
}
public function getEmail()
{
return $this->email;
}
public function setEmail($email)
{
$this->email = $email;
}
public function setUsername($username)
{
$this->username = $username;
}
public function getUsername()
{
var_dump($this->username);
return $this->username;
}
public function getSalt()
{
return null;
}
public function getPassword()
{
return $this->password;
}
public function setPassword($password)
{
$this->password = $password;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function eraseCredentials()
{
}
}
Now your Api tokenController.php to generate token and validate token
namespace \your_namespace\Api;
use FOS\RestBundle\Context\Context;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Controller\FOSRestController;
use AppBundle\Document\User;
# Below most of the components belongs to Nelmio api or Sensio or Symfony
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Encoder\EncoderFactory;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\ExpiredTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\PreAuthenticationJWTUserToken;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
class TokenController extends FOSRestController
{
/**
* #Rest\Post("/tokens", name="api_token_new",
* options={"method_prefix" = false},
* defaults={"_format"="json"}
* )
*
* #ApiDoc(
* section = "Security",
* description = "Get user token",
* parameters={
* {"name"="username", "dataType"="string", "required"=true, "description"="username or email"},
* {"name"="pass", "dataType"="string", "required"=true, "description"="password "},
* }
* )
*/
public function newTokenAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$username = $request->get('username');
/** #var User $user */
$user = $em->getRepository('AppBundle:User')->findOneBy(['email' => $username]);
if (!$user) {
throw $this->createNotFoundException();
}
if (!$user->isEnabled()){
throw $this->createNotFoundException($this->get('translator')->trans('security.user_is_disabled'));
}
$pass = $request->get('pass');
$isValid = $this->get('security.password_encoder')->isPasswordValid($user, $pass);
if (!$isValid) {
throw new BadCredentialsException();
}
$token = $this->get('lexik_jwt_authentication.encoder')->encode([
'username' => $user->getUsername(),
'id' => $user->getId(),
'roles' => $user->getRoles(),
'exp' => time() + (30 * 24 * 3600) // 30 days expiration -> move to parameters or config
]);
// Force login
$tokenLogin = new UsernamePasswordToken($user, $pass, "public", $user->getRoles());
$this->get("security.token_storage")->setToken($tokenLogin);
// Fire the login event
// Logging the user in above the way we do it doesn't do this automatically
$event = new InteractiveLoginEvent($request, $tokenLogin);
$this->get("event_dispatcher")->dispatch("security.interactive_login", $event);
$view = $this->view([
'token' => $token,
'user' => $user
]);
$context = new Context();
$context->addGroups(['Public']);
$view->setContext($context);
return $this->handleView($view);
}
/**
* #Rest\Post("/validate", name="api_token_validate",
* options={"method_prefix" = false},
* defaults={"_format"="json"}
* )
*
* #ApiDoc(
* section = "Security",
* description = "Get user by token",
* parameters={
* {"name"="token", "dataType"="textarea", "required"=true, "description"="token"},
* }
* )
*/
public function validateUserToken(Request $request)
{
$token = $request->get('token');
//get UserProviderInterface
$fos = $this->get('fos_user.user_provider.username_email');
//create PreAuthToken
$preAuthToken = new PreAuthenticationJWTUserToken($token);
try {
if (!$payload = $this->get('lexik_jwt_authentication.jwt_manager')->decode($preAuthToken)) {
throw new InvalidTokenException('Invalid JWT Token');
}
$preAuthToken->setPayload($payload);
} catch (JWTDecodeFailureException $e) {
if (JWTDecodeFailureException::EXPIRED_TOKEN === $e->getReason()) {
throw new ExpiredTokenException();
}
throw new InvalidTokenException('Invalid JWT Token', 0, $e);
}
//get user
/** #var User $user */
$user = $this->get('lexik_jwt_authentication.security.guard.jwt_token_authenticator')->getUser($preAuthToken, $fos);
$view = $this->view([
'token' => $token,
'user' => $user
]);
$context = new Context();
$context->addGroups(array_merge(['Public'],$user->getRoles()));
$view->setContext($context);
return $this->handleView($view);
}
if you face any problem then , let me know.
you need to add the provider before json_login tag this the case for me
provider: fos_userbundle
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'));
}
}
I'm working on a Symfony2 application with an API available for other applications.
I want to secure the access to the API. For this part I have no problem.
But I have to make this connection available not with the usual login/password couple but just with an API key.
So I went to the official site and its awesome cookbook for creating a custom authentication provider, just what I need I said to myself.
The example was not what I needed but I decided to adapt it to my needs.
Unfortunately I didn't succeed.
I'll give you my code and I will explain my problem after.
Here is my Factory for creating the authentication provider and the listener:
<?php
namespace Pmsipilot\UserBundle\DependencyInjection\Security\Factory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
class ApiFactory implements SecurityFactoryInterface
{
/**
* #param \Symfony\Component\DependencyInjection\ContainerBuilder $container
* #param string $id
* #param aray $config
* #param string $userProvider
* #param string $defaultEntryPoint
* #return array
*/
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$providerId = 'security.authentification.provider.api.'.$id;
$container
->setDefinition($providerId, new DefinitionDecorator('api.security.authentification.provider'))
->replaceArgument(0, new Reference($userProvider))
;
$listenerId = 'security.authentification.listener.api.'.$id;
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('api.security.authentification.listener'));
return array($providerId, $listenerId, $defaultEntryPoint);
}
/**
* #return string
*/
public function getPosition()
{
return 'http';
}
/**
* #return string
*/
public function getKey()
{
return 'api';
}
/**
* #param \Symfony\Component\Config\Definition\Builder\NodeDefinition $node
* #return void
*/
public function addConfiguration(NodeDefinition $node)
{
}
}
Next my listener code:
<?php
namespace Pmsipilot\UserBundle\Security\Firewall;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Pmsipilot\UserBundle\Security\WsseUserToken;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
class ApiListener implements ListenerInterface
{
protected $securityContext;
protected $authenticationManager;
/**
* Constructor for listener. The parameters are defined in services.xml.
*
* #param \Symfony\Component\Security\Core\SecurityContextInterface $securityContext
* #param \Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface $authenticationManager
*/
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager)
{
$this->securityContext = $securityContext;
$this->authenticationManager = $authenticationManager;
}
/**
* Handles login request.
*
* #param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* #return void
*/
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
$securityToken = $this->securityContext->getToken();
if($securityToken instanceof AuthenticationToken)
{
try
{
$this->securityContext->setToken($this->authenticationManager->authenticate($securityToken));
}
catch(\Exception $exception)
{
$this->securityContext->setToken(null);
}
}
}
}
My authentication provider code:
<?php
namespace Pmsipilot\UserBundle\Security\Authentication\Provider;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class ApiProvider implements AuthenticationProviderInterface
{
private $userProvider;
/**
* Constructor.
*
* #param \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider An UserProviderInterface instance
*/
public function __construct(UserProviderInterface $userProvider)
{
$this->userProvider = $userProvider;
}
/**
* #param string $username
* #param \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken $token
* #return mixed
* #throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException
*/
protected function retrieveUser($username, UsernamePasswordToken $token)
{
$user = $token->getUser();
if($user instanceof UserInterface)
{
return $user;
}
try
{
$user = $this->userProvider->loadUserByApiKey($username, $token->getCredentials());
if(!$user instanceof UserInterface)
{
throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
}
return $user;
}
catch (\Exception $exception)
{
throw new AuthenticationServiceException($exception->getMessage(), $token, 0, $exception);
}
}
/**
* #param TokenInterface $token
* #return null|\Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
* #throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\BadCredentialsException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException
*/
function authenticate(TokenInterface $token)
{
$username = $token->getUsername();
if(empty($username))
{
throw new AuthenticationServiceException('No username given.');
}
try
{
$user = $this->retrieveUser($username, $token);
if(!$user instanceof UserInterface)
{
throw new AuthenticationServiceException('retrieveUser() must return a UserInterface.');
}
$authenticatedToken = new UsernamePasswordToken($user, null, 'api', $user->getRoles());
$authenticatedToken->setAttributes($token->getAttributes());
return $authenticatedToken;
}
catch(\Exception $exception)
{
throw $exception;
}
}
/**
* #param TokenInterface $token
* #return bool
*/
public function supports(TokenInterface $token)
{
return true;
}
}
To use these two objects I used a yml file to configure them:
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="pmsipilot.api.security.authentication.factory" class="Pmsipilot\UserBundle\DependencyInjection\Security\Factory\ApiFactory" public="false">
<tag name="security.listener.factory" />
</service>
</services>
</container>
Now the authentication provider code:
<?php
namespace Pmsipilot\UserBundle\Security\Authentication\Provider;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class ApiProvider implements AuthenticationProviderInterface
{
private $userProvider;
/**
* Constructor.
*
* #param \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider An UserProviderInterface instance
*/
public function __construct(UserProviderInterface $userProvider)
{
$this->userProvider = $userProvider;
}
/**
* #param string $username
* #param \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken $token
* #return mixed
* #throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException
*/
protected function retrieveUser($username, UsernamePasswordToken $token)
{
$user = $token->getUser();
if($user instanceof UserInterface)
{
return $user;
}
try
{
$user = $this->userProvider->loadUserByApiKey($username, $token->getCredentials());
if(!$user instanceof UserInterface)
{
throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
}
return $user;
}
catch (\Exception $exception)
{
throw new AuthenticationServiceException($exception->getMessage(), $token, 0, $exception);
}
}
/**
* #param TokenInterface $token
* #return null|\Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
* #throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\BadCredentialsException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException
*/
function authenticate(TokenInterface $token)
{
$username = $token->getUsername();
if(empty($username))
{
throw new AuthenticationServiceException('No username given.');
}
try
{
$user = $this->retrieveUser($username, $token);
if(!$user instanceof UserInterface)
{
throw new AuthenticationServiceException('retrieveUser() must return a UserInterface.');
}
$authenticatedToken = new UsernamePasswordToken($user, null, 'api', $user->getRoles());
$authenticatedToken->setAttributes($token->getAttributes());
return $authenticatedToken;
}
catch(\Exception $exception)
{
throw $exception;
}
}
/**
* #param TokenInterface $token
* #return bool
*/
public function supports(TokenInterface $token)
{
return true;
}
}
Just FYI my user provider:
<?php
namespace Pmsipilot\UserBundle\Security\Provider;
use Propel\PropelBundle\Security\User\ModelUserProvider;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use \Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder;
class ApiProvider extends ModelUserProvider
{
/**
* Constructeur
*/
public function __construct()
{
parent::__construct('Pmsipilot\UserBundle\Model\User', 'Pmsipilot\UserBundle\Proxy\User', 'username');
}
/**
* #param string $apikey
* #return mixed
* #throws \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
*/
public function loadUserByApiKey($apikey)
{
$queryClass = $this->queryClass;
$query = $queryClass::create();
$user = $query
->filterByApiKey($apikey)
->findOne()
;
if(null === $user)
{
throw new UsernameNotFoundException(sprintf('User with "%s" api key not found.', $apikey));
}
$proxyClass = $this->proxyClass;
return new $proxyClass($user);
}
}
And for the configuration part my security.yml:
security:
factories:
PmsipilotFactory: "%kernel.root_dir%/../src/Pmsipilot/UserBundle/Resources/config/security_factories.xml"
providers:
interface_provider:
id: pmsipilot.security.user.provider
api_provider:
id: api.security.user.provider
encoders:
Pmsipilot\UserBundle\Proxy\User: sha512
firewalls:
assets:
pattern: ^/(_(profiler|wdt)|css|images|js|favicon.ico)/
security: false
api:
provider: api_provider
access_denied_url: /unauthorizedApi
pattern: ^/api
api: true
http_basic: true
stateless: true
interface:
provider: interface_provider
access_denied_url: /unauthorized
pattern: ^/
anonymous: ~
form_login:
login_path: /login
check_path: /login_check
use_forward: true
default_target_path: /
logout: ~
access_control:
- { path: ^/api, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: SUPER_ADMIN }
Wow it's a lot of code, I hope it's not too boring.
My problem here is that my custom authentication provider is called by the two firewalls api and interface instead of just by the api one.
And of course they don't behave as I wanted.
I didn't find anything about such an issue.
I know I made a mistake, otherwise it will be working, but where and why I don't know.
I also found this tutorial but it didn't help much more.
Of course, don't hesitate to suggest me if there is another solution for using another authentication provider than the default one.
So I will answer my own question because I found the solution to my problem and I'll tell you how I solved it.
There was some mistake in my example and I understood them searching in the Symfony code.
Like the key returned by the getKey method of the Factory class. I found that the api one I've created was for me not an other parameter to my security.yml file, but a replacement to the http_basic one.
That's why I'm having some trouble using two providers instead of just one, because I got two keys (api and http_basic) which both used a provider. In fact I think it's the reason to that problem.
To make it simple I follow the Symfony tutorial, except for the token class but I replaced the code of the new classes by the code of the Symfony classes.
In a kind of way I recreated the http basic authentication of Symfony to make it posssible to overload.
And here I am, I could do what I want, configure a different type of http authentication based on the Symfony one but with several changes.
This story helped me because know I know that the best way to understand Symfony principles is to go deeper in the code and look after.
I have found much simpler solution. In config.yml you can point to your custom auth. provider class, like this:
security.authentication.provider.dao.class: App\Security\AuthenticationProvider\MyDaoAuthenticationProvider
Of course MyDaoAuthenticationProvider have to extend Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider
I have come upon your problem, and it seems that you did your code well.
The thing that could also be causing problems is the order of firewall definitions in security.xml.
Try to imagine, if there is some defined Listener(firewall-entry) before your CustomListener and it returns some Response, it will break handlers loop.Eventually it will cause that your CustomListener is registered, but handle method will never be called.
Maybe a little late (5 years later actually), but you have a typo in your Factory.
You wrote:
$providerId = 'security.authentification.provider.api.'.$id;
Where "authentification" has to be authentication