I have 4 different user types in a system (on top of Symfony 2). Each type have some specific fields and behaviour, but all of them have a common base. So it seems it would be good idea to implement single class for each user extending the same superclass.
How can it be achieved? All I have found on the topic is some RollerworksMultiUserBundle.
Using table inheritance in the ORM level and OOP inheritance. Go for Single Table Inheritance if performance is critical (no JOINs) or Class Table Inheritance if you are a purist.
E.g.
Common base class:
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
/**
* #ORM\Entity(repositoryClass="Some\Bundle\Repository\UserRepository")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="userType", type="string")
* #ORM\DiscriminatorMap({
* "userType1" = "UserType1",
* "userType2" = "UserType2",
* "userType3" = "UserType3",
* "userType4" = "UserType4"
* })
*/
abstract class User implements AdvancedUserInterface
{
/**
* #ORM\Id()
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=250, unique=true)
*/
protected $email;
/**
* #ORM\Column(type="string", length=128, nullable=true)
*/
protected $password;
// other fields
public function getSalt()
{
return "some salt number";
}
public function getUsername()
{
return $this->email;
}
public function getPassword()
{
return $this->password;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function eraseCredentials() {}
public function isCredentialsNonExpired()
{
return true;
}
public function isAccountNonLocked()
{
return true;
}
public function isAccountNonExpired()
{
return true;
}
public function isEnabled()
{
return true;
}
public function equals(UserInterface $user)
{
return $user->getUsername() === $this->getUsername() || $user->getEmail() === $this->getEmail();
}
}
The children classes are straightforward (below an example for class UserType1 only):
/**
* #ORM\Entity
*/
class UserType1 extends User
{
// fields of UserType1 class go here
public function getRoles()
{
return array('ROLE_USER_TYPE_1', 'ROLE_USER');
}
}
The rest is pretty much like in the examples. In security.yml:
security:
encoders:
Some\Bundle\Repository\User:
algorithm: sha512
encode_as_base64: false
iterations: 1000
providers:
provider1:
entity: { class: "SomeBundle:User" }
role_hierarchy:
ROLE_USER_TYPE_1: ROLE_USER
ROLE_USER_TYPE_2: ROLE_USER
ROLE_USER_TYPE_3: ROLE_USER
ROLE_USER_TYPE_4: ROLE_USER
firewalls:
firewall1:
pattern: ^/
provider: provider1
form_login:
login_path: /login
check_path: /auth
post_only: true
username_parameter: email
password_parameter: password
always_use_default_target_path: true
default_target_path: /
logout:
path: /logout
target: /login
anonymous: ~
The repository class:
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Security\Core\User\UserInterface;
class UserRepository extends EntityRepository implements UserProviderInterface
{
public function loadUserByUsername($username)
{
$qb = $this->createQueryBuilder('u');
$query = $qb->where('LOWER(u.email) = :email')
->setParameter('email', strtolower($username))
->getQuery();
try {
$user = $query->getSingleResult();
}
catch (NoResultException $e) {
throw new UsernameNotFoundException('User not found.', null, $e);
}
return $user;
}
public function supportsClass($class)
{
return $this->getEntityName() === $class ||
is_subclass_of($class, $this->getEntityName());
}
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->find($user->getId());
}
}
Related
I want to implement the following authentication scenario in symfony 5:
User sends a login form with username and password, authentication is processed against an LDAP server
if authentication against the LDAP server is successful :
if there is an instance of my App\Entity\User that as the same username as the ldap matching entry, refresh some of its attributes from the ldap server and return this entity
if there is no instance create a new instance of my App\Entity\User and return it
I have implemented a guard authenticator which authenticates well against the LDAP server but it's returning me an instance of Symfony\Component\Ldap\Security\LdapUser and I don't know how to use this object to make relation with others entities!
For instance, let's say I have a Car entity with an owner property that must be a reference to an user.
How can I manage that ?
Here is the code of my security.yaml file:
security:
encoders:
App\Entity\User:
algorithm: auto
# 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: email
my_ldap:
ldap:
service: Symfony\Component\Ldap\Ldap
base_dn: "%env(LDAP_BASE_DN)%"
search_dn: "%env(LDAP_SEARCH_DN)%"
search_password: "%env(LDAP_SEARCH_PASSWORD)%"
default_roles: ROLE_USER
uid_key: uid
extra_fields: ['mail']
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
provider: my_ldap
guard:
authenticators:
- App\Security\LdapFormAuthenticator
I finally found a good working solution.
The missing piece was a custom user provider.
This user provider has the responsibility to authenticate user against ldap and to return the matching App\Entity\User entity. This is done in getUserEntityCheckedFromLdap method of LdapUserProvider class.
If there is no instance of App\Entity\User saved in the database, the custom user provider will instantiate one and persist it. This is the first user connection use case.
Full code is available in this public github repository.
You will find below, the detailed steps I follow to make the ldap connection work.
So, let's declare the custom user provider in security.yaml.
security.yaml:
providers:
ldap_user_provider:
id: App\Security\LdapUserProvider
Now, configure it as a service, to pass some ldap usefull string arguments in services.yaml.
Note since we are going to autowire the Symfony\Component\Ldap\Ldap service, let's add this service configuration too:
services.yaml:
#see https://symfony.com/doc/current/security/ldap.html
Symfony\Component\Ldap\Ldap:
arguments: ['#Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
arguments:
- host: ldap
port: 389
# encryption: tls
options:
protocol_version: 3
referrals: false
App\Security\LdapUserProvider:
arguments:
$ldapBaseDn: '%env(LDAP_BASE_DN)%'
$ldapSearchDn: '%env(LDAP_SEARCH_DN)%'
$ldapSearchPassword: '%env(LDAP_SEARCH_PASSWORD)%'
$ldapSearchDnString: '%env(LDAP_SEARCH_DN_STRING)%'
Note the arguments of the App\Security\LdapUserProvider come from env vars.
.env:
LDAP_URL=ldap://ldap:389
LDAP_BASE_DN=dc=mycorp,dc=com
LDAP_SEARCH_DN=cn=admin,dc=mycorp,dc=com
LDAP_SEARCH_PASSWORD=s3cr3tpassw0rd
LDAP_SEARCH_DN_STRING='uid=%s,ou=People,dc=mycorp,dc=com'
Implement the custom user provider :
App\Security\LdapUserProvider:
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Ldap\LdapInterface;
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 LdapUserProvider implements UserProviderInterface
{
/**
* #var Ldap
*/
private $ldap;
/**
* #var EntityManager
*/
private $entityManager;
/**
* #var string
*/
private $ldapSearchDn;
/**
* #var string
*/
private $ldapSearchPassword;
/**
* #var string
*/
private $ldapBaseDn;
/**
* #var string
*/
private $ldapSearchDnString;
public function __construct(EntityManagerInterface $entityManager, Ldap $ldap, string $ldapSearchDn, string $ldapSearchPassword, string $ldapBaseDn, string $ldapSearchDnString)
{
$this->ldap = $ldap;
$this->entityManager = $entityManager;
$this->ldapSearchDn = $ldapSearchDn;
$this->ldapSearchPassword = $ldapSearchPassword;
$this->ldapBaseDn = $ldapBaseDn;
$this->ldapSearchDnString = $ldapSearchDnString;
}
/**
* #param string $username
* #return UserInterface|void
* #see getUserEntityCheckedFromLdap(string $username, string $password)
*/
public function loadUserByUsername($username)
{
// must be present because UserProviders must implement UserProviderInterface
}
/**
* search user against ldap and returns the matching App\Entity\User. The $user entity will be created if not exists.
* #param string $username
* #param string $password
* #return User|object|null
*/
public function getUserEntityCheckedFromLdap(string $username, string $password)
{
$this->ldap->bind(sprintf($this->ldapSearchDnString, $username), $password);
$username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_FILTER);
$search = $this->ldap->query($this->ldapBaseDn, 'uid=' . $username);
$entries = $search->execute();
$count = count($entries);
if (!$count) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
}
if ($count > 1) {
throw new UsernameNotFoundException('More than one user found');
}
$ldapEntry = $entries[0];
$userRepository = $this->entityManager->getRepository('App\Entity\User');
if (!$user = $userRepository->findOneBy(['userName' => $username])) {
$user = new User();
$user->setUserName($username);
$user->setEmail($ldapEntry->getAttribute('mail')[0]);
$this->entityManager->persist($user);
$this->entityManager->flush();
}
return $user;
}
/**
* Refreshes the user after being reloaded from the session.
*
* When a user is logged in, at the beginning of each request, the
* User object is loaded from the session and then this method is
* called. Your job is to make sure the user's data is still fresh by,
* for example, re-querying for fresh User data.
*
* If your firewall is "stateless: true" (for a pure API), this
* method is not called.
*
* #return UserInterface
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
}
return $user;
// Return a User object after making sure its data is "fresh".
// Or throw a UsernameNotFoundException if the user no longer exists.
throw new \Exception('TODO: fill in refreshUser() inside ' . __FILE__);
}
/**
* Tells Symfony to use this provider for this User class.
*/
public function supportsClass($class)
{
return User::class === $class || is_subclass_of($class, User::class);
}
}
Configure the firewall to use our custom user provider:
security.yaml
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
provider: ldap_user_provider
logout:
path: app_logout
guard:
authenticators:
- App\Security\LdapFormAuthenticator
Write an authentication guard:
App\SecurityLdapFormAuthenticator:
<?php
namespace App\Security;
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\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class LdapFormAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
private $urlGenerator;
private $csrfTokenManager;
public function __construct(UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager)
{
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
}
public function supports(Request $request)
{
return 'app_login' === $request->attributes->get('_route') && $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'username' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['username']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $userProvider->getUserEntityCheckedFromLdap($credentials['username'], $credentials['password']);
if (!$user) {
throw new CustomUserMessageAuthenticationException('Username could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
//in this scenario, this method is by-passed since user authentication need to be managed before in getUser method.
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$request->getSession()->getFlashBag()->add('info', 'connected!');
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('app_homepage'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate('app_login');
}
}
My user entity looks like this:
`App\Entity\User`:
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Entity(repositoryClass=UserRepository::class)
*/
class User implements UserInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
private $email;
/**
* #var string The hashed password
* #ORM\Column(type="string")
*/
private $password = 'password is not managed in entity but in ldap';
/**
* #ORM\Column(type="string", length=255)
*/
private $userName;
/**
* #ORM\Column(type="json")
*/
private $roles = [];
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $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->email;
}
/**
* #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;
}
/**
* #see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security.yaml
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function setUserName(string $userName): self
{
$this->userName = $userName;
return $this;
}
}
For Symfony 6 I do like this.
No extra implementation
security:
role_hierarchy:
ROLE_USER: ROLE_USER
ROLE_ADMIN: [ROLE_USER, ROLE_ADMIN]
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
providers:
pmh_db:
entity:
class: App\Entity\User
property: username
pmh_ldap:
ldap:
service: Symfony\Component\Ldap\Ldap
base_dn: '%base_dn%'
search_dn: '%search_dn%'
search_password: '%search_password%'
default_roles: 'ROLE_USER'
uid_key: '%uid_key%'
extra_fields: ['email']
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
pattern: ^/
provider: pmh_db
switch_user: { role: ROLE_ALLOWED_TO_SWITCH }
login_throttling:
max_attempts: 5
form_login_ldap:
login_path: app_login
check_path: app_login
service: Symfony\Component\Ldap\Ldap
dn_string: 'DOMAIN\{username}'
query_string: null
default_target_path: /
logout:
path: /logout
target: /
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: /
# by default, the feature is enabled by checking a
# checkbox in the login form (see below), uncomment the
# following line to always enable it.
always_remember_me: 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: '^/login', roles: PUBLIC_ACCESS }
- { path: '^/admin', roles: [IS_AUTHENTICATED_FULLY, ROLE_ADMIN] }
- { path: '^/', roles: ROLE_USER }
I have setup my own login form in a security controller. to use the LoginForm I have configured this in the security configuration.
I want to use a custom login form authenticator to have more control over the authentication progress, register logins in the system,
and do anything I'd like to add (IP-check etc, etc...)
So there is also a LoginFormAuthenticator class in my application. somehow the authentication process doesn't even seem to use the methods of
the custom LoginFormAuthenticator. Is my security.yaml configured properly? how do I get all my configuration to work together?
The security in symfony seems so messy at some points, I can't begin to understand how people manage to properly configure it..
LoginFormAuthenticator:
class LoginFormAuthenticator extends AbstractGuardAuthenticator
{
/**
* Constructor
*
* #param Logger $logger
* #param LoginAttemptManagerInterface $loginAttemptManager
* #param LocationManagerInterface $locationManager
* #param RouterInterface $router
* #param UserPasswordEncoderInterface $userPasswordEncoder
* #param UserRepositoryInterface $userRepository
*/
public function __construct(Logger $logger, LoginAttemptManagerInterface $loginAttemptManager, LocationManagerInterface $locationManager, RouterInterface $router, UserPasswordEncoderInterface $userPasswordEncoder, UserRepositoryInterface $userRepository)
{
$this->_logger = $logger;
$this->_loginAttemptManager = $loginAttemptManager;
$this->_locationManager = $locationManager;
$this->_router = $router;
$this->_userPasswordEncoder = $userPasswordEncoder;
$this->_userRepository = $userRepository;
}
/**
* {#inheritdoc}
*/
protected function getLoginUrl()
{
return $this->_router->generate("login");
}
/**
* {#inheritdoc}
*/
public function getCredentials(Request $request)
{
$credentials = $request->get("login_form");
return [
"username" => $credentials["username"],
"password" => $credentials["password"],
"token" => $credentials["_token"],
];
}
/**
* {#inheritdoc}
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials["username"];
try {
$user = $this->_userRepository->findOneByUsername($username);
if (null !== $user && $user instanceof UserInterface) {
/* #var LoginAttempt $loginAttempt */
$loginAttempt = $this->_loginAttemptManager->create();
$user->addLoginAttempt($loginAttempt);
}
}
catch (NoResultException $e) {
return null;
}
catch (NonUniqueResultException $e) {
return null;
}
catch (UsernameNotFoundException $e) {
return null;
}
}
/**
* {#inheritdoc}
*/
public function checkCredentials($credentials, UserInterface $user)
{
/* #var string $rawPassword the unencoded plain password */
$rawPassword = $credentials["password"];
if ($this->_userPasswordEncoder->isPasswordValid($user, $rawPassword)) {
return true;
}
return new CustomUserMessageAuthenticationException("Invalid credentials");
}
/**
* {#inheritdoc}
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
/* #var AbstractUser $user */
$user = $token->getUser();
/* #var LoginAttempt $loginAttempt */
$loginAttempt = $user->getLastLoginAttempt();
$loginAttempt->success();
this->_loginAttemptManager->saveOne($loginAttempt, true);
}
/**
* {#inheritdoc}
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
// without this method the authentication process becomes a loop
}
/**
* {#inheritdoc}
*/
public function start(Request $request, AuthenticationException $authException = null)
{
return new RedirectResponse($this->getLoginUrl());
}
/**
* {#inheritdoc}
*/
public function supports(Request $request)
{
return $request->getPathInfo() != $this->getLoginUrl() || !$request->isMethod(Request::METHOD_POST);
}
/**
* {#inheritdoc}
*/
public function supportsRememberMe()
{
return true;
}
}
SecurityController:
class SecurityController extends AbstractController
{
/**
* #Route(path = "login", name = "login", methods = {"GET", "POST"})
* #Template(template = "security/login.html.twig")
*
* #param AuthenticationUtils $authUtils
* #param Request $request
* #return array
*/
public function login(AuthenticationUtils $authUtils, Request $request)
{
$form = $this->createLoginForm();
if (null !== $authUtils->getLastAuthenticationError()) {
$form->addError(new FormError(
$this->_translator->trans("error.authentication.incorrect-credentials", [], "security")
));
}
if (null != $authUtils->getLastUsername()) {
$form->setData([
"username" => $authUtils->getLastUsername(),
]);
}
// settings are in config/packages/security.yaml
// configuration authenticates user in login form authenticator service
return [
"backgroundImages" => $this->_backgroundImageManager->findAll(),
"form" => $form->createView(),
];
}
/**
* #return FormInterface
*/
private function createLoginForm() : FormInterface
{
$form = $this->createForm(LoginForm::class, null, [
"action" => $this->generateUrl("login"),
"method" => Request::METHOD_POST,
]);
$form->add("submit", SubmitType::class, [
"label" => $this->_translator->trans("btn.login", [], "button"),
"icon_name" => "sign-in",
"translation_domain" => false,
]);
return $form;
}
}
security.yaml:
security:
providers:
user_provider:
entity:
class: App\Entity\Model\AbstractUser
property: username
oauth_provider:
entity:
class: App\Entity\Model\ApiClient
property: name
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# The API-Oauth-Token-Firewall must be above the API-firewall
api_oauth_token:
pattern: ^/api/oauth/token$
security: false
# The API-firewall must be above the Main-firewall
api:
pattern: ^/api/*
security: true
stateless: true
oauth2: true
provider: oauth_provider
access_denied_handler: App\Service\Api\Security\ApiAccessDeniedHandler
main:
anonymous: true
guard:
authenticators:
- App\Service\Security\LoginFormAuthenticator
access_denied_handler: App\Service\Security\AccessDeniedHandler
provider: user_provider
form_login:
login_path: /login
check_path: /login
default_target_path: / #index
username_parameter: "login_form[username]"
password_parameter: "login_form[password]"
logout:
# the logout path overrides the implementation of the logout method
# in the security controller
path: /logout
target: / #index
remember_me:
secret: '%kernel.secret%'
lifetime: 43200 # 60 sec * 60 min * 12 hours
path: /
remember_me_parameter: "login_form[remember]"
encoders:
App\Entity\Model\AbstractUser:
algorithm: bcrypt
cost: 13
access_control:
# omitted from this question
role_hierarchy:
# omitted from this question
How did you come up with the logic of LoginFormAuthenticator::supports()?
shouldn't this be the opposite like:
return 'login' === $request->attributes->get('_route')
&& $request->isMethod('POST');
Apparently I had two forms of authentication configured in security.yaml
So I've removed the form_login key from the config:
security.yaml
security:
providers:
user_provider:
entity:
class: App\Entity\Model\AbstractUser
property: username
oauth_provider:
entity:
class: App\Entity\Model\ApiClient
property: name
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# The API-Oauth-Token-Firewall must be above the API-firewall
api_oauth_token:
pattern: ^/api/oauth/token$
security: false
# The API-firewall must be above the Main-firewall
api:
pattern: ^/api/*
security: true
stateless: true
oauth2: true
provider: oauth_provider
access_denied_handler: App\Service\Api\Security\ApiAccessDeniedHandler
main:
anonymous: true
guard:
authenticators:
- App\Service\Security\LoginFormAuthenticator
access_denied_handler: App\Service\Security\AccessDeniedHandler
provider: user_provider
logout:
# the logout path overrides the implementation of the logout method
# in the security controller
path: /logout
target: / #index
remember_me:
secret: '%kernel.secret%'
lifetime: 43200 # 60 sec * 60 min * 12 hours
path: /
remember_me_parameter: "login_form[remember]"
And updated the LoginFormAuthenticator
- integrated
- also added checking CSRF token
LoginFormAuthenticator
class LoginFormAuthenticator extends AbstractGuardAuthenticator
{
const FORM = "login_form";
const USERNAME = "username";
const PASSWORD = "password";
const CSRF_TOKEN = "token";
/**
* Constructor
*
* #param CsrfTokenManagerInterface $csrfTokenManager
* #param Logger $logger
* #param LoginAttemptManagerInterface $loginAttemptManager
* #param LocationManagerInterface $locationManager
* #param RouterInterface $router
* #param UserPasswordEncoderInterface $userPasswordEncoder
* #param UserRepositoryInterface $userRepository
*/
public function __construct(CsrfTokenManagerInterface $csrfTokenManager, Logger $logger, LoginAttemptManagerInterface $loginAttemptManager, LocationManagerInterface $locationManager, RouterInterface $router, UserPasswordEncoderInterface $userPasswordEncoder, UserRepositoryInterface $userRepository)
{
$this->_csrfTokenManager = $csrfTokenManager;
$this->_logger = $logger;
$this->_loginAttemptManager = $loginAttemptManager;
$this->_locationManager = $locationManager;
$this->_router = $router;
$this->_userPasswordEncoder = $userPasswordEncoder;
$this->_userRepository = $userRepository;
}
/**
* Get Login URL
*
* #return string
*/
protected function getLoginUrl()
{
return $this->_router->generate("login");
}
/**
* Get Target URL
*
* #return string
*/
protected function getTargetUrl()
{
return $this->_router->generate("index");
}
/**
* {#inheritdoc}
*/
public function getCredentials(Request $request)
{
$credentials = $request->request->get(self::FORM);
$request->getSession()->set(Security::LAST_USERNAME, $credentials["username"]);
return [
self::USERNAME => $credentials["username"],
self::PASSWORD => $credentials["password"],
self::CSRF_TOKEN => $credentials["_token"],
];
}
/**
* {#inheritdoc}
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials[self::USERNAME];
try {
$user = $this->_userRepository->findOneByUsername($username);
if (null !== $user && $user instanceof UserInterface) {
/* #var LoginAttempt $loginAttempt */
$loginAttempt = $this->_loginAttemptManager->create();
$user->addLoginAttempt($loginAttempt);
}
return $user;
}
catch (NoResultException $e) {
throw new BadCredentialsException("Authentication failed");
}
catch (NonUniqueResultException $e) {
throw new BadCredentialsException("Authentication failed");
}
}
/**
* {#inheritdoc}
*/
public function checkCredentials($credentials, UserInterface $user)
{
$csrfToken = new CsrfToken(self::FORM, $credentials[self::CSRF_TOKEN]);
if (false === $this->_csrfTokenManager->isTokenValid($csrfToken)) {
throw new InvalidCsrfTokenException('Invalid CSRF token');
}
/* #var string $rawPassword the unencoded plain password */
$rawPassword = $credentials[self::PASSWORD];
if ($this->_userPasswordEncoder->isPasswordValid($user, $rawPassword)) {
return true;
}
/* #var AbstractUser $user */
$loginAttempt = $user->getLastLoginAttempt();
if (null !== $loginAttempt) {
$this->_loginAttemptManager->saveOne($loginAttempt);
}
return new CustomUserMessageAuthenticationException("Invalid credentials");
}
/**
* {#inheritdoc}
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
/* #var AbstractUser $user */
$user = $token->getUser();
/* #var LoginAttempt $loginAttempt */
$loginAttempt = $user->getLastLoginAttempt();
$loginAttempt->setStatus(LoginAttempt::STATUS_AUTHENTICATION_SUCCESS);
if (null !== $loginAttempt) {
$this->_loginAttemptManager->saveOne($loginAttempt);
}
return new RedirectResponse($this->getTargetUrl());
}
/**
* {#inheritdoc}
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception->getMessage());
return new RedirectResponse($this->getLoginUrl());
}
/**
* {#inheritdoc}
*/
public function start(Request $request, AuthenticationException $authException = null)
{
return new RedirectResponse($this->getLoginUrl());
}
/**
* {#inheritdoc}
*/
public function supports(Request $request)
{
return $request->getPathInfo() === $this->getLoginUrl() && $request->isMethod(Request::METHOD_POST);
}
/**
* {#inheritdoc}
*/
public function supportsRememberMe()
{
return true;
}
}
I've been having trouble implementing Symfony's Security features in my project. I have configured my Security.yaml and created a securityController , my Userclass implements userInterface , and from what I can see on the docs I haven't missed anything out. My form renders fine, and I can input my username and password, but when I submit valid credentials it just refreshes the page. Profiler showed that no SQL queries had been made, and despite me configuring authenticationUtils to display errors (as per the tutorial on the docs) nothing is displayed.
Security.yaml
security:
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
encoders:
App\Entity\User: sha256
providers:
in_memory: { memory: ~ }
main_db_provider:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
# anonymous: true
pattern: ^/$ #test
form_login:
login_path: login
check_path: login
csrf_token_generator: security.csrf.token_manager
provider: main_db_provider
# activate different ways to authenticate
# http_basic: true
# https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate
# form_login: true
# https://symfony.com/doc/current/security/form_login_setup.html
# 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: ^/$, roles: ROLE_USER }
Security Controller
<?php
// src/Controller/SecurityController.php
namespace App\Controller;
use App\Entity\User;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
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('ad-lotto-theme/login.html.twig', array(
'last_username' => $lastUsername,
'error' => $error,
));
}
}
User class
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity("email", message="This email is already in use.")
* #UniqueEntity("username", message="This username is already in use")
*/
class User implements UserInterface, \Serializable
{
/**
* #ORM\Column(name="roles",type="string", length=255)
*/
private $roles;
/**
* #ORM\Column(name="salt",type="string", length=255)
*/
private $salt = "saltyboye";
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(name="username",type="string", length=255, unique=true)
*/
private $username;
/**
* #ORM\Column(name = "password", type="string", length=255)
*/
private $password;
/**
* #ORM\Column(name="email", type="string", length=255, unique=true)
*/
private $email;
/**
* #ORM\Column(type="datetime")
*/
private $registeredOn;
/**
* #ORM\Column(type="integer", nullable=true)
*/
private $referrer;
/**
* #ORM\Column(type="smallint")
*/
private $entries;
/**
* #ORM\Column(type="string", length=3)
*/
private $currency;
/** #see \Serializable::serialize() */
public function serialize()
{
return serialize(array(
$this->registeredOn,
$this->id,
$this->email,
$this->username,
$this->password,
$this->roles,
$this->referrer,
$this->currency,
$this->entries,
$this->salt));
// see section on salt below
// ,
}
public function unserialize($serialized)
{
list (
$this->id,
$this->username,
$this->password,
// see section on salt below
// $this->salt
) = unserialize($serialized, array('allowed_classes' => false));
}
public function eraseCredentials()
{
}
public function getRoles()
{
return array("ROLE_USER");
}
public function getSalt()
{
return $this->salt;
}
public function getId()
{
return $this->id;
}
public function getUsername(): ?string
{
return $this->username;
}
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function getRegisteredOn(): ?\DateTimeInterface
{
return $this->registeredOn;
}
public function setRegisteredOn(\DateTimeInterface $registeredOn): self
{
$this->registeredOn = $registeredOn;
return $this;
}
public function getReferrer(): ?interedThisWeek
{
return $this->referrer;
}
public function setReferrer(?int $referrer): self
{
$this->referrer = $referrer;
return $this;
}
public function getEntries(): ?bool
{
return $this->entries;
}
public function setEntries(bool $entries): self
{
$this->entries = $entries;
return $this;
}
public function setCurrency(bool $currency): self
{
$this->currency = $currency;
return $this;
}
public function getCurrency(): ?bool
{
return $this->currency;
}
}
that salt is temporary, don't worry :) I haven't figured out how to implement SHA256 yet, but I needed to fill the field in the db :)
I'm assuming you follow this official tutorial: How to Build a Traditional Login Form
First of all your firewall configured to only 1 URI - / because of regex ^/$, so login form and other routes are not under the firewall.
Try to follow the tutorial from start to end just as it says there, make sure everything works and only then make changes.
I am writing regarding the Symfony authentication problem, which occurred last month and I still cannot find a solution, so I am dependent on you :D
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Table(name="app_users")
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface, \Serializable
{
//id,username,password
public function getSalt()
{
return null;
}
public function getPassword()
{
return $this->password;
}
public function getRoles()
{
return array('ROLE_USER');
}
public function eraseCredentials()
{
}
public function serialize()
{
return serialize(array(
$this->id,
$this->username,
$this->password,
// see section on salt below
// $this->salt,
));
}
public function unserialize($serialized)
{
list (
$this->id,
$this->username,
$this->password,
// see section on salt below
// $this->salt
) = unserialize($serialized);
}
}
This is my User entity and now below you can see my security.yaml which I think I configured right:
security:
encoders:
App\Entity\User:
algorithm: bcrypt
providers:
db_provider:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|img|js)/
security: false
main:
anonymous: true
http_basic: ~
provider: db_provider
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
Whenever I am trying to access /admin route it shows me http-basic login but whenever I input "admin, admin" nothing happens. IN my database I have one user with username:admin and password admin which is hashed by bcrypt.
Not using authentication then everything works as it should, I get all data from the database as it should be after authentication.
Thanks for your help guys!
Your problem
As Med already pointed out, your User entity has the ROLE_USER role as default:
/* App/Entity/User.php */
public function getRoles()
{
return array('ROLE_USER');
}
Your access_control configuration on the other hand states that the route /admin can only be accessed with a user that has the ROLE_ADMIN role:
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
That means, your user "admin" lacks the sufficient role to access /admin.
Solution
You need to be able to assign multiple roles to the user. One possible way is saving the roles as a concatenated string and returning it as an array:
/* App/Entity/User.php */
/**
* #ORM\Column(name="roles", type="string")
* #var string
*/
private $roles;
/**
* Get the user roles as an array of strings
* #return array
*/
public function getRoles()
{
return explode($roles, ',');
}
You can even add some methods to manage your roles via the entity class:
/* App/Entity/User.php */
/**
* Add a new role
* #param string $role name of the role
* #return this
*/
public function addRole($role)
{
$roles = $this->getRoles();
if (array_search($role, $roles) === false) {
$roles[] = $role;
$this->roles = implode(',', $roles);
}
return $this;
}
/**
* Remove a role
* #param string $role name of the role
* #return this
*/
public function removeRole($role)
{
$roles = $this->getRoles();
$searchResult = array_search($role, $roles);
if ($searchResult !== false) {
unset($roles[$searchResult]);
$this->roles = implode(',', $roles);
}
return $this;
}
I'm trying since 3 hours to install and configure FOSuser, which many developpers adviced me to use it.I wanted actually to make a normal login form without to use FOS but I had a lot of problems.I followed all steps in the documentation. the installation was ok , the configuration also but everytime when I try to log in , it shows "Bad credentials".So i find somehow this command that I executed :php app/console fos:user:create i give name-email-password. it work somehow but only with what i write, I mean when I register user in my registration form and try to log in it shows "Bad credentials".I hope that I was clear else please tell me what do you need to know
Here are my Users.php where i have all my users info to login...
namespace test\indexBundle\Document;
use FOS\UserBundle\Model\User as BaseUser;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
/**
*
* #MongoDB\Document
*/
class Users extends BaseUser
{
/**
* #MongoDB\Id
*/
protected $id;
/**
* #MongoDB\String
*/
protected $userId;
/**
* #MongoDB\String
*/
protected $userEmail;
/**
* #MongoDB\String
*/
protected $userPassword;
/**
* #MongoDB\String
*/
protected $salt;
/**
* #MongoDB\Int
*/
protected $isActive;
public function __construct()
{
parent::__construct();
$this->isActive = true;
$this->salt = md5(uniqid(null, true));
}
/**
* Set id
*
* #param id $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* Get id
*
* #return id $id
*/
public function getId()
{
return $this->id;
}
/**
* Set userId
*
* #param string $userId
*/
public function setUserId()
{
$this->userId = $this->salt;
}
/**
* Get userId
*
* #return string $userId
*/
public function getUserId()
{
return $this->userId;
}
/**
* Set userName
*
* #param string $userName
*/
public function setUserName($userName)
{
$this->userName = $userName;
}
/**
* Get userName
*
* #return string $userName
*/
public function getUserName()
{
return $this->username;
}
/**
* Set userEmail
*
* #param string $userEmail
*/
public function setUserEmail($userEmail)
{
$this->userEmail = $userEmail;
}
/**
* Get userEmail
*
* #return string $userEmail
*/
public function getUserEmail()
{
return $this->userEmail;
}
/**
* Set userPassword
*
* #param string $userPassword
*/
public function setPassword($userPassword)
{
$this->userPassword = $userPassword;
}
/**
* Get userPassword
*
* #return string $userPassword
*/
public function getPassword()
{
return $this->userPassword;
}
/**
* #inheritDoc
*/
public function getSalt()
{
return '';
}
/**
* #inheritDoc
*/
public function getRoles()
{
return array('ROLE_USER');
}
/**
* #inheritDoc
*/
public function eraseCredentials()
{
}
/**
* #see \Serializable::serialize()
*/
public function serialize()
{
return serialize(array(
$this->id
));
}
/**
* #see \Serializable::unserialize()
*/
public function unserialize($serialized)
{
list (
$this->id
) = unserialize($serialized);
}
}
and here my security.yml:
jms_security_extra:
secure_all_services: false
expressions: true
security:
encoders:
FOS\UserBundle\Model\UserInterface: sha512
test\indexBundle\Document\Users:
algorithm: sha1
encode_as_base64: false
iterations: 1
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: true
form_login:
check_path: /login_check
login_path: /login
provider: fos_userbundle
post_only: true
use_forward: false
username_parameter: email
password_parameter: password
failure_path: null
failure_forward: false
target_path_parameter: redirect_url
logout:
path: /logout
target: /blog
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
and login function:
public function loginAction()
{
$request = $this->getRequest();
$session = $request->getSession();
if ($this->get('security.context')->isGranted('IS_AUTHENTICATED_FULLY'))
{
return $this->redirect($this->generateUrl('index_homepage'));
}
if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR))
{
$error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
}
else
{
$error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
$session->remove(SecurityContext::AUTHENTICATION_ERROR);
}
return $this->render('indexBundle:index:logIn.html.twig', array(
'last_username' => $session->get(SecurityContext::LAST_USERNAME),
'error' => $error,
));
}
I might be wrong but I think FOSUserBundle requires a user to be activated after it's been created if you use the form registration, it's send out and email with a link I believe. I think you can use app/console fos:user:activate to activate if there is no email.