How to log out a banned user in Symfony? - php

Symfony provides a simple way of preventing users from logging in using the isEnabled property if the user class implements AdvancedUserInterface.
However, if the user is logged in nothing will prevent them from accessing the website until their session expires.
The idea would be to check the isEnabled property upon getting the user entity ($this->get('security.token_storage')->getToken()->getUser()) and to invalidate the session then. What is the preferred way of doing so?
Or is there a better way to achieve this goal?

Maybe catch request before controller execution (there is a listener for that I think), check if his account is blocked and if it's true just make 403 response and hasta la vista baby

You can set the following in your security.yml:
security:
always_authenticate_before_granting: true
This will always re-authenticate a user before storing the information into the token. It will take care of these kind of changes in the user configuration, but also things like changed roles etc..

Better way is redirecting them to the logout url, which is handled by Security system. You can do it by creating a listener which listens the kernel.request event. Here is a gist which I created about a month ago, which provides such functionality..

You could do this with a custom user provider (it's an easy change if you are already using one).
http://symfony.com/doc/current/cookbook/security/custom_provider.html
loadUserByUsername is called when a user logs in.
refreshUser is called on every subsequent request. The user-object is unserialized and then refreshed. You can then do whatever you want in the refresh method, including reloading it from the database and check if the user is banned.

Related

Can't login with Symfony Guard, maybe because of cookies & multidomain

I need a very simple form login, so I'm using a Symfony Guard authenticator for this.
My application is multi-domain, for now login is possible on only 3 of them (and 2 are subdomains). But I can't log in on any of them.
First, here are some resources:
The authenticator: FormLoginAuthenticator.php
My security configuration: [https://gist.github.com/Pierstoval/1e29a9badab1cba03e45a306aa658c83#file-security-yaml)
And in case it's needed, the session configuration in the framework: config/packages/framework.yaml
I don't understand why, but to debug this, I tried updating config/packages/dev/web_profiler.yaml to set intercept_redirects: true, to debug cookies and security config, and here's what I have when reproducing the common login scenario:
When accessing the backend page (that shows the login form) I have the session cookie with a value like llno5smlmsema5nq02kr30s6m2.
After I submit the form with correct values, I see the profiler's redirection page, and the Web Debug Toolbar shows me that I'm Logged in as Pierstoval and the session cookie sent by the response changes id and becomes b1vbcnvp7e6p0j0o0vtd7ui7bf (because by default the Firewall migrates the session when logging in), so this works. I can even see that the token is in the session. I also added dump() statements in the Session::save() method, and dumping session content shows the token. And the token is in the session file when I dump it too (in var/sessions/dev/sess_b1vbcnvp7e6p0j0o0vtd7ui7bf).
I click on the http://back.esteren.docker/fr/ link showed by the redirection page, and then I see the session cookie id did not change (it's the one that have been set after logging in), but this time, the Web Debug Toolbar shows me that I'm Logged in as anon..
To me, there's a problem when the session is saved, so I took a look at the session files in var/sessions/dev/, after the redirection.
When I cat var/sessions/dev/sess_b1vbcnvp7e6p0j0o0vtd7ui7bf, I see this:
_sf2_attributes|a:2:{s:18:"_csrf/authenticate";s:43:"bCm23vBGyrq52caehAH9cCTjJkAmdT9j9Z8yygaET78";s:26:"_security.main.target_path";s:30:"http://back.esteren.docker/fr/";}_sf2_meta|a:3:{s:1:"u";i:1538092899;s:1:"c";i:1538092750;s:1:"l";s:5:"86400";}
Which, when unserializing the session attribute bag array, gives this:
array:2 [
"_csrf/authenticate" => "bCm23vBGyrq52caehAH9cCTjJkAmdT9j9Z8yygaET78"
"_security.main.target_path" => "http://back.esteren.docker/fr/"
]
This means the token has not been saved in the session after the successful login.
And even worse: since I already debugged the session file before redirecting, I know that it was saved ni the session and at some time it was removed from it before the session being saved another time.
How is this even possible? Did I forget something in my security configuration?
As a side-note, I tested with and without docker (with a plain php-fpm+nginx setup) and none worked, this probably means that it's not a permission issue from the docker side...
Finally, thanks to #Lynn and #Wirone on the public Symfony Slack channels, I ended up with a solution.
First, the bug was the fact that my User entity (which is serialized in the session) did not store the password.
And when a token is created, it checks if the "old" (in session) user password is the same as the "new" (refreshed) one, thanks to the Abstractoken::hasUserChanged() method.
And if the user object has changed, the firewall logs the user out.
To fix this, either serialize the password with the user, or implement EquatableInterface and use your own checks.
And this is what I did:
Implement EquatableInterface
Serialize only id and the timestampable fields createdAt and updatedAt
Make isEqualTo() check class and date fields to know if the user has changed.
Hope this helps 👍

laravel and multi-sessions from the same browser

In our web app, If I use a single browser, login to our application as user A, open another tab and login as user B - User A loses his session data. I assume this is due to a shared cookie made out with the user-agent. Is there a way to concat its name with a username? so that sessions can co-exist between concurrent logged in users using the same browser on the same machine?
We use Laravel 5. Is there any way around it?
Laravel Session Background
Sessions
Skip this section for a quick easy solution
In Laravel, session cookies are created via the Illuminate\Session\SessionManager class, namely through the buildSession method:
SessionManager::buildSession
protected function buildSession($handler)
{
if ($this->app['config']['session.encrypt']) {
return new EncryptedStore(
$this->app['config']['session.cookie'], $handler, $this->app['encrypter']
);
} else {
return new Store($this->app['config']['session.cookie'], $handler);
}
}
In this method we can clearly see that the name of the session comes from our config\session.php, looking in particular this line:
session.php
'cookie' => 'laravel_session', # ~~ ln 121 at time of writing
Ok, but that doesn't help a lot, changing this, changes it everywhere, as noted by the comment proceeding it in the config.
The name specified here will get used every time a new session cookie
is created by the framework for every driver.
And even if we could pass it some dynamic value, something like:
'cookie' => 'laravel_session' . user()->id,
This creates a paradoxical, time ending, universe imploding outcome because you are requesting the id from the user which is accessed via the session looked up by the cookie name laravel_session.. (mindblown)
Let's leave SessionManager and it's session.php configuration alone. We can see from above that regardless of how we approach this, all our session info will be fall under that single laravel_session key.
Guard
Maybe Guard will have some more information.
Guard is your key to auth into your app, and one of the many things that makes Laravel awesome for quickly creating applications.
The method to look at is Guard::user().
One of the first things Guard::user() does after some initial cache and logged out checking, is a session check.
Guard::user()
$id = $this->session->get($this->getName());
So here, Laravel is fetching the session values that match the result of getName() - awesome - all we need to do is mod getName() to return a value, let's take a took at that method:
Guard::getName()
public function getName()
{
return 'login_'.md5(get_class($this));
}
That's pretty straight forward. $this refers to the Guard class, so the md5 will effectively always be the same (if anyone knows the 'why' behind md5'ing the class name which would be the same each time, leave a comment).
There are a few places where this should be updated, such as getRecallerName.
So from here, you can extend the core Guard class and splice in your getName and getRecallerName methods.
You will probably want to wrap some service provider around this, write some unit tests, possibly even overwrite the original auth manager.
"Geez, that seems like a lot of work"
"It sure is Billy, it sure is"
https://www.youtube.com/watch?v=dTxQ9yhGnAg
See the next part
The quick "I just need an answer" answer
Ollie Read has already created a solution, found here:
https://github.com/ollieread/multiauth
I encourage you to have a look, especially the custom Guard class which extends core Guard with custom getName methods.
Any major browser will only store one session cookie for a site, but the site developer gets to choose what's in that cookie. It seems like your site is storing user information in the session cookie, which is then getting overwritten when the other tab stores different information in the same cookie.
You don't provide much detail about how your specific site operates, but here are a few general ways of approaching this problem.
1) Use different browsers for different users. Different browsers don't share cookies between them. If your goal is simply to test your site with multiple users, this is the way. You can also use Incognito/Private mode to log in a separate user, as this mode doesn't share cookies either.
2) Don't use session cookies to store user information. This is a non-starter on most websites, but if this is an internal site or strictly controlled environment, you may be able to pass user identification via the URL, POST data, or some other hidden identifier in the request.
3) Store data in the session cookie for all currently logged in users. Depending on the web framework, it may be possible to create a map of user -> cookieData and look up the correct one based on which user is making the request. This is an advanced technique, and I don't actually know if Laravel exposes this level of control.
Multi userlogin with same browser like google add account. for that you need follow some steps and re-write auth library which provided by the Laravel,
Steps
Tack backup of your Auth file.
Change all session store functionality to store it first in array and then store that array to session
Now you need to create the new session variable which will store the current user instance id like user 0 1 2 ...
Now you need to change all the function from you will get the values from the session you need to check if the session object is empty then user is logout else you need to get data of the user base on the user instance.
You need to change your instance when user want to switch from one account to another.
The easiest is just a URL based sessionID which could be a security issue depending on how your application is designed, especially when sharing urls with non-expired sessions.
Since L5 doesn't support php native sessions anymore, you'll have to use a custom provider like below:
This will use sessionID in the url for laravel V5:
https://github.com/iMi-digital/laravel-transsid
Basically the session is URL based, so you can just login in a different tab and get a new sessionID, and that person can easily do a "open page in new tab" to have two pages of the same user if needed as well.
The library above locks the session to the IP and User Agent so link sharing won't accidentally leak a session.
tl;dr: Yagni
Consider a person (http client in your case) with 2 identities: Dr Jekyll and Mr Hyde.
He visits his new friend Sir RM1970 (http server in your case): "How do you do, RM1970!".
Here is the problem. Poor RM1970 need to welcome back the monster, and there are few options:
fall deep into this rabbit hole: "How do you do both Dr Jekyll and Mr Hyde!", which incredibly complicates further conversation (your ACl, for example, will need to operate with list of identities, and make a decision about priorities if they conflict realtime)
make a decision on your own: "How do you do Dr Jekyll!" and pray you made the right choice (randomly pick user's identity and bring your users some fun with unpredictable responses)
be sly and shift this responsibility back to him: "Pardon me? Who are you? Name yourself!" (require single identity per request)
The later is how it actually works. The browser provides the latest confirmed identity.
You've been asked to change this, but do you really want it? Hold the line and don't accept this responsibility.
If you are not going with first 2 dead-end options, you will need to ask user on which behalf he sends the request. The best option here is to make your frontend stateful, maintain list of opened sessions, and provide a UI for user to pick one. It is almost the 3rd Ryan Bemrose's option, but store this data on client side, and send only the chosen one. No changes in laravel backend required.
The problem here is switching tabs will not automatically switch user, and will be rather confusing, providing very little difference with logout/login path, which is already implemented.
Some browsers support multiple profiles (example), which may be an acceptable alternative. Basically it is the same as 1st Ryan Bemrose's option, but does not require multiple browsers installed, and can benefit from permanent cookies, aka 'remember-me'.
I don't exactly know what do you need this for, but as a developer I sometimes have to log into an application with multiple users. To do that I usually use incognito mode or if its more than 2 users I had some luck using this extension in chrome.
I know its not an answer to your question but it just might be what your looking for.
Different seesions coexist between concurrent logged in users cannot just only implemented by session cookie,because cookie is stored by browser. So the logged
in user's seesion must be stored by Server.
As all we know, Once session_start is called,SessionID is created and then temp file is created in server's temporary
directory.
Diffent user has different SessionID and after session_destory called then all SessionIDs stored in Server and Cookies are recovered. You can rewrite this behavior by implementing SessionHandlerInterface. Of cause many web framework support this,Laravel has not exception.
Here is the document:
custom-session-drivers
I don't know how complicate it is to code it into laravel but this could be one solution:
You use a different session name, has to be a string, and code it into the url every time so the application knows which user made a request. So you can call the session variables by a normal name.
<?php
if(isset($_GET['id']) && !empty($_GET['id']))
session_name($_GET['id']);
session_start();
if(isset($_GET['user'])) {
$_SESSION['user'] = $_GET['user'];
}
if(!empty($_SESSION['user']))
echo "Hello ".$_SESSION['user'];

Invalidate session for a specific user in Symfony2

I have written a system in which a background PHP process (written using the RabbitMQBundle) processes messages from a message queue. The worker process updates user accounts. This may include a change in the user roles.
The problem is that a user won't notice any changes in his roles while being logged in. The new roles only get applied after logging out and in again. From a security perspective this is not desirable. A user should loose any role as soon as an administrator takes away privileges from that user in the backend system.
The question is: How can a session for a specific user be updated with the new roles? Or when that is not possible, how can the session be invalidated?
Note that in the background process we don't have an active security.context or request that we can use. Code like this therefore doesn't work:
$this->get('security.context')->setToken(null);
$this->get('request')->getSession()->invalidate();
You can solve this in several ways:
1) via security.always_authenticate_before_granting
You can force Symfony to refresh user on each request, effectively reloading all roles with it.
See this SO question/answer:
Change the role of a distant user without having to relog
2) Via EquatableInterface:
You need to implement isEqualsTo(User) which in turn should compare everything with User class, including roles.
See this link: Create a User Class
3) Finally, you can use DBMS to store sessions. Then, it's just matter of find the record and delete it.

