On Symfony 5, using the built-in login system, it seems impossible to add a confirmation message after logout. I have followed strictly the steps described on the official website. Unfortunately, the method logout inside the SecurityController is useless. I'm redirected directly on the login page.
Here you will have 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
# used to reload user from session & other features (e.g. switch_user)
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: lazy
provider: app_user_provider
guard:
authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: logout
target: login
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: home
always_remember_me: true
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/logout$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/admin, roles: [IS_AUTHENTICATED_FULLY, ROLE_ADMIN] }
- { path: ^/profile, roles: [IS_AUTHENTICATED_FULLY, ROLE_USER] }
And the Controller :
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends AbstractController
{
public function login(AuthenticationUtils $authenticationUtils): Response
{
if ($this->getUser()) {
return $this->redirectToRoute('home');
}
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
return $this->render('security/login.html.twig', ['last_username' => null, 'error' => $error]);
}
public function logout()
{
throw new \Exception('Don\'t forget to activate logout in security.yaml');
}
}
?>
Thank you for your help !
For anyone who's wondering how exactly they can implement the external redirect with the new logout customization:
As stated in the documentation, create a new CustomLogoutListener class and add it to your services.yml configuration.
The CustomLogoutListener class should implement the onSymfonyComponentSecurityHttpEventLogoutEvent method, which will receive the LogoutEvent as a parameter, that will allow you to set the response:
namespace App\EventListener;
use JetBrains\PhpStorm\NoReturn;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Event\LogoutEvent;
class CustomLogoutListener
{
/**
* #param LogoutEvent $logoutEvent
* #return void
*/
#[NoReturn]
public function onSymfonyComponentSecurityHttpEventLogoutEvent(LogoutEvent $logoutEvent): void
{
$logoutEvent->setResponse(new RedirectResponse('https://where-you-want-to-redirect.com', Response::HTTP_MOVED_PERMANENTLY));
}
}
# config/services.yaml
services:
# ...
App\EventListener\CustomLogoutListener:
tags:
- name: 'kernel.event_listener'
event: 'Symfony\Component\Security\Http\Event\LogoutEvent'
dispatcher: security.event_dispatcher.main
logout method in SecurityController won’t actually get hit because Symfony will intercept the request.
if you need to do something after logout you can use logout success handler
namespace App\Logout;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface;
class MyLogoutSuccessHandler implements LogoutSuccessHandlerInterface
{
/**
* {#inheritdoc}
*/
public function onLogoutSuccess(Request $request)
{
// you can do anything here
return new Response('logout successfully'); // or render a twig template here, it's up to you
}
}
and you can register your logout success handler to security.yaml
firewalls:
main:
anonymous: lazy
provider: app_user_provider
guard:
authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: logout
success_handler: App\Logout\MyLogoutSuccessHandler # assume you have enable autoconfigure for servicess or you need to register the handler
As of version 5.1 LogoutSuccessHandlerInterface is deprecated, it is recommended to use LogoutEvent.
Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface is deprecated
But there are no examples or information about LogoutEvent in the official documentation
Thanks to Indra Gunawan, this solution works. My goal was to redirect to the login page with a message like "You've been successfully logged out".
In that case, the LogoutSuccessHandler must be adapted to route to the login page :
namespace App\Logout;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
class MyLogoutSuccessHandler extends AbstractController implements LogoutSuccessHandlerInterface
{
private $urlGenerator;
public function __construct(UrlGeneratorInterface $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
public function onLogoutSuccess(Request $request)
{
return new RedirectResponse($this->urlGenerator->generate('login', ['logout' => 'success']));
}
}
The route login need to be defined in routes.yaml :
login:
path: /login
controller: App\Controller\SecurityController::login
logout:
path: /logout
methods: GET
In that case, when logout, you will be redirected on an url like : /login?logout=success
Lastely, you can catch logout parameter in twig template like :
{%- if app.request('logout') -%}
<div class="alert alert-success">{% trans %}Logout successful{% endtrans %}</div>
{%- endif -%}
Here is the doc for the LogoutEvent :
https://symfony.com/blog/new-in-symfony-5-1-simpler-logout-customization
You must create an event and implement the onSymfonyComponentSecurityHttpEventLogoutEvent's method.
Its work for me
If you are using Symfony 6 and above you can use as bellow
<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
class LogoutListener implements EventSubscriberInterface
{
public function onLogout(LogoutEvent $logoutEvent): void
{
// Do your stuff
}
public static function getSubscribedEvents(): array
{
return [
LogoutEvent::class => 'onLogout',
];
}
}
Related
I am trying to implement a simple LDAP authentication in my Symfony application.
A user should first be authenticated against LDAP, whereby a custom user entity should be returned from the database.
If a user is not in the database but could be authenticated successfully, I want to create the user.
Except for the automatic creation of the user in the database, it works so far.
providers:
users_db:
entity:
# the class of the entity that represents users
class: 'App\Entity\User'
# the property to query by - e.g. email, username, etc
property: 'username'
users_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
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users_db
http_basic_ldap:
service: Symfony\Component\Ldap\Ldap
dn_string: '%env(LDAP_DN_STRING)%'
With the above configuration, the auth runs against LDAP but the user comes from the DB. So if I have not created a corresponding user, the login attempt won't work.
I have tried the automatic creation of users via a UserChecker, a UserProvider and a LoginEventListener (listens to event onAuthenticationSuccess), unfortunately without success.
The onAuthenticationSuccess event is only called after successful authentication and can therefore not be used in the configuration described above, because the users_db provider does not (yet) contain the user, even if the LDAP Basic auth works.
Then I tried it with a UserChecker and chained provider [users_ldap, users_db]. This is also executed, but then I no longer get a user object but an LDAP user. So I tried to create my own UserProvider, unfortunately without success.
If anyone knows a good way to do this, I would appreciate an answer or a short comment. Thank you!
I've faced your problem before, I didn't able to save their informations automaticly in my local database, so I followes these steps:
signing in by using cas auth and Ldap as a user provider.
redirect them to form and retrieve them inside the fields like this:
<div>
{{ form_row(user_form.uid, { label: 'UID :*',
required: 'true',
attr: {
value : app.user.uid,
readonly: true
}
}) }}
</div>
After submiting the form thier roles will change from "ROLE_VISIT" to "ROLE_USER" and all LDAP information will be saved at my local database.
you can save some data privatly like this:
<div class="invisible" >
{{ form_row(user_form.genre,{
attr: { value : app.user.supannCivilite }
}) }}
</div>
I'm using this bundle for LDAP so please let me know if you need any help!
Also, you can take a look at my security configration as below, hope it will be useful:
providers:
chain_provider:
chain:
providers: [in_memory, database, ldap]
in_memory:
memory:
users:
__NO_USER__:
password:
roles: ROLE_ANON
database:
entity:
class: App\Entity\User
property: uid
ldap:
id: ldap_user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
l3_firewall:
pattern: ^/
provider: chain_provider
security: true
guard:
authenticators:
- cas.security.authentication.authenticator
logout:
path: /logout
success_handler: authentication_handler
invalidate_session: false
access_denied_handler: App\EventListener\AccessDeniedListener
main:
pattern: ^/
security: true
lazy: true
provider: chain_provider
guard:
authenticators:
- cas.security.authentication.authenticator
Thanks to this answer I now got it working.
config/services.yaml
services:
App\Security\UserProvider:
arguments:
$em: '#Doctrine\ORM\EntityManagerInterface'
$ldap: '#Symfony\Component\Ldap\Ldap'
$baseDn: "%env(LDAP_BASE_DN)%"
$searchDn: "%env(LDAP_SEARCH_DN)%"
$searchPassword: "%env(LDAP_SEARCH_PASSWORD)%"
$defaultRoles: ["ROLE_USER"]
$uidKey: "uid"
$extraFields: []
App\EventListener\LoginListener:
arguments:
- "#doctrine.orm.entity_manager"
config/packages/security.yml
security:
enable_authenticator_manager: true
password_hashers:
App\Entity\User: 'auto'
providers:
users:
id: App\Security\UserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users
stateless: false
http_basic_ldap:
service: Symfony\Component\Ldap\Ldap
dn_string: 'uid={username},ou=accounts,dc=example,dc=com'
src/Security/UserProvider.php
<?php
namespace App\Security;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Ldap\Security\LdapUserProvider;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Ldap\Security\LdapUser;
class UserProvider extends LdapUserProvider
{
private $em;
public function __construct(EntityManagerInterface $em, Ldap $ldap, string $baseDn, string $searchDn = null, string $searchPassword = null, array $defaultRoles = [], string $uidKey = null, string $filter = null, string $passwordAttribute = null, array $extraFields = [])
{
parent::__construct($ldap, $baseDn, $searchDn, $searchPassword, $defaultRoles, $uidKey, $filter, $passwordAttribute, $extraFields);
$this->em = $em;
}
/**
* Refreshes the user after being reloaded from the session.
*
* #return UserInterface
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
$refreshUser = $this->em->getRepository(User::class)->findOneBy(['username' => $user->getUserIdentifier()]);
return $refreshUser;
}
/**
* Tells Symfony to use this provider for this User class.
*/
public function supportsClass(string $class): bool
{
return User::class === $class || is_subclass_of($class, User::class) || LdapUser::class === $class || is_subclass_of($class, LdapUser::class);
}
}
src/EventListener/LoginListener.php
<?php
namespace App\EventListener;
use App\Entity\User;
use Doctrine\ORM\EntityManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Ldap\Security\LdapUser;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class LoginListener implements EventSubscriberInterface
{
private $em;
private $tokenStorage;
function __construct(EntityManager $em, TokenStorageInterface $tokenStorage)
{
$this->em = $em;
$this->tokenStorage = $tokenStorage;
}
public static function getSubscribedEvents(): array
{
return [LoginSuccessEvent::class => 'onLoginSuccess'];
}
public function onLoginSuccess(LoginSuccessEvent $loginSuccessEvent)
{
$ldapUser = $loginSuccessEvent->getAuthenticatedToken()->getUser();
if (!($ldapUser instanceof LdapUser)) {
return;
}
$localUser = $this->em->getRepository(User::class)->findOneBy(['username' => $ldapUser->getUserIdentifier()]);
if (!$localUser) {
// No local user found in database -> create new user
$localUser = new User();
$localUser->setUsername($ldapUser->getUserIdentifier());
}
// We don't store user passwords -> generate random token
$rmdBytes = random_bytes(32);
$localUser->setPassword($rmdBytes);
$this->em->persist($localUser);
$this->em->flush();
// Login user
$token = new UsernamePasswordToken($localUser, $rmdBytes, 'main', $localUser->getRoles());
$this->tokenStorage->setToken($token);
}
}
I also implemented EquatableInterface in the User entity as suggested in the referenced stackoverflow.
I want to use the security.interactive_login event to update my User's last login field.
The event is successfully registered:
php bin/console debug:event-dispatcher security.interactive_login
Registered Listeners for "security.interactive_login" Event
===========================================================
------- ------------------------------------------------------------------------ ----------
Order Callable Priority
------- ------------------------------------------------------------------------ ----------
#1 App\EventSubscriber\UserLocaleSubscriber::onSecurityInteractiveLogin() 0
------- ------------------------------------------------------------------------ ----------
But it lands on Not called listeners in the Symfony profiler.
This is the event subscriber:
class UserLocaleSubscriber implements EventSubscriberInterface
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
/** #var User $user */
$user = $event->getAuthenticationToken()->getUser();
$user->setLastLoginAt(new DateTime());
$this->em->persist($user);
$this->em->flush();
}
public static function getSubscribedEvents()
{
return [
SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin',
];
}
}
And there is my security.yaml file:
security:
enable_authenticator_manager: true
encoders:
App\Entity\User:
algorithm: auto
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js|fonts)/
security: false
main:
lazy: true
provider: app_user_provider
guard:
authenticators:
- App\Security\LoginAuthenticator
logout:
path: app_logout
target: app_login # where to redirect after logout
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: /
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/(?!login), roles: ROLE_ADMIN }
The LoginAuthenticator class is Symfony's default generated one.
Why the interactive login event is not called?
When using the new(ish) authentication manager, the INTERACTIVE_LOGIN event is replaced with the LoginSuccessEvent.
# my subscriber
public static function getSubscribedEvents()
{
return [
//SecurityEvents::INTERACTIVE_LOGIN => 'onSecurityInteractiveLogin',
LoginSuccessEvent::class => 'onLoginSuccess'
];
}
public function onLoginSuccess(LoginSuccessEvent $event)
{
$user = $event->getUser();
$user->setCount($user->getCount() + 1);
$this->em->flush();
//dd($user);
}
I'm not sure if this is explicitly documented yet. Like many upgrade deprecations, the code is very confusing. I tried to trace through what was happening and quickly got lost (once again) in the Security forest.
Events are talked about here.
I discovered this behavior by creating a fresh 5.1 project, running make:auth and adding a listener for both events. But I forgot to add enable_authenticator_manager: true to the security config.
So the INTERACTIVE_LOGIN event was fired. After enabling the new manager, the LoginSuccessEvent was fired. Notice that the new event has some additional helper methods such as getUser. Makes the code a tiny bit cleaner.
Off-topic but I would caution against flushing the entity manager inside of a listener. It can be a bit unpredictable depending on what else is going on. Might consider just getting the database connection and executing a SQL update.
I'm working with Symfony 4.4.
I'm using JWT Authentication and I'm now creating a custom user checker:
I want to return a custom response code and a custom message when user checker detect that user can not connect.
security.yaml:
client_login:
pattern: ^/api/login
provider: client_entity
stateless: true
anonymous: true
json_login:
check_path: api_login
username_path: email
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
user_checker: App\Security\UserChecker
refresh:
pattern: ^/api/token/refresh
stateless: true
anonymous: true
api:
pattern: ^/api
stateless: true
anonymous: true
guard:
authenticators:
- App\Security\TokenAuthenticator
provider: chain_providers #this provider will be ignored when getting the User
user_checker: App\Security\UserChecker
UserChecker:
class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user)
{
return;
}
public function checkPostAuth(UserInterface $user)
{
if (!$user instanceof Client) {
return;
}
if (!$user->isActive()) {
throw new AuthenticationException('userNotActive');
}
}
}
With this user checker the response when client is not active:
{
"code": 401,
"message": "An authentication exception occurred."
}
I want just to customize code and message.
If you only want to update response, you should create a listiner to handle failure authentification:
<?php
namespace App\EventListener;
use App\Entity\User;
use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationFailureEvent;
/**
* Authentication Failure Listener.
*
* This listener add data to payload.
*/
class AuthenticationFailureListener
{
/**
* When this event happened, response can be updated.
*
* #param AuthenticationFailureEvent $event the authentication Failure event
*/
public function onAuthenticationFailureResponse(AuthenticationFailureEvent $event): void
{
$response = $event->getResponse();
//TODO : edit your response here
//dd($response);
$event->setResponse($response);
}
}
Declare the service in services.yaml file:
App\EventListener\AuthenticationFailureListener:
tags:
- { name: kernel.event_listener, event: lexik_jwt_authentication.on_authentication_failure, method: onAuthenticationFailureResponse }
I'm using this LexikJWTAuthenticationBundle with FosUserBundle.
I have this in security.yml :
firewalls:
app:
pattern: ^/api
stateless: true
anonymous: true
lexik_jwt: ~
with the following access_control :
- { path: ^/api/user/action1, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/api/user/action2, roles: IS_AUTHENTICATED_ANONYMOUSLY }
The behaviour I was expecting for /api/user/action2 is having access no matter what is inside the request header. However I'm getting a 401 in the case where the Authorization Bearer is set but not valid (it is ok with valid token or no Authorization Bearer at all).
My use case is I need to check in my controller if the user is logged in but if not, I still want to let that anonymous user access the route.
You have to create a specific firewall for the route/pattern you want allow for anonymous users :
action2:
pattern: ^/api/user/action2
anonymous: true
lexik_jwt: ~
Then, just move your less-protected access_control just before the fully-protected :
- { path: ^/api/user/action2, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api/user/action1, roles: IS_AUTHENTICATED_FULLY }
In this way, you are application doesn't care about an Authorization header, and all users can access the resource without JWT.
Update
Change the anonymous route's firewall to :
action2:
pattern: ^/api/user/action2
anonymous: true
lexik_jwt: ~
And make the access_control accepting anonymous And fully authenticated users :
- { path: ^/api/user/action2, roles: [IS_AUTHENTICATED_ANONYMOUSLY, IS_AUTHENTICATED_FULLY] }
- { path: ^/api/user/action1, roles: IS_AUTHENTICATED_FULLY }
Please use the same order and clear your cache correctly.
It's working well in my JWT/FOSUB application, if it doesn't work for you I'll give you a working ready-to-use example.
And the controller :
$currentToken = $this->get('security.token_storage')->getToken();
if (is_object($currentToken->getUser())) {
// Do your logic with the current user
return new JsonResponse(['user' => $currentToken->getUser()->getUsername()]);
} else {
return new JsonResponse(['user' => 'Anonymous']);
}
Hope it works for you.
I resolved your problem in this way:
api_public:
pattern: ^/api/v1/public
anonymous: true
lexik_jwt:
authorization_header:
enabled: false
prefix: Bearer
query_parameter:
enabled: false
name: bearer
api:
pattern: ^/api
stateless: true
anonymous: true
lexik_jwt:
authorization_header:
enabled: true
prefix: Bearer
query_parameter:
enabled: true
name: bearer
*** For those landing here in 2022 ***
To allow anonymous access with JWT
You must write your own JWTAuthenticator class -
(Code Source)
// src/Security/JWTAuthenticator.php
namespace App\Security;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Guard\JWTTokenAuthenticator;
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
// use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; // For Symfony 4.4 and above
final class JWTAuthenticator extends JWTTokenAuthenticator
{
private $firewallMap;
public function __construct(
JWTTokenManagerInterface $jwtManager,
EventDispatcherInterface $dispatcher,
TokenExtractorInterface $tokenExtractor,
// TokenStorage $tokenStorage, // For Symfony 4.4 and above
FirewallMap $firewallMap
) {
parent::__construct($jwtManager, $dispatcher, $tokenExtractor);
// For Symfony 4.4 and above, use the next line instead of the above one
// parent::__construct($jwtManager, $dispatcher, $tokenExtractor, $tokenStorage);
$this->firewallMap = $firewallMap;
}
/* For Symfony 3.x and below */
public function getCredentials(Request $request)
{
try {
return parent::getCredentials($request);
} catch (AuthenticationException $e) {
$firewall = $this->firewallMap->getFirewallConfig($request);
// if anonymous is allowed, do not throw error
if ($firewall->allowsAnonymous()) {
return;
}
throw $e;
}
}
/* For Symfony 4.x and above */
public function supports(Request $request) {
try {
return parent::supports($request) && parent::getCredentials($request);
} catch (AuthenticationException $e) {
$firewall = $this->firewallMap->getFirewallConfig($request);
// if anonymous is allowed, skip authenticator
if ($firewall->allowsAnonymous()) {
return false;
}
throw $e;
}
}
}
Register this class as a service by adding the following to your services.yaml file
app.jwt_authenticator:
#autowire: false # uncomment if you had autowire enabled.
autoconfigure: false
public: false
parent: lexik_jwt_authentication.security.guard.jwt_token_authenticator
class: App\Security\JWTAuthenticator
arguments: ['#security.firewall.map']
Then update the firewall in security.yaml to use the newly registered service
api:
pattern: ^/api
stateless: true
guard:
authenticators:
- app.jwt_authenticator
Lastly, here's a complete tutorial to setup Lexik JWT bundle with Symfony.
I had to add token extractor to config. I was wrong thinking this is enabled by default.
# lexic_jwt_authentication.yaml
token_extractors:
authorization_header:
enabled: true
prefix: Bearer
name: Authorization
We have a FOSUserBundle login system authenticating via LDAP and the fr3d LDAP bundle. It behaves like a normal multiple page application using sessions. We also have several RESTful endpoints using the FOSRestbundle and normal sessions for authentication. However, we need to share a few end points with an external application.
We managed to implement JWT using the Lexik bundle. It returns a token just fine. However, I don't know the best way to let a user using our login form to get this token so their request can pass it along in the header or session. My question is how to allow a user to login to our application in a stateful manner, but also receive the JWT and pass it to the server on ajax requests. This way I can allow external clients to connect directly to the API. Below is my symfony2 security configuration, security.yml:
security:
#erase_credentials: false
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
chain_provider:
chain:
providers: [my_user_provider, fr3d_ldapbundle]
in_memory:
memory:
users:
admin: { password: secret, roles: 'ROLE_ADMIN' }
my_user_provider:
id: app.custom_user_provider
fos_userbundle:
id: fos_user.user_provider.username
fr3d_ldapbundle:
id: fr3d_ldap.security.user.provider
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, role: IS_AUTHENTICATED_FULLY }
- { path: ^/api, role: IS_AUTHENTICATED_FULLY }
- { path: ^/api/login, role: IS_AUTHENTICATED_ANONYMOUSLY }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api_login:
pattern: ^/api/login
fr3d_ldap: ~
provider: chain_provider
anonymous: true
stateless: false
form_login:
check_path: /api/login_check
username_parameter: username
password_parameter: password
require_previous_session: false
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api:
pattern: ^/api
provider: chain_provider
stateless: false
lexik_jwt:
throw_exceptions: true
create_entry_point: true
main:
pattern: ^/
fr3d_ldap: ~
form_login:
# provider: fos_userbundle
provider: chain_provider
always_use_default_target_path: true
default_target_path: /
csrf_provider: security.csrf.token_manager
logout: true
anonymous: true
switch_user: { role: ROLE_LIMS-BIOINFO}
EDIT:
Based on Kévin's answer I decided to implement a custom Twig extension to get the token for the logged in user on each page load:
AppBundle/Extension/JsonWebToken.php:
<?php
namespace AppBundle\Extension;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class JsonWebToken extends \Twig_Extension
{
/**
* #var ContainerInterface
*/
private $container;
/**
* #var JWTManagerInterface
*/
private $jwt;
public function __construct(ContainerInterface $container, JWTManagerInterface $jwt)
{
$this->container = $container;
$this->jwt = $jwt;
}
public function getName()
{
return 'json_web_token';
}
public function getFunctions()
{
return [
'json_web_token' => new \Twig_Function_Method($this, 'getToken')
];
}
public function getToken()
{
$user = $this->container->get('security.token_storage')->getToken()->getUser();
$token = $this->jwt->create($user);
return $token;
}
}
app/config/services.yml:
app.twig_jwt:
class: AppBundle\Extension\JsonWebToken
arguments: ["#service_container", "#lexik_jwt_authentication.jwt_manager"]
tags:
- { name: twig.extension }
app/Resources/views/layout.html.twig
<script>window.jsonWebToken = '{{ json_web_token() }}';</script>
app/Resources/modules/layout/app.js:
var jsonWebToken = window.jsonWebToken;
$.ajaxSetup({
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization","Bearer " + jsonWebToken);
}
});
So far this seems to be working well. It let's my external API users and internal application users share the same authentication methods.
As the JWT token must be stored client-side (and not in a cookie to prevent CSRF attacks), you can use the create method of the lexik_jwt_authentication.jwt_manager service provided by LexikJWTAuthenticationBundle to generate a token after the login, then inject this token in a <script> tag in the generated HTML.
Hey i recently came across this same situation.
To generate the JWT I created a redirect listener
class RedirectListener implements EventSubscriberInterface
{
//the private variables go up here
public function __construct(\Twig_Environment $twig, TokenStorageInterface $sam, EntityManagerInterface $em, JWTTokenManagerInterface $JWTTokenManager)
{
$this->twig = $twig;
$this->sam = $sam;
$this->em = $em;
$this->JWTTokenManager = $JWTTokenManager;
}
public function kernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$route = $request->get('_route');
$routeParams = $request->get('_route_params');
$pathInfo = $request->getPathInfo();
$matchApp = preg_match('/\/app/', $pathInfo);
if ($event->isMasterRequest()) {
if ($matchApp) {
$token = $this->sam->getToken();
if ($token) {
/** #var User $user */
$user = $token->getUser();
if($user instanceof User){
$token = $this->JWTTokenManager->create($user);
$this->twig->addGlobal('jwt', $token);
}
}
}
}
return $event;
}
}
This helped me get the JWT to my twig template (I used my base template to make sure it's present on every page)
{% if jwt is defined %}
<span class="hidden" id="jwt" data-jwt="{{ jwt ? jwt : 'null' }}"></span>
{% endif %}
Now using autobahn JS I can subscribe using the JWT :
let jwt = $('#jwt').data('jwt');
let connection = new Connection({url:"ws://127.0.0.1:8080/ws", realm:"realm1"});
connection.onopen = (session) => {
function onevent(args) {
console.log("Event:", args[0])
}
session.subscribe(jwt, onevent);
}
connection.open();
Next the server can now receive messages with the JWT from the JS
public function __construct(ContainerInterface $container)
{
$router = new Router();
$realm = "realm1";
$router->addInternalClient(new Pusher($realm, $router->getLoop()));
$router->addTransportProvider(new RatchetTransportProvider("0.0.0.0", 8080));
try{
$router->start();
} catch (\Exception $exception){
var_dump($exception->getMessage());
}
}
I now need to register a module for the router that will act as a listener and send messages back to the registered topic (JWT).
I'm not 100% there yet so any advice would be appreciated and I'll keep this updated as I go along.