User object is not accessable in the controller in symfony - symfony

I'm trying to use auto login feature in symfony something like firing the login event and setting the user object. The user object is available in that controller but when I try to use other controller the user object says annon instead of showing the logged in user info
Controller A
private function autoLogin($request, $username)
{
$em = $this->getDoctrine()->getManager();
if (!$usr = $em->getRepository('AppBundle:User')->findOneBy(['username' => $username])) {
throw $this->createNotFoundException('User does not exist');
}
$token = new UsernamePasswordToken($usr, $usr->getPassword(), "secured_area", $usr->getRoles());
$this->get('security.token_storage')->setToken($token);
$loginEvent = new InteractiveLoginEvent($request, $token);
$this->get("event_dispatcher")->dispatch("security.interactive_login", $loginEvent);
$user = $this->get('security.token_storage')->getToken()->getUser();
dump($user); // can see user object without any issue
if (!$this->get('security.authorization_checker')->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException();
}
return $usr;
}
Controller B
public function editAction(Request $request)
{
$user = $this->get('security.token_storage')->getToken()->getUser();
print_r($user); // result is annon.
}
security.yml
security:
encoders:
AppBundle\Entity\User:
algorithm: bcrypt
providers:
doctrine_provider:
entity:
class: AppBundle:User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
secured_area:
anonymous: ~
provider: doctrine_provider
pattern: ^/
form_login:
login_path: security_login
check_path: security_login
csrf_token_generator: security.csrf.token_manager
logout:
path: /logout
target: /login
access_control:
- { path: ^/.*, roles: IS_AUTHENTICATED_ANONYMOUSLY }

I'd assume that you're not using any security setup and the second controller is called after user refreshes the page.
The most likely problem is that your user is not persisted into the session. Symfony\Component\Security\Http\Firewall\ContextListener is responsible for that. If you have a look at onKernelResponse() method you can find out how it does it. Basically it gets token from token storage, serialize it and stores to the session. On the request it does opposite: gets token from session and puts it to token storage.
I'd suggest that you play with the configuration of firewall and set up something like this:
firewalls:
autologin:
pattern: /autologinUrl/
context: autologing
In this case context listener will be called doing session-related stuff and your code should work.

Related

Symfony 4, PHPUnit - authorize user for unit test