Chrome: "continue where I left off" mode and Symfony2

Using Symfony2 and FOSUserBundle, I am not getting the expected behavior on the following implementation.
To begin, know that Continue where I left off option in Chrome restores completely the user session independently of having checked some "Remember me" or something like that. Therefor,it saves a cookie with all the information of the session.
What I am trying to do is to avoid the creation of a session from the cookie stored through that Continue where I left off option on Chrome.
Or, if I cannot avoid the creation of the session, at least try to know that the session comes from that completely transparent way.
I have found this in Symfony2 documentation (specifically here):
In some cases, however, you may want to force the user to actually re-authenticate before accessing certain resources. For example, you might allow "remember me" users to see basic account information, but then require them to actually re-authenticate before modifying that information.
The security component provides an easy way to do this. In addition to roles explicitly assigned to them, users are automatically given one of the following roles depending on how they are authenticated:
IS_AUTHENTICATED_ANONYMOUSLY - automatically assigned to a user who is in a firewall protected part of the site but who has not actually logged in. This is only possible if anonymous access has been allowed.
IS_AUTHENTICATED_REMEMBERED - automatically assigned to a user who was authenticated via a remember me cookie.
IS_AUTHENTICATED_FULLY - automatically assigned to a user that has provided their login details during the current session.
So, if I don't get it wrong, a user that transparently logs in as a result of the Copntinue where I left off option, should have the IS_AUTHENTICATED_REMEMBERED.
Well, the reality is that it is not thus. The reality is that the granting is IS_AUTHENTICATED_FULLY.
Has anyone passed through this?
Any idea on all this?
Thanks
Sessions are handled server side. Depending on your server's configuration for sessions lifetime, you can close your browser and re-open it without losing the session. This has nothing to do with the Continue where I left off option of Google Chrome.
The granting is IS_AUTHENTICATED_FULLY because the session is still active on the server and not because of the Google Chrome option.
Simple example use case. Let's say we set a 5 minutes session lifetime.
Without the remember me option :
I log in : a session is created on the server.
I close the browser.
I come back 10 minutes later : session has expired therefore I have to provide my credentials.
With the remember option :
I log in : a session is created on the server AND a cookie is created on my browser saying hey I'm connected.
I close the browser.
I come back 10 minutes later : session has expired BUT as the result of the cookie saying hey I'm connected, a new session will be automatically created. Therefore I will not have to provide my credentials again.
In both case if you come back within the first 5 minutes you will be automatically logged in because the server still handle a session for your browser.

