I am developing a new Symfony2 project (eibrowser) and am trying to set up two things:
1. Custom Authentication Provider
2. A custom User provider
I followed the example / tutorial (word by word nearly) given on the Symfony2 documentation pages:
http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html
My bundle in which the security folder lives is called 'NiwaUtilitiesBundle'.
In the end of all I end up with this (horribly) confusing error message
Catchable Fatal Error: Argument 3 passed to Niwa\UtilitiesBundle\Security\Firewall
\WsseListener::__construct() must be an instance of Niwa\UtilitiesBundle\SecurityFirewall
\SessionAuthenticationStrategyInterface, none given, called in /home/uwe/www/eibrowser2
/app/cache/dev/appDevDebugProjectContainer.php on line 2007 and defined in /home/uwe
/www/eibrowser2/src/Niwa/UtilitiesBundle/Security/Firewall/WsseListener.php line 21
I am not even sure which parts of my code to show here ....
But the code below shows the WsseProvider which I needed to change a bit as we are using a user management system in house which does the authentication for us
<?php
namespace Niwa\UtilitiesBundle\Security\Authentication\Provider;
use Symfony\Component\Security\Core\Authentication\Provider AuthenticationProviderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\NonceExpiredException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Niwa\UtilitiesBundle\Security\Authentication\Token\WsseUserToken;
use Niwa\UsermanagementBundle\Lib\UserManagement;
class WsseProvider implements AuthenticationProviderInterface
{
private $userProvider;
private $cacheDir;
private $userManagement;
public function __construct(UserProviderInterface $userProvider, $cacheDir,$userManagement)
{
$this->userProvider = $userProvider;
$this->cacheDir = $cacheDir;
$this->userManagement = $userManagement;
}
public function authenticate(TokenInterface $token)
{
$user = $this->userProvider->loadUserByName($token->getUsername());
if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword())) {
$authenticatedToken = new UmUserToken($user->getRoles());
$authenticatedToken->setUser($user);
return $authenticatedToken;
}
throw new AuthenticationException('The WSSE authentication failed.');
}
/**
* This function is specific to Wsse authentication and is only used to help this example
*
* For more information specific to the logic here, see
* https://github.com/symfony/symfony-docs/pull/3134#issuecomment-27699129
*/
protected function validateDigest($digest, $nonce, $created, $secret)
{
// Check created time is not in the future
if (strtotime($created) > time()) {
return false;
}
// Expire timestamp after 5 minutes
if (time() - strtotime($created) > 300) {
return false;
}
// Validate that the nonce is *not* used in the last 5 minutes
// if it has, this could be a replay attack
if (file_exists($this->cacheDir.'/'.$nonce) && file_get_contents($this->cacheDir.'/'.$nonce) + 300 > time()) {
throw new NonceExpiredException('Previously used nonce detected');
}
// If cache directory does not exist we create it
if (!is_dir($this->cacheDir)) {
mkdir($this->cacheDir, 0777, true);
}
file_put_contents($this->cacheDir.'/'.$nonce, time());
// Validate Secret
// $expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true));
//return $digest === $expected;
$username = $this->extractUsername($token->getUsername());
$domain = $this->extractDomain($token->getUsername());
$response = $this->userManagement->authenticate($username,$token->getCredentials(),$domain);
return 200 == $response[$status];
}
public function supports(TokenInterface $token)
{
return $token instanceof WsseUserToken;
}
}
I am trying to make Symfony use my custom provider by setting configuring my security,yml this way:
security:
providers:
user_provider:
id: um.user.provider
encoders:
Niwa\UtilitiesBundle\Security\User\UmUser: plaintext
firewalls:
wsse_secured:
pattern: ^/
provider: user_provider
wsse: true
form_login:
login_path: /login
check_path: /login_check
anonymous: ~
The user provider is my custom user provider which I also created following the documentation. I created unit tests around it and it seems to work fine...
If I take the line 'wsse: true' out, Symfony2 will work without that error message but will simply use its standard authentication...
I know this ticket is a bit messy - but I just don't know where to start with his problem really.
If you have any idea what could be wrong - please let me know,
Uwe
Related
I am trying to create a logout feature which when clicked from the below (example) redirects to the necessary page.
Example:
If anybody clicks logout with website beginning with aaa.io/user -> go to aaa.io/login.
If anybody clicks logout with website beginning with aaa.io/dev/{id} -> go to aaa.io/home/{id}
How to create two logout feature which will redirect to two seperate pages? I have tried with first example and works fine.I heard we can do it using Symfony firewall but unable to get it.
#security.yaml
security:
enable_authenticator_manager: true
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
App\Entity\Dimitry:
algorithm: auto
providers:
app_user_provider:
entity:
class: App\Entity\Dimitry
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
custom_authenticator: App\Security\LoginAuthenticator
logout:
path: app_logout
target: app_login
//Security Controller
#[Route(path: '/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
if ($this->getUser()) {
return $this->redirectToRoute('app_home');
}
// 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', ['last_username' => $lastUsername, 'error' => $error]);
}
#[Route(path: '/logout', name: 'app_logout')]
public function logout()
{
return $this->redirectToRoute('app_login');
//throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
The logout method is never called because it's intercepted by the logout key on your firewall. So the code lines in the public function logout won't be executed.
IMO, you could use events :
Create a subscriber,
Subscribe on LogoutEvent::class,
Analyze the request provided by the logout event
Use it to determine the route,
Catch the response provided by the logout event,
Use the UrlGenerator to redirect user,
Update the response to redirect to the corresponding route
Documentation provides a very good example, you can use as a template for your logic. Your subscriber could be like this one:
// src/EventListener/LogoutSubscriber.php
namespace App\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
class LogoutSubscriber implements EventSubscriberInterface
{
public function __construct(
private UrlGeneratorInterface $urlGenerator
) {
}
public static function getSubscribedEvents(): array
{
//2 - Subscribe to LogoutEvent
return [LogoutEvent::class => 'onLogout'];
}
public function onLogout(LogoutEvent $event): void
{
// get the security token of the session that is about to be logged out
$token = $event->getToken();
// 3. get the current request
$request = $event->getRequest();
// 4. Your own logic to analyze the URL
$route = 'homepage';//default route
if (...) {
$route = 'URL1';
}
if (...) {
$route = 'URL2';
}
// 5. get the current response, if it is already set by another listener
$response = $event->getResponse();
// configure a custom logout response to the homepage
$response = new RedirectResponse(
$this->urlGenerator->generate($route),
RedirectResponse::HTTP_SEE_OTHER
);
$event->setResponse($response);
}
}
Suppose I have two authentication mechanisms. How do I allow/deny a user's access to content depending on which authentication mechanism they used to authenticate themselves.
For example, let's say I have:
two type of users: admin and user such that all admins are users but users are not admins
two authentication mechanisms: admin_login and user_login
I would like to make it so that an admin has to authenticate through admin_login in order to have admin accesses. This means he would be considered a regular user if he was authenticated through user_login.
I first thought about using firewall context, but quickly realized it wasn't gonna work as I would need a firewall to support multiple contexts :
# config/packages/security.yaml
security:
# ...
firewalls:
admin:
# ...
context: // not currently supported
- admin
- user
user:
# ...
context: user
The other idea I came up with was creating a property called is_allow_admin in the User class and using it to change the way the roles are retrieved by setting it in the admin_login authenticator :
// src/Entity/User.php
// ...
class User implements UserInterface
{
// ...
private $is_allow_admin = false;
// ...
public function setIsAllowAdmin(bool $is_allow_admin)
{
$this->is_allow_admin = $is_allow_admin ;
}
public function getRoles(): array
{
if (!$this->is_allow_admin) {
return ['ROLE_USER'];
}
//...
return $roles;
}
// ...
}
// src/Security/AdminAuthenticator.php
class AdminAuthenticator extends AbstractFormLoginAuthenticator
{
//...
public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface
{
$user = $userProvider->loadUserByUsername($credentials['username']);
if (!$user) {
throw new CustomUserMessageAuthenticationException('username could not be found.');
}
$user->setIsAllowAdmin(true); // <-
return $user;
}
//...
}
This unfortunately doesn't work. Everything goes smoothly with the authenticator, onAuthenticationSuccess is triggered. But somehow in the end, no user is authenticated.
I know it has to do with $this->setIsAllowAdmin(true); since it works correctly when I remove the line.
Is there another way to tackle this problem?
Thank you in advance.
We've updated a base project from Symfony 2.8 to 3.4. This has largely gone well, blah blah, but I've noticed quite an important issue.
It seems that logging in is instantly forgotten after the request for "check_path" has completed unless the user chooses the "Remember Me" option. -- We do not provide this option for management interfaces to ensure the user has authenticated properly, therefore the management interface can't be accessed at all.
Request flow goes as shown in Symfony profiler:
Attempt to access firewalled route.
401 response showing login form with Anonymous token.
Submit login form.
302 response with UsernamePassword token. -- This shows the username and password has been accepted.
Redirected to original firewalled route.
200 response with Anonymous token. -- UsernamePassword token has gone!
This response does not appear in the web browser's network debugger.
Redirected to login form again.
401 response showing login form with Anonymous token.
Contents of "app/config/security.yml":
security:
encoders:
App\UserBundle\Entity\User:
algorithm: bcrypt
cost: 16
providers:
local_db:
entity: { class: AppUserBundle:User }
firewalls:
dev:
pattern: ^/(_(profiler|wdt))/
security: false
assets:
pattern: ^/(css|images|js)/
security: false
admin:
pattern: ^/admin
provider: local_db
anonymous: ~
logout_on_user_change: true
form_login:
csrf_token_generator: security.csrf.token_manager
login_path: user_admin_login
check_path: user_admin_login
default_target_path: dashboard
use_forward: true
use_referer: true
logout:
path: user_admin_logout
target: dashboard
handler: auth_listener
invalidate_session: true
switch_user:
role: ROLE_TOP_ADMIN
parameter: _login_as_user
# remember_me:
# secret: "%secret%"
front:
pattern: ^/
provider: local_db
anonymous: ~
logout_on_user_change: true
form_login:
csrf_token_generator: security.csrf.token_manager
# login_path should be "user_account_login" or "user_account_auth" depending on which view you want.
login_path: user_account_login
check_path: user_account_login
default_target_path: user_account
use_forward: true
use_referer: true
logout:
path: user_account_logout
target: home
handler: auth_listener
invalidate_session: true
switch_user:
role: ROLE_ADMIN
parameter: _login_as_user
remember_me:
secret: "%secret%"
access_control:
- { path: ^/admin/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: [ROLE_CONTRIBUTOR, ROLE_EDITOR, ROLE_ADMIN, ROLE_TOP_ADMIN] }
- { path: ^/account/auth$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/account/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/account/register$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/account/forgot_password$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/account/change_password$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/account, roles: [ROLE_USER, ROLE_CONTRIBUTOR, ROLE_EDITOR, ROLE_ADMIN, ROLE_TOP_ADMIN] }
# Change the below "IS_AUTHENTICATED_ANONYMOUSLY" to "ROLE_USER" if this is going to be a private website.
# This will ensure users have to login on the landing page.
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
Routes:
user_admin_login points to "/admin/login"
user_admin_logout points to "/admin/logout"
user_account_auth points to "/account/auth".
user_account_login points to "/account/login".
user_account_logout points to "/account/logout".
I'm wondering if this is cookie related. I did notice that the Symfony session ID cookie ("webapp" below) value is changing after logging in, but it does remain consistent between page navigations elsewhere. It only changes twice when submitting the login form. Using the response references above:
Attempt to access firewalled route:
"webapp" value is "h76n9kcra43stmjb5accnqlg70itnavf" on 401 response.
Submit login form:
"webapp" value is "15iscbl51k2mjs14bck5m54f4m8qhtme" on 302 response.
Redirected to firewalled route:
"webapp" value is "ciibpf8h54u2vp3gdi31bvdm5oj3r3ts" on 200 response.
Redirected to login form:
"webapp" value is "ciibpf8h54u2vp3gdi31bvdm5oj3r3ts" on 401 response.
Contents of "app/config/config.yml" session section:
session:
storage_id: "session.storage.native"
handler_id: "session.handler.native_file"
name: "webapp"
cookie_lifetime: 604800
gc_divisor: 10
gc_probability: 1
gc_maxlifetime: 14400
save_path: "%kernel.root_dir%/../var/sessions"
I tried using different web browsers with very default cookie settings in case something was up with Chrome, no different though.
If it helps, when successfully logged in with the "remember me" option ticked, the token is a RememberMeToken -- not a UsernamePasswordToken.
Please let me know if any further information is required.
The goal here is to be able to login without needing a "remember me" option enabled.
Edit: User entity model
As requested, here is some detail about the user entity model. It's quite big (2016 lines) so I'll just paste in the parts relevant to Symfony's user interface.
Declaration
class User implements AdvancedUserInterface, UserPermissionInterface, DataContentEntityInterface, \Serializable
Interfaces UserPermissionInterface and DataContentEntityInterface are custom for our application. (Irrelevant.)
Serializable relevant parts
/**
* #see \Serializable::serialize()
*/
public function serialize()
{
return serialize([
$this->id,
$this->userName,
$this->email,
$this->password,
// $this->salt,
]);
}
/**
* #see \Serializable::unserialize()
*/
public function unserialize($serialized)
{
list(
$this->id,
$this->userName,
$this->email,
$this->password,
// $this->salt,
) = unserialize($serialized, ["allowed_classes" => false]);
}
UserInterface relevant parts
/**
* #inheritDoc
*/
public function getSalt()
{
return null;
}
/**
* #inheritDoc
*/
public function getRoles()
{
if (!$this->group) {
return [];
}
$rolesArray = array();
foreach ($this->getGroup()->getPermissions() as $k => $permission) {
$role = strtoupper($permission);
$role = str_replace('.', '_', $role);
$role = sprintf("ROLE_%s", $role);
$rolesArray[$k] = $role;
}
$rolesArray[] = $this->getGroup()->etRole();
// If user is top admin, also give admin group
if ($this->getGroup()->getRole() === "ROLE_TOP_ADMIN") {
$rolesArray[] = "ROLE_ADMIN";
}
return $rolesArray;
}
/**
* #inheritDoc
*/
public function eraseCredentials()
{
}
/**
* Get userName
*
* #return string
*/
public function getUserName()
{
return $this->userName;
}
/**
* Get password
*
* #return string
*/
public function getPassword()
{
return $this->password;
}
AdvancedUserInterface relevant parts
public function isAccountNonExpired()
{
if (!$this->expires) {
return true;
}
if (new \DateTime() <= $this->expires) {
return true;
}
return false;
}
public function isAccountNonLocked()
{
return $this->status === self::STATUS_VERIFIED;
}
public function isCredentialsNonExpired()
{
if (!$this->passwordExpires) {
return true;
}
if (new \DateTime() <= $this->passwordExpires) {
return true;
}
return false;
}
public function isEnabled()
{
return $this->isAccountNonLocked() && !$this->activationCode;
}
Entity repository UserRepository declaration
class UserRepository extends EntityRepository implements UserLoaderInterface
Function to load user
/**
* UserLoaderInterface
* #param string $userName User to look for
* #return User|null User entity, or null if not found
*/
public function loadUserByUsername($userName)
{
$qb = $this
->createQueryBuilder("u")
->where("u.userName = :userName OR u.email = :userName")
->setParameter("userName", $userName)
->andWhere("u.status != :statusDeleted")
->setParameter("statusDeleted", User::STATUS_DELETED)
->andWhere("u.status = :statusVerified")
->setParameter("statusVerified", User::STATUS_VERIFIED)
->orderBy("u.status", "DESC")
->addOrderBy("u.group", "ASC")
->addOrderBy("u.created", "ASC")
->setMaxResults(1)
;
$query = $qb->getQuery();
try {
// The Query::getSingleResult() method throws an exception
// if there is no record matching the criteria.
$user = $query->getSingleResult();
} catch (NoResultException $e) {
throw new UsernameNotFoundException(sprintf("Unable to find an active user identified by \"%s\".", $username), 0, $e);
} catch (NonUniqueResultException $e) {
throw new UsernameNotFoundException(sprintf("Unable to find a unique active user identified by \"%s\".", $username), 0, $e);
}
return $user;
}
This function works fine. A valid user entity is definitely returned.
In your security.yml, remove:
logout_on_user_change: true
or set it to false.
This will solve the instant logout issue, though it will also bypass a security feature of Symfony.
It seems as if something in the serialize() and unserialize() isn't matching, and Symfony then logs the user out as a precaution. With AdvancedUserInterface, Symfony also checks that the AdvancedUserInterface methods match too. If you have anything else going on in those methods which could cause the users to not match (like some bespoke roles management), that could be triggering the logout. To debug, I would suggest returning true in each of the AdvancedUserInterface methods, then re-adding your functionality until the logout gets triggered.
From Symfony's documentation:
If you're curious about the importance of the serialize() method inside
the User class or how the User object is serialized or deserialized,
then this section is for you. If not, feel free to skip this.
Once the user is logged in, the entire User object is serialized into
the session. On the next request, the User object is deserialized.
Then, the value of the id property is used to re-query for a fresh
User object from the database. Finally, the fresh User object is
compared to the deserialized User object to make sure that they
represent the same user. For example, if the username on the 2 User
objects doesn't match for some reason, then the user will be logged
out for security reasons.
Even though this all happens automatically, there are a few important
side-effects.
First, the Serializable interface and its serialize() and
unserialize() methods have been added to allow the User class to be
serialized to the session. This may or may not be needed depending on
your setup, but it's probably a good idea. In theory, only the id
needs to be serialized, because the refreshUser() method refreshes the
user on each request by using the id (as explained above). This gives
us a "fresh" User object.
But Symfony also uses the username, salt, and password to verify that
the User has not changed between requests (it also calls your
AdvancedUserInterface methods if you implement it). Failing to
serialize these may cause you to be logged out on each request. If
your user implements the EquatableInterface, then instead of these
properties being checked, your isEqualTo() method is called, and you
can check whatever properties you want. Unless you understand this,
you probably won't need to implement this interface or worry about it.
Resolved this with insight from #jedge.
Turns out the logout_on_user_change option being true was the cause. Changing this to false resolved the issue. -- I'm not sure what this does as there is little documentation on it, and worryingly this has become true by default in Symfony 4...
Other things we tried were the temporary removal of CSRF, forwarding, and logout event. -- None of these turned out to conflict. We were also able to login programmatically by manually creating a token for a specific user and dispatching an InteractiveLoginEvent, which led us on to the firewall configuration.
I am following the Symfony book and cookbook recipes and I met problem with simple login form - no matter if entered login/pass are valid, message shows up - 'Invalid credentials'. Users are loaded via Doctrine (User class which implements UserInterface). Source codes :
Security file:
providers:
user_provider:
entity:
class: BakaMainBundle:User
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
default:
anonymous: ~
http_basic: ~
provider: user_provider
form_login:
login_path: /login
check_path: /login_check
target_path_parameter: /index/welcome
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
encoders:
Baka\MainBundle\Entity\User:
algorithm: bcrypt
cost: 12
Controller :
class SecurityController extends Controller
{
/**
* #Route("/login", name="login_route")
*/
public function loginAction()
{
$authUtils = $this->get('security.authentication_utils');
$error = $authUtils->getLastAuthenticationError();
$enteredUsername = $authUtils->getLastUsername();
return $this->render('BakaMainBundle::Login.html.twig',
array
(
'last_username' => $enteredUsername,
'error' => $error,
'site' => 'login'
));
}
/**
* #Route("/login_check", name="login_check")
*/
public function loginCheckAction()
{
}
}
User repository :
class UserRepository extends \Doctrine\ORM\EntityRepository implements UserProviderInterface
{
public function loadUserByUsername($username)
{
$user = $this->createQueryBuilder('u')
->where('u.username = :username OR u.email = :email')
->setParameter('username', $username)
->setParameter('email', $username)
->getQuery()
->getOneOrNullResult();
if ($user === null)
{
$returnMessage = sprintf(
'%s - such username of email adress does not exist in database! Try again with other login data.',
$username);
throw new UnsupportedUserException($returnMessage);
}
return $user;
}
public function refreshUser(UserInterface $user)
{
$userClass = get_class($user);
if (!$this->supportsClass($userClass))
{
throw new UnsupportedUserException
(sprintf('Ops! Something goes wrong. Your user class is not supported by security system.'));
}
return $this->find($user->getId());
}
public function supportsClass($userclass)
{
return $this->getEntityName() === $userclass || is_subclass_of($userclass, $this->getEntityName());
}
And the form html tag :
<form action="{{ path('login_check') }}" method="post">
Any suggestions? I will be grateful for resolving my problem.
I think you should use the class namespace instead of the bundle name, when specifying the provider class. Also, you need to specify which property you will be selecting as the "username" from your Entity:
security:
providers:
user_provider:
entity:
class: Baka\MainBundle\Entity\User
property: username (this should be an existing property of your entity class)
Also, your User entity needs to implement Symfony\Component\Security\Core\User\UserInterface (or AdvancedUserInterface). Once you're done with that, everything should work if you have users in the database with a properly encoded password.
You should read:
How to Load Security Users from the Database (the Entity Provider) to understand how to load users from the database
Security to get better understanding of how the security component works and how it should be configured.
I've already identified the reason of issue, and it transpired to be trivial -
field which serves as an Encoded Password row in the DB had 15 characters long limit :
/**
* #ORM\Column(type="string", length=15)
*/
protected $password;
And since '12 rounds' bcrypt needs much more digits to represent plain password, Doctrine was forced to shorten encrypted pass so it was impossible to decode later. After changing to suggested by Symfony size the problem has gone :
/**
* #ORM\Column(type="string", length=4096)
*/
protected $password;
Thank you for all support.
I have an example where I am trying to create an AJAX login using Symfony2 and FOSUserBundle. I am setting my own success_handler and failure_handler under form_login in my security.yml file.
Here is the class:
class AjaxAuthenticationListener implements AuthenticationSuccessHandlerInterface, AuthenticationFailureHandlerInterface
{
/**
* This is called when an interactive authentication attempt succeeds. This
* is called by authentication listeners inheriting from
* AbstractAuthenticationListener.
*
* #see \Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener
* #param Request $request
* #param TokenInterface $token
* #return Response the response to return
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
if ($request->isXmlHttpRequest()) {
$result = array('success' => true);
$response = new Response(json_encode($result));
$response->headers->set('Content-Type', 'application/json');
return $response;
}
}
/**
* This is called when an interactive authentication attempt fails. This is
* called by authentication listeners inheriting from
* AbstractAuthenticationListener.
*
* #param Request $request
* #param AuthenticationException $exception
* #return Response the response to return
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
if ($request->isXmlHttpRequest()) {
$result = array('success' => false, 'message' => $exception->getMessage());
$response = new Response(json_encode($result));
$response->headers->set('Content-Type', 'application/json');
return $response;
}
}
}
This works great for handling both successful and failed AJAX login attempts. However, when enabled - I am unable to login via the standard form POST method (non-AJAX). I receive the following error:
Catchable Fatal Error: Argument 1 passed to Symfony\Component\HttpKernel\Event\GetResponseEvent::setResponse() must be an instance of Symfony\Component\HttpFoundation\Response, null given
I'd like for my onAuthenticationSuccess and onAuthenticationFailure overrides to only be executed for XmlHttpRequests (AJAX requests) and to simply hand the execution back to the original handler if not.
Is there a way to do this?
TL;DR I want AJAX requested login attempts to return a JSON response for success and failure but I want it to not affect standard login via form POST.
David's answer is good, but it's lacking a little detail for newbs - so this is to fill in the blanks.
In addition to creating the AuthenticationHandler you'll need to set it up as a service using the service configuration in the bundle where you created the handler. The default bundle generation creates an xml file, but I prefer yml. Here's an example services.yml file:
#src/Vendor/BundleName/Resources/config/services.yml
parameters:
vendor_security.authentication_handler: Vendor\BundleName\Handler\AuthenticationHandler
services:
authentication_handler:
class: %vendor_security.authentication_handler%
arguments: [#router]
tags:
- { name: 'monolog.logger', channel: 'security' }
You'd need to modify the DependencyInjection bundle extension to use yml instead of xml like so:
#src/Vendor/BundleName/DependencyInjection/BundleExtension.php
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
Then in your app's security configuration you set up the references to the authentication_handler service you just defined:
# app/config/security.yml
security:
firewalls:
secured_area:
pattern: ^/
anonymous: ~
form_login:
login_path: /login
check_path: /login_check
success_handler: authentication_handler
failure_handler: authentication_handler
namespace YourVendor\UserBundle\Handler;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class AuthenticationHandler
implements AuthenticationSuccessHandlerInterface,
AuthenticationFailureHandlerInterface
{
private $router;
public function __construct(Router $router)
{
$this->router = $router;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
if ($request->isXmlHttpRequest()) {
// Handle XHR here
} else {
// If the user tried to access a protected resource and was forces to login
// redirect him back to that resource
if ($targetPath = $request->getSession()->get('_security.target_path')) {
$url = $targetPath;
} else {
// Otherwise, redirect him to wherever you want
$url = $this->router->generate('user_view', array(
'nickname' => $token->getUser()->getNickname()
));
}
return new RedirectResponse($url);
}
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
if ($request->isXmlHttpRequest()) {
// Handle XHR here
} else {
// Create a flash message with the authentication error message
$request->getSession()->setFlash('error', $exception->getMessage());
$url = $this->router->generate('user_login');
return new RedirectResponse($url);
}
}
}
If you want the FOS UserBundle form error support, you must use:
$request->getSession()->set(SecurityContext::AUTHENTICATION_ERROR, $exception);
instead of:
$request->getSession()->setFlash('error', $exception->getMessage());
In the first answer.
(of course remember about the header: use Symfony\Component\Security\Core\SecurityContext;)
I handled this entirely with javascript:
if($('a.login').length > 0) { // if login button shows up (only if logged out)
var formDialog = new MyAppLib.AjaxFormDialog({ // create a new ajax dialog, which loads the loginpage
title: 'Login',
url: $('a.login').attr('href'),
formId: '#login-form',
successCallback: function(nullvalue, dialog) { // when the ajax request is finished, look for a login error. if no error shows up -> reload the current page
if(dialog.find('.error').length == 0) {
$('.ui-dialog-content').slideUp();
window.location.reload();
}
}
});
$('a.login').click(function(){
formDialog.show();
return false;
});
}
Here is the AjaxFormDialog class. Unfortunately I have not ported it to a jQuery plugin by now... https://gist.github.com/1601803
You must return a Response object in both case (Ajax or not). Add an `else' and you're good to go.
The default implementation is:
$response = $this->httpUtils->createRedirectResponse($request, $this->determineTargetUrl($request));
in AbstractAuthenticationListener::onSuccess
I made a little bundle for new users to provide an AJAX login form : https://github.com/Divi/AjaxLoginBundle
You just have to replace to form_login authentication by ajax_form_login in the security.yml.
Feel free to suggest new feature in the Github issue tracker !
This may not be what the OP asked, but I came across this question, and thought others might have the same problem that I did.
For those who are implementing an AJAX login using the method that is described in the accepted answer and who are ALSO using AngularJS to perform the AJAX request, this won't work by default. Angular's $http does not set the headers that Symfony is using when calling the $request->isXmlHttpRequest() method. In order to use this method, you need to set the appropriate header in the Angular request. This is what I did to get around the problem:
$http({
method : 'POST',
url : {{ path('login_check') }},
data : data,
headers: {'X-Requested-With': 'XMLHttpRequest'}
})
Before you use this method, be aware that this header does not work well with CORS. See this question