I'm writing test to do the unit testing of controller. I want to check if I get the proper content for the url, which requires user to be logged in.
I have following test written
class AdminControllerTest extends WebTestCase
{
private $client = null;
public function setUp()
{
$this->client = static::createClient();
}
public function testAdminHome(): void
{
$this->logIn();
$crawler = $this->client->request('GET', '/admin/');
$this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
$this->assertGreaterThan(0, $crawler->filter('html:contains("Welcome to Admin Panel")')->count());
}
private function logIn()
{
$user = new User();
$user->setId(1);
$user->setEmail('test#admin.com');
$user->setFullName('Admin User');
$user->setEnabled(true);
$session = $this->client->getContainer()->get('session');
$firewallName = 'main';
$firewallContext = 'main';
$token = new UsernamePasswordToken($user, null, $firewallName, ['ROLE_ADMIN']);
$session->set('_security_'.$firewallContext, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$this->client->getCookieJar()->set($cookie);
}
}
Configuration in my security.yml
security:
encoders:
App\Entity\User: auto
providers:
database_users:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
pattern: ^/
context: main
form_login:
check_path: security_login
login_path: security_login
csrf_token_generator: security.csrf.token_manager
default_target_path: admin_home
logout:
path: security_logout
target: security_login
access_control:
- { path: ^/admin, roles: [ROLE_ADMIN, ROLE_EDITOR] }
role_hierarchy:
ROLE_ADMIN: ROLE_EDITOR
ROLE_EDITOR: ROLE_USER
However assertion for the Response code returns me 200, but I land on login page instead of admin home. Also there is not required content, as I'm on login page not an admin home.
Could you please check the code and help me find out the issue?
User #isom had right, I should have the user object persisted, so I have used fixtures to have a user persisted and then everything is fine.
Instead of creating new user object I pull it from DB with following code:
$user = self::$kernel->getContainer()->get('doctrine')->getRepository(User::class)->find(1);

Reload user role after change without re-logging

How to refresh logged in user role e.g. when it has been changed by admin user? I've found the always_authenticate_before_granting security option (it's not included in Symfony 4 documentation) and set it to true.
security.yaml:
security:
always_authenticate_before_granting: true
encoders:
App\Entity\Main\User:
algorithm: bcrypt
providers:
app_user_provider:
entity:
class: App\Entity\Main\User
property: email
role_hierarchy:
ROLE_ADMIN: ROLE_USER
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
guard:
authenticators:
- App\Security\LoginFormAuthenticator
form_login:
login_path: login
check_path: login
logout:
path: logout
target: homepage
remember_me:
secret: '%kernel.secret%'
path: /
access_control:
- { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/.*, roles: ROLE_USER }
but it doesn't take effect.
UPDATE
I've created onRequest subscriber:
class RequestSubscriber implements EventSubscriberInterface
{
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => 'onRequest'
];
}
public function onRequest(GetResponseEvent $event): void
{
if (!$event->isMasterRequest()) {
return;
}
if(!$token = $this->tokenStorage->getToken()) return;
$sessionUser = $token->getUser();
if ($sessionUser instanceof User) {
$this->tokenStorage->setToken(new PostAuthenticationGuardToken($sessionUser, 'main', $sessionUser->getRoles()));
}
}
}
and now I can refresh the updated roles on every request, but comparing sessionUser to databaseUser is pointless, because the sessionUser always contains newly updated roles, though in Symfony Profiler > Security Token are listed the old ones (in case when I don't set the new token, of course).
Tl;dr I'm afraid you will have to introduce a mechanism of your own in order to make this work.
The session token is stored inside the user's session. This will have quite an impact on your application's performance, because each time a call to the database will be required in order to check if the role had changed.
So you will need a request listener which will compare database role with current user role, and if it is not same, replace the token in the session, this time with new role list, eg. (pseudo code):
sessionUser = tokenStorage.token.user
databaseUser = userRepository.find(sessionUser.id)
if sessionUser.roles !== databaseUser.roles
tokenStorage.setToken(new PostAuthenticationGuardToken(…))
or use a cache as a flag carrier to notify the user about the change. This method is going to be much quicker for sure.
sessionUser = tokenStorage.token.user
// of course the flag has to be set when the user's role gets changed
cacheToken = 'users.role_reload.' . sessionUser.id
if cache.has(cacheToken)
databaseUser = userRepository.find(sessionUser.id)
tokenStorage.setToken(new PostAuthenticationGuardToken(…))
cache.remove(cacheToken)
Either way the user has to ask the application has there been role change, on each request.

authentication in symfony api with ldaptools+jwt+fosuser

In my api I'm trying to authenticate a user using LdapToolsBundle, FOSUserBundle and LexikJWTAuthenticationBundle. Doing things step by step and following the integration docs for fosuser and ldaptools and later the jwt docs I manage to acomplish the following:
FosUserBundle + LdapToolsBundle was successfull
Api Integration + FosUserBundle + LdapToolsBundle was successfull
Jwt + FosUserBundle + LdapToolsBundle on Api failed.
the problem is that I just can only log in against my database but not ldap.
in my database I have one user record which I created with fos command and any password(making shure the authentication is on ldap and not fosuser). So far so good. but once instroduced JWT the authentication is made by fosuser instead of ldap authentication guard. When I change the password with fos command I can get the token with out problems.
this is my config:
security:
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUser: plaintext
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
ldap:
id: ldap_tools.security.user.ldap_user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api_login:
pattern: ^/login
stateless: true
provider: fos_userbundle
anonymous: true
form_login:
check_path: /login
require_previous_session: false
username_parameter: username
password_parameter: password
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
require_previous_session: false
guard:
authenticators:
- ldap_tools.security.ldap_guard_authenticator
logout: true
api:
pattern: ^/
stateless: true
lexik_jwt: ~
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, role: IS_AUTHENTICATED_FULLY }
My question is how can I get to work the ldap authenticator? I'll show the wanted workflow
Auth request is received (username and password)
The fosuser provider finds the user or user not found(bad credentials-->end)
The ldap authenticator guard authenticate the user against the domain server or bad credentials -->end
The user is logged in successfully and token is received
but I still getting bad credentials with a registered user in database and domain server (the user credential works)
Thanks in advance!
Diggin into the ldap_tools.security.ldap_guard_authenticator authenticator(namespace LdapTools\Bundle\LdapToolsBundle\Security;) I found this
public function getUser($credentials, UserProviderInterface $userProvider)
{
$domain = $this->ldap->getDomainContext();
try {
$credDomain = isset($credentials['ldap_domain']) ? $credentials['ldap_domain'] : '';
$this->switchDomainIfNeeded($credDomain);
$this->setLdapCredentialsIfNeeded($credentials['username'], $credentials['password'], $userProvider);
$user = $userProvider->loadUserByUsername($credentials['username']);
$this->userChecker->checkPreAuth($user);
return $user;
} catch (UsernameNotFoundException $e) {
$this->hideOrThrow($e, $this->options['hide_user_not_found_exceptions']);
} catch (BadCredentialsException $e) {
$this->hideOrThrow($e, $this->options['hide_user_not_found_exceptions']);
} catch (LdapConnectionException $e) {
$this->hideOrThrow($e, $this->options['hide_user_not_found_exceptions']);
} catch (\Exception $e) {
$this->hideOrThrow($e, $this->options['hide_user_not_found_exceptions']);
} finally {
$this->switchDomainBackIfNeeded($domain);
}
}
This method loads an user given an userprovider and some credentials.
So, the getUser relies in the userprovider in order to load any user, but since you are using fos_userbundle as userprovider for your api_login firewall you are, in fact, authenticating against your local database. Try using the ldap userprovider in your config instead.
Of course, doing this you will authenticate against the ldap server and not the flow you descibed above. To do so consider handling authentication by yourself, so you can handle the flow as you want.
Also you can do like this
customauthenticator and in your public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
you can check credentials against your ldap server with the service "#ldap_tools.ldap_manager"->authenticate($user, $password, &$errorMessage = false, &$errorNumber = false).
Here is my answer: Just force the auth after checking the ldap server.
public function onAuthenticationFailure(AuthenticationFailureEvent $event)//when auth fails on DB(Allways!!!)
{
$userToken = $event->getAuthenticationToken();
$username = $userToken->getUsername();
$password = $this->requestStack->getCurrentRequest()->get('password');
if ($this->ldapManager->authenticate($username, $password)) {//good credentials
$token = new UsernamePasswordToken($userToken, 'yes', "public", $userToken->getRoles());
$this->container->get('security.token_storage')->setToken($token);//set a token
$event = new InteractiveLoginEvent($this->requestStack->getCurrentRequest(), $token); //dispatch the auth event
$event->stopPropagation();
$this->container->get('event_dispatcher')->dispatch(SecurityEvents::INTERACTIVE_LOGIN,$event);
}
//symfony takes care of the response
}
So far this is the best answer I've found