Dividing lines between Session Object, User Object, and Login Controller

I'm developing my own PHP framework, and I'm trying to do things more "by the book".
I want to build login system. I have done this plenty of times, but now I just want to confirm/get some feedback on a system.
I know we need...
A Session Object
A User Object
A Login Controller
What my question is, is who holds what power?
Here's my thought - when you submit your un/pw, it obviously goes to the Login Controller. We need to look up that un/pw combo in the user database, and therefore I feel as if that should be done in the in the user object ... $User->authenticate($un, $pw).
All that should do i return true or false. Then the LoginController should tell the Session object to create a session ... $session->create(). But apart of me wonders if the User and Session object should be a bit closer knit, and not rely on a Controller.
Looking for opinions, thanks in advance.
In my opinion, the user object shouldn't know about persistence (session) or the application (controllers). All it should care for is representing a user and handling user-related functions like authenticate.
The controller is all about application logic, and the session is part of the application. I feel like it's the controllers task to open the session and store the user object for later usage, once authenticated.
P.s. Did you publish your framework? Github it! :-D
My thoughts for a framework like this:
Initialize session object on page loads, and set it to default to a "guest account".
Upon this initialization make your session object to look for a session identifier (from a cookie?) and validate that ID against your session tracking back-end, be it database or whatever you are using.
If the session ID validates, you get your relevant user ID which you can then use to populate the user object.
Login control should just authenticate the user from login, and initialize the session by binding session ID and user ID on the back end, and by writing the session ID to a cookie.
... A more efficient way, however, would be to authenticate the user once and write the user ID to a digitally signed cookie set by the server, and trust all valid cookies returned by the client. This would make session management so much lighter and save round trips do database.

Categories