I want to get user inside a profile service, but using tokenStorage and SecurityContext, the user will be everytime null.
that's my services.yml file:
project.service.profiler:
class: Project\Service\Profiler
arguments:
- "#security.helper"
- "#=service('doctrine').getRepository('bundle:ProfileKey')"
- "#=service('doctrine').getRepository('bundle:ProfileKeyUsers')"
- "#=service('doctrine').getRepository('bundle:ProfileKeyRoles')"
- "#logger"
- "#security.token_storage"
- "#security.authorization_checker"
- "#fos_oauth_server.access_token_manager.default"
and that's my class Profiler
class Profiler
{
public function __construct(
Security $security,
ProfileKeyRepositoryInterface $profileKeyRepository,
ProfileKeyUserRepositoryInterface $profileKeyUserRepository,
ProfileKeyRoleRepositoryInterface $profileKeyRoleRepository,
$logger,
$tokenStorage,
$authChecker,
TokenManagerInterface $tokenManager
){
if ($tokenStorage && $tokenStorage->getToken() && $tokenStorage->getToken()->getUser()) {
$this->user = $tokenStorage->getToken()->getUser();
}
}
}
The problem is that tokenStorage->getToken is always null (I'm sure, I'm logged in!).
So, this profiler was called from a controller, where the user is present, then I suspect that when the profiler was called during the symfony loading flow, the user is not created yet.
finally, if I set this line of code:
$security->isGranted('IS_AUTHETICATED_FULLY'); --> thrown an Exception
or getToken method:
$security->getToken() --> return null
I obtain everytime this error:
Why this behaviour?
In previous symfony version (I mean 3.3) this problem never occurred.
Thanks a lot to anyone who helps me
Update 08/04/2019
Following the symfony3.2 docs (https://symfony.com/blog/new-in-symfony-3-2-firewall-config-class-and-profiler), this post says to check if the request is under firewall, otherwise the user token should be null.
But, in my case, I checked with debug toolbar that all it's ok.
Finally, I absolutely have no idea why the user token is null under my service
Here my security firewall section:
security:
restricted_area:
anonymous: ~
access_denied_url: /unauthorized
access_denied_handler: app.security.access_denied_handler
form_login:
provider: fos_userbundle
csrf_token_generator: security.csrf.token_manager
logout:
path: /logout
target: /
Finally I've found the answer myself!!
So, the problem was in my class code inside the service class: I was trying to obtain the user directly in the constructor (in the symfony flow, services are loaded before token management), and here the TokenStorage->getToken was always null.
So, when I need to obtain the user inside the service procedures, the tokenStorage->getToken() returns the correct value.
Hope that this answer can help someone with my same (old) problem.
Related
The default Symfony behavior is to redirect to '/' after logout.
I don't require any redirects from Symfony as it's an api app.
Like how during login when Symfony takes control to do authentication, but then still runs the login controller to perform further actions. This would be ideal for logout in this case also.
security.yaml
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
json_login:
check_path: app_login
username_path: email
password_path: password
logout:
path: app_logout
src/Controller/SecurityController.php from Symfony docs
/**
* #Route("/logout", name="app_logout", methods={"GET"})
*/
public function logout(): void
{
// controller can be blank: it will never be called!
throw new \Exception('Don\'t forget to activate logout in security.yaml');
}
You can write a custom logout handler. Actually, there is a new approach for this, introduced in symfony 5.1. Basically, now you can register an event listener either globally or for some specific firewall, and perform any actions after the person has logged out.
Returning to your problem (from the blog post below):
The Symfony\Component\Security\Http\Event\LogoutEvent object
received by the listener contains useful methods such as getToken()
(to get the security token of the session), getRequest() and
setResponse().
The later will help you. It means you can return anything you want instead of default RedirectResponse by setting new response object to the event.
services.yaml:
services:
App\EventListener\CustomLogoutListener:
tags:
- name: 'kernel.event_listener'
event: 'Symfony\Component\Security\Http\Event\LogoutEvent'
dispatcher: security.event_dispatcher.main
method: onLogout
And your listener:
namespace App\EventListener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Http\Event\LogoutEvent;
class CustomLogoutListener
{
public function onLogout(LogoutEvent $logoutEvent): void
{
$logoutEvent->setResponse(new JsonResponse([]));
}
}
Read more: Simpler logout customization
Since day we are getting a php error from Google's ip's:
[client 66.249.xx.xx:xxxx] Got error 'PHP message: PHP Fatal error: Uncaught Symfony\\Component\\Debug\\Exception\\FatalThrowableError: Call to a member function get() on null in /usr/home/project/myProject/src/AppBundle/Service/LocaleListener.php:22
It seems that Symfony's $request->getSession() returns null sometimes. Can't figure out the reason. Here's where error occurs:
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!empty($request->getSession()->get('_locale'))) {
return;
}
...
}
Any idea?
With symfony you define firewalls, by default a user is authenticated only under one firewall.
For example security.yml:
firewalls:
secondfirewall:
pattern: ^/secondfirewall
provider: broker
main:
pattern: ^/
form_login:
provider: fos_userbundle
csrf_provider: form.csrf_provider
logout: true
anonymous: true
I assume your user is logged under the main firewall
You'll have a session on all urls except them starting by /secondfirewall. This might explain why you don't get a session
To debugging which route is going into error you could add log to your listener and exit if no session available.
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if(null === $request->getSession()) {
// Log everything you want to debug, url, paremeters, body content etc
// Then you can redirect the user to the login page
return new RedirectResponse('someloginurl');
// Or you can just exit from your listener
return;
}
if (!empty($request->getSession()->get('_locale'))) {
return;
}
...
}
If as you mentionned in the comment the call which go into error are Google's robot calls, you could log :
$request->headers->get('User-Agent') -> check the returned string,
Do a regex with preg_match to exit from listener if it match the google's user agent.
Moreover you could configure the pages which google robots will parse with the robot.txt file, please check google documentation to controle the urls to crawl
You won't have a session for all requests. If it's from CLI for example. Also unless for some reason it's been designed that way, a "traditional" API request won't either.
When I am authenticating usual way (using login form), it works all right. I am getting this error only when /check_form is accessed via GET method directly, in which case an exception being thrown:
You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.
Here is the relevant security.yml part:
firewalls:
acme_area:
pattern: ^/(acme|admin)/
provider: fos_userbundle
form_login:
provider: fos_userbundle
csrf_provider: form.csrf_provider
login_path: acme_login
check_path: /acme/login_check
logout:
path: /acme/logout
target: acme_login
anonymous: true
I am using 2.3, thus no methods option is applicable (though I have no idea if it would help).
It is not really an issue as no proper usage could be spoiled by this error, but it pollutes the error log when some diligent bot is visiting the site and it's just untidy. So, I'd like to know which configuration option I can change to get rid of this error.
To boil this down, it seems that I want some 4xx error to be thrown instead of 500. Ideally it should be 405 Method Not Allowed, but 404 cold do too.
EDIT:
As as I learned from the Alex's answer below, this happens because POST requests are handled by the firewall and GET requests by the Controller. Thus, it seems that default checkAction() have to be extended to be able to handle two cases:
When request is POST but no firewal entry is present (already nandled)
When firewall entry is present but request is GET (my case)
There is no configuration option for that. If the request reach the controller, it unconditionally throws the exception: credible source.
POST request to the route are handled by firewall: official docs; GET ones go to the controller as usual.
There are few options to get rid of the error in the log, if you don't care about such events. The simplest one in my opinion is to override SecurityController::checkAction to return 500 error without throwing an exception. The official docs how to achieve it: Overriding Default FOSUserBundle Controllers.
EDIT:
In the controller you can return whatever code you like:
public function checkAction()
{
return new Response('', 418); // or better use Response constants
}
Another way is to disable GET method to /acme/login_check in the routing config, and let router do its job and return normal 405 Method Not Allowed as usual.
EDIT2:
You can analyse request in the action, and still throw an exception:
public function checkAction(Request $request)
{
if ($request->getMethod() == Request::METHOD_POST) {
throw new \RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.');
} else {
return new Response('', Response::HTTP_METHOD_NOT_ALLOWED);
}
}
but I would recommend to debug your routes instead. This logic should belong to the router, not controller. In the long run, your routing config will mislead devs who will maintain this code, and they will have several hard debugging hours trying to figure out why it returns 405, when app/console debug:router clearly states that GET method is allowed.
I'm using fr3d/ldap-bundle. It logs me in and imports users from AD if they're not in db. That's fine.
Despite AD users I also have local users, which are in my db. There is special column authType which says how user should be authenticated - via LDAP or natively ( FOS ). I've created my own user provider:
public function chooseProviderForUsername($username)
{
if($user->getAuthType() == User::LOGIN_LDAP) {
$this->properProvider = $this->ldapUserProvider;
} elseif($user->getAuthType() == User::LOGIN_NATIVE) {
$this->properProvider = $this->fosUserProvider;
} else {
throw new InvalidArgumentException('Error');
}
}
public function loadUserByUsername($username)
{
return $this->chooseProviderForUsername($username)->loadUserByUsername($username);
}
PROBLEM: Chain provider isn't an option - it allows user to login with his LDAP password AND with his local password! That's a big security issue.
Is there a way to login user via different authentication providers, depending on the db field?
EDIT:
My security.yml:
providers:
fos_userbundle:
id: fos_user.user_provider.username
appbundle_user_provider:
id: appbundle.user_provider
fr3d_ldapbundle:
id: fr3d_ldap.security.user.provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
admin:
pattern: ^/admin.*
context: user
fr3d_ldap: ~
form_login:
provider: appbundle_user_provider
csrf_provider: security.csrf.token_manager
always_use_default_target_path: true
default_target_path: admin_main
login_path: /admin/login
check_path: /admin/login_check
logout:
path: /admin/logout
target: /admin/login
anonymous: true
Here is security.yml. This line fr3d_ldap: ~ enables the ldap bundle, which authorize ldap users and saves them into my db. Without it I cannot authorize them, probably I would have to write custom AuthenticationProvider.
I am not very familiar with ldap but I would suggest try doing a completely manual login
$token = new UsernamePasswordToken($user, null, "firewallname", $user->getRoles());
$securityContext = $this->container->get('security.context');
$securityContext->setToken($token);
Then you can manually do the checks yourself, and depending on the result of the check decide how you want to verify the user before authenticating. For example, run a query by username and password before executing this login code or whatever, depending on the db field you want.
Your approach seems fine but you should check logic of your methods.
First of all this one:
public function chooseProviderForUsername($username)
{
if($user->getAuthType() == User::LOGIN_LDAP) {
$this->properProvider = $this->ldapUserProvider;
} elseif($user->getAuthType() == User::LOGIN_NATIVE) {
$this->properProvider = $this->fosUserProvider;
} else {
throw new InvalidArgumentException('Error');
}
}
You pass $username to this method as an argument, but then use $user object, which seems to be undefined in current context.
Secondly:
public function loadUserByUsername($username)
{
return $this->chooseProviderForUsername($username)->loadUserByUsername($username);
}
So as chooseProviderForUsername method actually does not return any value you are not able to chain it this way.
I hope refactoring these issues should make your provider work properly.
Ok, so very brief answer, but I think at the moment Symfony is searching for the user amongst any old User Provider rather than the one you want it to for that particular user (which explains the whole logging in with two passwords thing). A solution should be to make AppBundleUserProvider implement UserProviderInterface, remove the other User Providers from security.yml and then to ensure that the first thing AppBundleUserProvider does it to find out which User Provider is required for that user then mimic it for every method in the UserProviderInterface. You could set $this->realUP based on Username, then set every method to just return $this->realUP->someMethod().
The cleanest way I can think of is to create your own ChainProvider class that only allows login with one provider and use the Dependency Injection Container to use yours.
You just need to override the security.user.provider.chain.clas parameter definition in your bundle's config file.
In this case, after a successful login, I need to update the user's login time into the underlying table.
The User entity currently implements UserInterface and is doing fine. Just want to add some extra code to log the login date time of the user.
Searched the site and seemed to me to use an EventListener etc is a bit heavy. Other lighter alternatives?
You can implement a Success Hander.
Write a class that implement the AuthenticationSuccessHandlerInterface:
Define it as a service (you can inject other services like doctrine
or the security context, in your case the Session)
Add the handler service in the security config like:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
secured_area:
pattern: ^/
anonymous: ~
form_login:
login_path: login
check_path: login_check
success_handler: some.service.id
logout:
path: logout
target: /
Check this for a complete example and the reference doc for all the symfony2 security configuration options (you can configure the failure_handler also).
In my working solutions I implements the method onSecurityInteractiveLogin in my listener as:
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
$user->setLastLogin(new \Datetime());
// Entity manager injected by container in the service definition
$this->em->persist($user);
$this->em->flush();
}
Hope this help