Combine a stateful application with stateless JWT

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.

Symfony2 Getting the Current User

I'm developing an api, and I can't get the current user when I try the BasicAuth :
FatalErrorException: Error: Call to a member function getUser() on a non-object
Below you'll find the security.yml part that cause the error (I think it does) :
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: true
stateless: true
http_basic:
realm: "REST Service Realm"
provider: fos_userbundle
access_control:
- { path: ^/users/me, role: IS_AUTHENTICATED_FULLY }
I just put the path in access_control I'm testing. I've several more.
I'm using FOSRestBundle as well as FOSUserBundle (as you can see) and I didn't want to put a prefix, because it would be redundant :
// Routing.yml
rest :
type : rest
resource : "routing_rest.yml"
Now the part where the error occur is in the UserController (extending the FOSUser one) :
private function response($data, $status, $groups = array())
{
$currentUser = $this->container->get('security.context')->getToken()->getUser();
if (!$currentUser)
$groups = array("anon");
else
{
if ($currentUser->hasRole("ROLE_SUPER_ADMIN"))
array_push($groups, "admin");
else if ($currentUser->hasRole("ROLE_ADMIN"))
array_push($groups, "admin");
else if ($currentUser->hasRole("ROLE_USER"))
array_push($groups, "user");
}
return $this->view($data, $status)->setSerializationContext(SerializationContext::create()->setGroups($groups));
}
Any idea what's wrong ?
The problem is that your token is null. You have an unknown user if your token does not exist.
$token = $this->container->get('security.context')->getToken();
if (!$token) {
return ('anon.');
}
$user = $token->getUser();

Resources