In Symfony, after a user successfully log out, how to display a success message like "you have successfully logged out" ?
1) Create a new service to handle the logout success event.
In services.yml add the service:
logout_success_handler:
class: Path\To\YourBundle\Services\LogoutSuccessHandler
arguments: ['#security.http_utils']
And add the class, replacing /path/to/your/login with the url of your login page (in the last line of the controller):
<?php
namespace Path\To\YourBundle\Services;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface;
class LogoutSuccessHandler implements LogoutSuccessHandlerInterface
{
protected $httpUtils;
protected $targetUrl;
/**
* #param HttpUtils $httpUtils
*/
public function __construct(HttpUtils $httpUtils)
{
$this->httpUtils = $httpUtils;
$this->targetUrl = '/path/to/your/login?logout=success';
}
/**
* {#inheritdoc}
*/
public function onLogoutSuccess(Request $request)
{
$response = $this->httpUtils->createRedirectResponse($request, $this->targetUrl);
return $response;
}
}
2) Configure your security.yml to use the custom LogoutSuccessHandler just created:
firewalls:
# ...
your_firewall:
# ...
logout:
# ...
success_handler: logout_success_handler
3) In the twig template of your login page add:
{% if app.request.get('logout') == "success" %}
<p>You have successfully logged out!</p>
{% endif %}
Imho, there is a definitively simpler way. In security.yaml, define a route to redirect to after logout thanks to the target key :
security:
firewalls:
main:
[...]
logout:
path: /logout
target: /logout_message
Then in a controller (SecurityController.php is fine for this), define this action, wich only add the flash message and then redirect where you want (home in this exemple) :
/**
* #Route("/logout_message", name="logout_message")
*/
public function logoutMessage()
{
$this->addFlash('success', "You've been disconnected. Bye bye !");
return $this->redirectToRoute('home');
}
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);
}
}
I am working on a symfony application where my goal is no matter what page the user is on it will navigate to the locale version of the page.
For example, if the user navigates to "/" the home page, it will redirect to "/en/"
If they are on "/admin" page it will redirect to "/en/admin", in such a way that the _locale property is set from the route.
Also it needs to determine the locale if they visit /admin from the users browser since no locale was determined so it knows which page to redirect to.
Currently my default controller looks like below since I'm testing. I'm using the dev mode & profiler to test that translations are working in correct.
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class DefaultController extends Controller
{
/**
* #Route("/", name="homepage")
* #Route("/{_locale}/", name="homepage_locale")
*/
public function indexAction(Request $request)
{
$translated = $this->get('translator')->trans('Symfony is great');
// replace this example code with whatever you need
return $this->render('default/index.html.twig', [
'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
'translated' => $translated
]);
}
}
This current method will keep the user at "/" if they navigate there, but I want to have it redirect to "/en/". This should work for other pages too, like /admin, or /somepath/pathagain/article1 (/en/admin , /en/somepath/pathagain/article1)
How would I do this?
References I've read that did not help:
Symfony2 Use default locale in routing (one URL for one language)
Symfony2 default locale in routing
::Update::
I have not solved my issue but I've come close as well as learned a few tricks to be more efficient.
DefaultController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class DefaultController extends Controller
{
/**
* #Route("/", name="home", defaults={"_locale"="en"}, requirements={"_locale" = "%app.locales%"})
* #Route("/{_locale}/", name="home_locale", requirements={"_locale" = "%app.locales%"})
*/
public function indexAction(Request $request)
{
$translated = $this->get('translator')->trans('Symfony is great');
// replace this example code with whatever you need
return $this->render('default/index.html.twig', [
'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
'translated' => $translated
]);
}
/**
* #Route("/admin", name="admin", defaults={"_locale"="en"}, requirements={"_locale" = "%app.locales%"})
* #Route("/{_locale}/admin", name="admin_locale", requirements={"_locale" = "%app.locales%"})
*/
public function adminAction(Request $request)
{
$translated = $this->get('translator')->trans('Symfony is great');
// replace this example code with whatever you need
return $this->render('default/index.html.twig', [
'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
'translated' => $translated
]);
}
}
?>
Config.yml
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: services.yml }
# Put parameters here that don't need to change on each machine where the app is deployed
# http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
locale: en
app.locales: en|es|zh
framework:
#esi: ~
translator: { fallbacks: ["%locale%"] }
secret: "%secret%"
router:
resource: "%kernel.root_dir%/config/routing.yml"
strict_requirements: ~
form: ~
csrf_protection: ~
validation: { enable_annotations: true }
#serializer: { enable_annotations: true }
templating:
engines: ['twig']
#assets_version: SomeVersionScheme
default_locale: "%locale%"
trusted_hosts: ~
trusted_proxies: ~
session:
# handler_id set to null will use default session handler from php.ini
handler_id: ~
save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%"
fragments: ~
http_method_override: true
assets: ~
# Twig Configuration
twig:
debug: "%kernel.debug%"
strict_variables: "%kernel.debug%"
# Doctrine Configuration
doctrine:
dbal:
driver: pdo_mysql
host: "%database_host%"
port: "%database_port%"
dbname: "%database_name%"
user: "%database_user%"
password: "%database_password%"
charset: UTF8
# if using pdo_sqlite as your database driver:
# 1. add the path in parameters.yml
# e.g. database_path: "%kernel.root_dir%/data/data.db3"
# 2. Uncomment database_path in parameters.yml.dist
# 3. Uncomment next line:
# path: "%database_path%"
orm:
auto_generate_proxy_classes: "%kernel.debug%"
naming_strategy: doctrine.orm.naming_strategy.underscore
auto_mapping: true
# Swiftmailer Configuration
swiftmailer:
transport: "%mailer_transport%"
host: "%mailer_host%"
username: "%mailer_user%"
password: "%mailer_password%"
spool: { type: memory }
Notice under parameters the value app.locales: en|es|zh. This is now a value I can reference whenever I create my routes if I plan to support more locales in the future which I do. Those routes are english, spanish, chinese in that order for those curious. In the DefaultController in the annotations the "%app.locales%" is the part that references the config parameter.
The problem with my current method is going to /admin for example does not redirect the user to /{browsers locale}/admin, which would be the more elegant solution to keep everything organized... but at least the routes work. Still looking for better solution.
****Update****
I think I may have possibly found the answer here as the bottom answer given (Add locale and requirements to all routes - Symfony2), the answer by Athlan. Just not sure how to implement this in symfony 3 as his directions were not clear enough to me.
I think this article might help also (http://symfony.com/doc/current/components/event_dispatcher/introduction.html)
I don't have enough reputation to add a comment to the correct solution. So I'm adding a new answer
You can add "prefix: /{_locale}" at app/config/routing.yml like this:
app:
resource: "#AppBundle/Controller/"
type: annotation
prefix: /{_locale}
So you don't need to add it to every route to every action. For the following steps. Thank you very much it's perfect.
After 12 hours of looking into this I finally found an acceptable solution. Please post revised versions of this solution if you can make it more efficient.
Some things to note, my solution is particular to my need. What it does is force any URL to go to a localized version if it exists.
This requires some conventions to be followed when you create routes.
DefaultController.php
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class DefaultController extends Controller
{
/**
* #Route("/{_locale}/", name="home_locale", requirements={"_locale" = "%app.locales%"})
*/
public function indexAction(Request $request)
{
$translated = $this->get('translator')->trans('Symfony is great');
// replace this example code with whatever you need
return $this->render('default/index.html.twig', [
'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
'translated' => $translated
]);
}
/**
* #Route("/{_locale}/admin", name="admin_locale", requirements={"_locale" = "%app.locales%"})
*/
public function adminAction(Request $request)
{
$translated = $this->get('translator')->trans('Symfony is great');
// replace this example code with whatever you need
return $this->render('default/index.html.twig', [
'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
'translated' => $translated
]);
}
}
?>
Notice that both routes always start with "/{_locale}/". For this to work every route in your project needs to have this. You just put the real route name afterwards. For me I was okay with this scenario. You can modify my solution to fit your needs easily enough.
The first step is to create a listen on the httpKernal to intercept requests before they go to the routers to render them.
LocaleRewriteListener.php
<?php
//src/AppBundle/EventListener/LocaleRewriteListener.php
namespace AppBundle\EventListener;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\RouteCollection;
class LocaleRewriteListener implements EventSubscriberInterface
{
/**
* #var Symfony\Component\Routing\RouterInterface
*/
private $router;
/**
* #var routeCollection \Symfony\Component\Routing\RouteCollection
*/
private $routeCollection;
/**
* #var string
*/
private $defaultLocale;
/**
* #var array
*/
private $supportedLocales;
/**
* #var string
*/
private $localeRouteParam;
public function __construct(RouterInterface $router, $defaultLocale = 'en', array $supportedLocales = array('en'), $localeRouteParam = '_locale')
{
$this->router = $router;
$this->routeCollection = $router->getRouteCollection();
$this->defaultLocale = $defaultLocale;
$this->supportedLocales = $supportedLocales;
$this->localeRouteParam = $localeRouteParam;
}
public function isLocaleSupported($locale)
{
return in_array($locale, $this->supportedLocales);
}
public function onKernelRequest(GetResponseEvent $event)
{
//GOAL:
// Redirect all incoming requests to their /locale/route equivlent as long as the route will exists when we do so.
// Do nothing if it already has /locale/ in the route to prevent redirect loops
$request = $event->getRequest();
$path = $request->getPathInfo();
$route_exists = false; //by default assume route does not exist.
foreach($this->routeCollection as $routeObject){
$routePath = $routeObject->getPath();
if($routePath == "/{_locale}".$path){
$route_exists = true;
break;
}
}
//If the route does indeed exist then lets redirect there.
if($route_exists == true){
//Get the locale from the users browser.
$locale = $request->getPreferredLanguage();
//If no locale from browser or locale not in list of known locales supported then set to defaultLocale set in config.yml
if($locale=="" || $this->isLocaleSupported($locale)==false){
$locale = $request->getDefaultLocale();
}
$event->setResponse(new RedirectResponse("/".$locale.$path));
}
//Otherwise do nothing and continue on~
}
public static function getSubscribedEvents()
{
return array(
// must be registered before the default Locale listener
KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
);
}
}
Finally you set the services.yml to start the listener up.
Services.yml
# Learn more about services, parameters and containers at
# http://symfony.com/doc/current/book/service_container.html
parameters:
# parameter_name: value
services:
# service_name:
# class: AppBundle\Directory\ClassName
# arguments: ["#another_service_name", "plain_value", "%parameter_name%"]
appBundle.eventListeners.localeRewriteListener:
class: AppBundle\EventListener\LocaleRewriteListener
arguments: ["#router", "%kernel.default_locale%", "%locale_supported%"]
tags:
- { name: kernel.event_subscriber }
Also in the config.yml you will want to add the following under parameters:
config.yml
parameters:
locale: en
app.locales: en|es|zh
locale_supported: ['en','es','zh']
I wanted there to be only one place you define the locales but I wound up having to do 2...but at least they are in the same spot so easy to change.
app.locales is used in default controller (requirements={"_locale" = "%app.locales%"}) and locale_supported is used in the LocaleRewriteListener. If it detects a locale that is not in the list it will fallback to the default locale, which in this case is the value of locale:en.
app.locales is nice with the requirements command because it will cause a 404 for any locales that do not match.
If you are using forms and have a login you will need to do the following to your security.yml
Security.yml
# To get started with security, check out the documentation:
# http://symfony.com/doc/current/book/security.html
security:
encoders:
Symfony\Component\Security\Core\User\User:
algorithm: bcrypt
cost: 12
AppBundle\Entity\User:
algorithm: bcrypt
cost: 12
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
# http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
database:
entity: { class: AppBundle:User }
#property: username
# if you're using multiple entity managers
# manager_name: customer
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: true
form_login:
check_path: login_check
login_path: login_route
provider: database
csrf_token_generator: security.csrf.token_manager
remember_me:
secret: '%secret%'
lifetime: 604800 # 1 week in seconds
path: /
httponly: false
#httponly false does make this vulnerable in XSS attack, but I will make sure that is not possible.
logout:
path: /logout
target: /
access_control:
# require ROLE_ADMIN for /admin*
#- { path: ^/login, roles: ROLE_ADMIN }
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/(.*?)/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }
The important change to note here is that (.*?)/login will authenticate anonymously so your users can still login. This does mean that routes like..dogdoghere/login could trigger, but the requirements I will show you shortly on the login routes prevent this and will throw 404 errors. I like this solution with the (.*?) versus [a-z]{2} incase you wanted to use en_US type locales.
SecurityController.php
<?php
// src/AppBundle/Controller/SecurityController.php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class SecurityController extends Controller
{
/**
* #Route("{_locale}/login", name="login_route", defaults={"_locale"="en"}, requirements={"_locale" = "%app.locales%"})
*/
public function loginAction(Request $request)
{
$authenticationUtils = $this->get('security.authentication_utils');
// 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 entered by the user
'last_username' => $lastUsername,
'error' => $error,
)
);
}
/**
* #Route("/{_locale}/login_check", name="login_check", defaults={"_locale"="en"}, requirements={"_locale" = "%app.locales%"})
*/
public function loginCheckAction()
{
// this controller will not be executed,
// as the route is handled by the Security system
}
/**
* #Route("/logout", name="logout")
*/
public function logoutAction()
{
}
}
?>
Note that even these paths use {_locale} in front. I like this however so I can give custom logins for different locales. Just keep that in mind. The only route that does not need the locale is logout which works just fine since its really only an intercept route for the security system. Also notice it uses the requirements which is set from the config.yml, so you only have to edit it in one place for all the routes across your projects.
Hope this helps someone trying to do what I was doing!
NOTE:: To test this easily I use 'Quick Language Switcher' extension for Google Chrome, which changes the accept-language header on all requests.
final function smallResumeOfResearching($localeRewrite, $opinion = 'IMHO') :)
The method, provided by mr. Joseph working great with routes like /{route_name}, or /, but not with routes like /article/slug/other.
If we use modified mr.Joseph's method, provided by https://stackoverflow.com/a/37168304/9451542, we will lost profiler and debugger in dev mode.
If we want more flexible solution, onKernelRequest method can be modified like this (thanks to mr. Joseph, thanks to https://stackoverflow.com/a/37168304/9451542):
public function onKernelRequest(GetResponseEvent $event)
{
$pathInfo = $event->getRequest()->getPathinfo();
$baseUrl = $event->getRequest()->getBaseUrl();
$checkLocale = explode('/', ltrim($pathInfo, '/'))[0];
//Or some other logic to detect/provide locale
if (($this->isLocaleSupported($checkLocale) == false) && ($this->defaultLocale !== $checkLocale)) {
if ($this->isProfilerRoute($checkLocale) == false) {
$locale = $this->defaultLocale;
$event->setResponse(new RedirectResponse($baseUrl . '/' . $locale . $pathInfo));
}
/* Or with matcher:
try {
//Try to match the path with the locale prefix
$this->matcher->match('/' . $locale . $pathInfo);
//$event->setResponse(new RedirectResponse($baseUrl . '/' . $locale . $pathInfo));
} catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
} catch (\Symfony\Component\Routing\Exception\MethodNotAllowedException $e) {
}
*/
}
}
note: $this->profilerRoutes = array('_profiler', '_wdt', '_error');
Thanks to Susana Santos for pointing to simple config method :)
Small improvement for Symfony 3.4:
Be sure, that the getSubscribedEvents() will register LocaleRewriteListener BEFORE RouterListener::onKernelRequest and BEFORE LocaleListener::onKernelRequest. Integer 17 must be greater than RouterListener::onKernelRequest priotity. Otherwise you will got 404.
bin/console debug:event-dispatcher
Service definition in services.yml must be (depends on Symfony configuration):
AppBundle\EventListener\LocaleRewriteListener:
arguments: ['#router', '%kernel.default_locale%', '%locale_supported%']
tags:
- { name: kernel.event_subscriber, event: kernel.request }
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 am trying to combine FOSUserBundle and HWIOAuthBundle following articles like https://gist.github.com/danvbe/4476697. However, I do not want the automatic registration of OAuth2 authenticated new users: additional information should be provided by the user.
Desired result
I would for example want the following information for a registered user:
(Username, although I'd rather just use e-mail)
Display name (required)
Profile picture (required)
Email address (required if no Facebook-id)
Password (required if no Facebook-id)
Facebook-id (required if no email address)
Now, when a user authenticates through Facebook and the user does not exist yet, I want a registration form to fill out the missing information (display name and profile picture). Only after this, the new FOSUser should be created.
In most tutorials, fields like Profile picture and Email address are automatically populated with the Facebook information. This is not always desirable nor possible.
Also, think of things like accepting Terms of Agreement and rules you wish to show before the user is created.
Possible approaches
A solution would be, I think, to create a new sort-of AnonymousToken, the OAuthenticatedToken, which holds the relevant OAuth2 information but does not count an authenticaton. Then, make all pages check for this kind of authentication and let other pages redirect to OAuth-registration-page. However, this seems an unnecessarily complicated solution to me.
Another solution would probably be to write the code from scratch and not use the two bundles mentioned. I really hope this is not necessary.
Q: How can I insert the registration-completion-code in the rest of the login flow?
(I'd love to share some code, but since it's the very concept I need help at, I don't have a lot to show.)
Edit: Solution
Following Derick's adivce, I got the basics working like this:
The Custom user provider saves the information (sadly, no access to the raw token so I cannot yet log the user in after registering):
class UserProvider extends FOSUBUserProvider {
protected $session;
public function __construct(Session $session, UserManagerInterface $userManager, array $properties) {
$this->session = $session;
parent::__construct( $userManager, $properties );
}
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
try {
return parent::loadUserByOAuthUserResponse($response);
}
catch ( AccountNotLinkedException $e ) {
$this->session->set( 'oauth.resource', $response->getResourceOwner()->getName() );
$this->session->set( 'oauth.id', $response->getResponse()['id'] );
throw $e;
}
}
}
Custom failure handler:
<?php
// OAuthFailureHandler.php
class OAuthFailureHandler implements AuthenticationFailureHandlerInterface {
public function onAuthenticationFailure( Request $request, AuthenticationException $exception) {
if ( !$exception instanceof AccountNotLinkedException ) {
throw $exception;
}
return new RedirectResponse( 'fb-register' );
}
}
Both are registered as a service:
# services.yml
services:
app.userprovider:
class: AppBundle\Security\Core\User\UserProvider
arguments: [ "#session", "#fos_user.user_manager", {facebook: facebookID} ]
app.oauthfailurehandler:
class: AppBundle\Security\Handler\OAuthFailureHandler
arguments: ["#security.http_utils", {}, "#service_container"]
And configured in security config:
# security.yml
security:
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
firewalls:
main:
form_login:
provider: fos_userbundle
csrf_provider: form.csrf_provider
login_path: /login
check_path: /login_check
default_target_path: /profile
oauth:
login_path: /login
check_path: /login_check
resource_owners:
facebook: hwi_facebook_login
oauth_user_provider:
service: app.userprovider
failure_handler: app.oauthfailurehandler
anonymous: true
logout:
path: /logout
target: /login
At /fb-register, I let the user enter a username and save the user myself:
/**
* #Route("/fb-register", name="hwi_oauth_register")
*/
public function registerOAuthAction(Request $request) {
$session = $request->getSession();
$resource = $session->get('oauth.resource');
if ( $resource !== 'facebook' ) {
return $this->redirectToRoute('home');
}
$userManager = $this->get('fos_user.user_manager');
$newUser = $userManager->createUser();
$form = $this->createForm(new RegisterOAuthFormType(), $newUser);
$form->handleRequest($request);
if ( $form->isValid() ) {
$newUser->setFacebookId( $session->get('oauth.id') );
$newUser->setEnabled(true);
$userManager->updateUser( $newUser );
try {
$this->container->get('hwi_oauth.user_checker')->checkPostAuth($newUser);
} catch (AccountStatusException $e) {
// Don't authenticate locked, disabled or expired users
return;
}
$session->remove('oauth.resource');
$session->remove('oauth.id');
$session->getFlashBag()
->add('success', 'You\'re succesfully registered!' );
return $this->redirectToRoute('home');
}
return $this->render( 'default/register-oauth.html.twig', array(
'form' => $form->createView()
) );
}
The user is not logged in afterwards, which is too bad. Also, the normal fosub functionality (editing profile, changing password) does not work out of the box anymore.
I'm simply using the username as the displayname, not sure why I didn't see that before.
Step 1:
Create your own user provider. Extend the OAuthUserProvider and customize to your needs. If the user successfully oauthed in, throw a specific exception (probably the accountnotlinkedException) and toss all relevant data about the login somewhere
Step 2:
Create your own authentication failure handler. Check to make sure the error being thrown is the specific one you threw in step 1.
In here you will redirect to your fill in additional info page.
This is how to register you custom handlers:
#security.yml
firewall:
main:
oauth:
success_handler: authentication_handler
failure_handler: social_auth_failure_handler
#user bundle services.yml (or some other project services.yml)
services:
authentication_handler:
class: ProjectName\UserBundle\Handler\AuthenticationHandler
arguments: ["#security.http_utils", {}, "#service_container"]
tags:
- { name: 'monolog.logger', channel: 'security' }
social_auth_failure_handler:
class: ProjectName\UserBundle\Handler\SocialAuthFailureHandler
arguments: ["#security.http_utils", {}, "#service_container"]
tags:
- { name: 'monolog.logger', channel: 'security' }
Step 3:
Create your fill in additional info page. Pull all relevant data that you stored back in step 1 and create the user if everything checks out.
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