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.
Related
Versions I Used:
using (symfony 5.2.6) with (api-platform 2.7.0) alongside (php 8.0.3) and (postgres 13)
Description
used maker bundle to generate a User Entity and configured the security.yaml to encode passwords already tried auto and bcrypt or even argon2i non of them seems to work and hash the passwords
Possible Solution
as the symfony documentation describes it should automatically encode passwords if we are using security bundle and implementing UserInterface but this one seems like a bug cause i have tried many things nothing works . maybe using UserPasswordEncoderInterface but this one should not be used when User class is implementing UserInterface anyway i hope someone check this and tell me i'm wrong otherwise seems like a bug.
security.yaml:
security:
encoders:
App\Entity\User:
algorithm: bcrypt
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
provider: app_user_provider
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# 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: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
# or require ROLE_ADMIN or IS_AUTHENTICATED_FULLY for /admin*
# - { path: '^/admin', roles: [IS_AUTHENTICATED_FULLY, ROLE_ADMIN] }
# the 'path' value can be any valid regular expression
# (this one will match URLs like /api/post/7298 and /api/comment/528491)
# - { path: ^/api/(post|comment)/\d+$, roles: ROLE_USER }
User.php:
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use App\Entity\Trait\HasDateTime;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ApiResource
* #ORM\Entity(repositoryClass=UserRepository::class)
* #ORM\Table(name="`user`")
* #ORM\HasLifecycleCallbacks
*/
class User implements UserInterface
{
use HasDateTime;
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
#[Assert\NotBlank]
#[Assert\Email]
private $email;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
#[Assert\NotBlank]
private $username;
/**
* #ORM\Column(type="json")
*/
private $roles = [];
/**
* #var string The hashed password
* #ORM\Column(type="string")
*/
#[Assert\NotBlank]
private $password;
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): string
{
return (string) $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUsername(): string
{
return (string) $this->username;
}
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
/**
* #see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* #see UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* Returning a salt is only needed, if you are not using a modern
* hashing algorithm (e.g. bcrypt or sodium) in your security.yaml.
*
* #see UserInterface
*/
public function getSalt(): ?string
{
return null;
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
}
tried to save data in the database each time it saved passwords as plaintext.
also tried to remove the trait class thought it might conflict some how which seems strange(i know) (but i just gave it a try) it was not hashing passwords even after removing the trait. what am i doing wrong or missing here?
thanks in advance
here is the solution:
i had to create a class that implements DataPersister then in there we have to encode the password . here is the code:
<?php
namespace App\DataPersister;
use App\Entity\User;
use ApiPlatform\Core\DataPersister;
use Doctrine\ORM\EntityManagerInterface;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserDataPersister implements DataPersisterInterface
{
public function __construct(private EntityManagerInterface $entityManager, private UserPasswordEncoderInterface $userPasswordEncoderInterface)
{
}
/**
* Is the data supported by the persister?
*/
public function supports($data): bool
{
return $data instanceof User;
}
/**
* #param User $data
* #return object|void Void will not be supported in API Platform 3, an object should always be returned
*/
public function persist($data)
{
if ($data->getPassword()) {
$data->setPassword(
$this->userPasswordEncoderInterface->encodePassword($data, $data->getPassword())
);
}
$this->entityManager->persist($data);
$this->entityManager->flush();
}
/**
* Removes the data.
*/
public function remove($data)
{
$this->entityManager->remove($data);
$this->entityManager->flush();
}
}
I'm pretty new to Symfony in general, I mostly used it because I needed to do something secure very fast, and also to discover Symfony 4.
I'm trying to make a secure connexion with the Security recipe but I'm facing two major problems (probably related) and a small one.
First, I tried to define the salt as nullable but it's still NOT NULL in db. Here's my definition of the column :
/**
* #ORM\Column(name="salt", type="string", nullable=true)
*/
private $salt;
So now the big problems : Passwords I add are not hashed and trying to connect returns error 500
I tried to follow the documentation and here are :
My Entity
use Doctrine\ORM\Mapping as ORM;
use PhpParser\Node\Scalar\String_;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Table(name="app_user")
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface, \Serializable
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=25, unique=true)
*/
private $username;
/**
* #ORM\Column(type="string", length=255)
*/
private $password;
/**
* #ORM\Column(type="string", length=254, unique=true, nullable=true)
*/
private $email;
/**
* #ORM\Column(name="is_active", type="boolean")
*/
private $isActive;
/**
* #ORM\Column(name="salt", type="string", nullable=true)
*/
private $salt;
/**
* #ORM\Column(name="alias", type="string")
*/
private $alias;
/**
* #return mixed
*/
public function getAlias()
{
return $this->alias;
}
/**
* #param mixed $alias
*/
public function setAlias($alias): void
{
$this->alias = $alias;
}
public function __construct()
{
$this->isActive = true;
// may not be needed, see section on salt below
// $this->salt = md5(uniqid('', true));
}
public function getUsername()
{
return $this->username;
}
public function getSalt() :String
{
// you *may* need a real salt depending on your encoder
// see section on salt below
return $this->salt;
}
public function getPassword()
{
return $this->password;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function eraseCredentials()
{
}
/** #see \Serializable::serialize() */
public function serialize()
{
return serialize([
$this->id,
$this->username,
$this->password,
// see section on salt below
// $this->salt
]);
}
/** #see \Serializable::unserialize() */
public function unserialize($serialized)
{
list (
$this->id,
$this->username,
$this->password,
// see section on salt below
// $this->salt
) = unserialize($serialized, ['allowed_classes' => false]);
}
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #param mixed $id
*/
public function setId($id): void
{
$this->id = $id;
}
/**
* #return mixed
*/
public function getEmail()
{
return $this->email;
}
/**
* #param mixed $email
*/
public function setEmail($email): void
{
$this->email = $email;
}
/**
* #return mixed
*/
public function getisActive()
{
return $this->isActive;
}
/**
* #param mixed $isActive
*/
public function setIsActive($isActive): void
{
$this->isActive = $isActive;
}
/**
* #param mixed $username
*/
public function setUsername($username): void
{
$this->username = $username;
}
/**
* #param mixed $password
*/
public function setPassword($password): void
{
$this->password = $password;
}
/**
* #param mixed $salt
*/
public function setSalt($salt): void
{
$this->salt = $salt;
}
}
My Controllers
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends Controller
{
/**
* #Route("/login", name="login")
*/
public function login(Request $request, AuthenticationUtils $authenticationUtils)
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('security/login.html.twig', array(
'last_username' => $lastUsername,
'error' => $error,
));
}
}
and
use App\Entity\User;
use App\Form\UserType;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
/**
* #Route("/user")
*/
class UserController extends Controller
{
/**
* #Route("/", name="user_index", methods="GET")
*/
public function index(UserRepository $userRepository): Response
{
return $this->render('user/index.html.twig', ['users' => $userRepository->findAll()]);
}
/**
* #Route("/new", name="user_new", methods="GET|POST")
*/
public function new(Request $request): Response
{
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
return $this->redirectToRoute('user_index');
}
return $this->render('user/new.html.twig', [
'user' => $user,
'form' => $form->createView(),
]);
}
/**
* #Route("/{id}", name="user_show", methods="GET")
*/
public function show(User $user): Response
{
return $this->render('user/show.html.twig', ['user' => $user]);
}
/**
* #Route("/{id}/edit", name="user_edit", methods="GET|POST")
*/
public function edit(Request $request, User $user): Response
{
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('user_edit', ['id' => $user->getId()]);
}
return $this->render('user/edit.html.twig', [
'user' => $user,
'form' => $form->createView(),
]);
}
/**
* #Route("/{id}", name="user_delete", methods="DELETE")
*/
public function delete(Request $request, User $user): Response
{
if ($this->isCsrfTokenValid('delete'.$user->getId(), $request->request->get('_token'))) {
$em = $this->getDoctrine()->getManager();
$em->remove($user);
$em->flush();
}
return $this->redirectToRoute('user_index');
}
public function register(User $user, UserPasswordEncoderInterface $encoder)
{
$plainPassword = $user->getPassword();
$encoded = $encoder->encodePassword($user, $plainPassword);
$user->setPassword($encoded);
}
}
and my security.yaml
security:
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
db_provider:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
provider: db_provider
form_login:
login_path: login
check_path: login
logout:
path: /logout
target: /homepage
pattern: ^/admin
http_basic: ~
encoders:
App\Entity\User:
algorithm: argon2i
# 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: ^/admin, roles: ROLE_ADMIN }
I tried to add this after checking if for isSubmited and isValid in my UserController::new()
$plainPassword = $user->getPassword;
$encoded = $encoder->encodePassword($user, $plainPassword);
$user->setPassword($encoded);
But I had an error Saying that the UserPasswordEncoderInterface $encoder I passed as method argument wasn't injected when loading the form. Still I'm not sure it would be a good solution to make it work as I would have to duplicate that logic in the UserController::edit(), which does not look like Symfony-like code.
(the error :)
"Controller "App\Controller\UserController::new()" requires that you provide a value for the "$encoder" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one."
I also tried to copy/paste (that how desperate I am...) the code in my UserController and then the SecurityController but this didn't work either
public function register(UserPasswordEncoderInterface $encoder)
{
// whatever *your* User object is
$user = new App\Entity\User();
$plainPassword = 'ryanpass';
$encoded = $encoder->encodePassword($user, $plainPassword);
$user->setPassword($encoded);
}
I'm getting this as log from the server :
"No encoder has been configured for account "App\Entity\User"."
I also tried to insert directly in my db some values, but trying to connect gave me a "Access Denied" message when entering the right password, which I think is another problem...
I really don't get where I'm wrong and I couldn't find people asking about this. I'd be sincerely grateful if you could help me.
Note :
The UserController routes start with /user and is completely public as I need a user to access secured admin panel.
EDIT
I'm using MySQL 5.7 and PHP 7.2 if that can be related
Since you're using Argon2i as the encoder algorithm for your entity, your $salt becomes obsolete:
Do you need to use a Salt property?
If you use bcrypt or argon2i, no. Otherwise, yes. All passwords must be hashed with a salt, but bcrypt and argon2i do this internally [...] the getSalt() method in User can just return null (it's not used). [...]
-How to Load Security Users from the Database (the Entity Provider)
Try removing the $salt property and the setter method, and let your getSalt() return null. Persist the user without encoding operations and check the persisted password.
While this can be seen as a dirty hack, it seems to be a good practice...
I finnally found a solution thanks to #LeonWillens. Actually removing the salt property and setters made me discover that the security recipe come without the validator. So I ran composer require doctrine form security validator. I added a plainText field in my Entity which is not a column
/**
* #Assert\NotBlank()
* #Assert\Length(max=4096)
*/
private $plainPassword;
With that, I could add this logic in UserController::new()
/**
* #Route("/new", name="user_new", methods="GET|POST")
*/
public function new(Request $request, UserPasswordEncoderInterface $passwordEncoder): Response
{
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$password = $passwordEncoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
return $this->redirectToRoute('user_index');
}
return $this->render('user/new.html.twig', [
'user' => $user,
'form' => $form->createView(),
]);
}
I change the encoders in my security.yaml
encoders:
Symfony\Component\Security\Core\User\User: plaintext
App\Entity\User:
algorithm: argon2i
And now adding a user work perfectly. I still have problems with connexion, but no such thing as an Exception thrown
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 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
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