Yii always starts a session when I touch CWebUser - php

I'm developing a RESTful API server which require on some of its API methods a valid session, indicated in the form of a cookie with a session ID.
I'm using Yii v1.1.15 with stock PHP session handler (probably 'files').
Thing is that on every call to CWebUser it creates a session and I don't want this. A session should exists only when I explicitly create it, meaning on login (or register which auto-logins the users). For example, if in a certain API method I check if the user is guest using a construct which involves:
Yii::app()->user->isGuest
it automatically creates a session since this code is given in CWebUser.init().
Now, I'm not in a hurry to change CWebUser (in fact, to change this in the already extending class which altered it slightly, in other aspects) since I'm afraid this will have un-anticipated impact on the system.
Can anyone enlight me on this?
What would you do?
Thanks!
Environment:
// Yii v1.1.15
// session component configuration: (but believe me, I've tried every
// combination - its not really related. Check CWebUser.init()...)
'session' => array(
'class' => 'CHttpSession',
'autoStart' => false,
'sessionName' => 'MY_COOKIE_NAME',
'cookieMode' => 'allow',
'cookieParams' => ['lifetime' => 1000],
'gcProbability' => 33,
'timeout' => 1000,
'savePath' => '/tmp/',
),
// Web User's _allowAutoLogin_ is set on 'false'

So you need to check if a user is logged in (This is why you use isGuest) but you don't want to use a session?
The method isGuest uses a session variable to check if an user is logged in. The session is opened when the CWebUser is created. (in the init method like you said.) The isGuest is part of the CWebUser class. If you want to call this method it will always create a session. Unless you overwrite it.
I think you can go 2 ways at this:
Open a session, check if a user is logged in (isGuest) and then close it only if the user is not logged in. You will need to overwrite the isGuest method. Overwrite any other methods to open a session when you need it. (login/register)
Let the client send its login data on every request so you dont have to check the session and thus don't have to open it.
In both cases you will need to overwrite the CWebUser.init() so it won't open a session whenever the CWebUser is created.

So basically this was all a need that came from the following requirement set:
Yii will be used (also) as a RESTful API server.
The RESTful server will establish session only after successful login.
The above last point implies that there is no session cookie for guest users but rather it exists only for an authenticated user session.
The advantages of the above? mostly the 'free' management of login sessions by the stock PHP session including timeouts, garbage collection etc.
Despite the initial appealing of this design, the drawback overcame the advantages:
Indeed, isGuest is a property of CWebUser that when tested, already implied a session generated for the request.
Trying to change the above behavior introduced lots of issues and bugs, and god knows what lurked ahead. In essence, trying to change this behavior on Yii v1.1.x was too problematic as lots of built in features and behaviors (in the abstract meaning...) of Yii based app are implicitly using an established session.
So, I reverted to the following design:
Yii's session management was reverted to stock - yes please! open a session for everything going on! (just do it well as you normally do).
The RESTful server sends an explicit session token on successful login calls.
The client needs to save this token and send it explicitly with every API method that requires an authenticated session.
Server side saves the session token in the "freely managed" (PHP) session and thus is able to verify on each distinct request that the token for the user with this PHP session is indeed his, and is valid.

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 👍

CakePHP 3 - login through non-Cake subdomain

I'm re-building a web application in CakePHP 3 and plan to change how the application is structured. At the moment the architecture is as follows - and none of it is written in Cake.
There is a login page at https://app.example.com which sets PHP session variables on successful login. All of the applications are then in sub-directories, e.g.
https://app.example.com/application-1
https://app.example.com/application-2
https://app.example.com/application-3
Each sub-directory has a script which checks the appropriate session vars are set, otherwise redirects to the login page, e.g. trying to access https://app.example.com/application-1 without being logged in sends the user to https://app.example.com/
I'm planning to rebuild one of the applications, https://app.example.com/application-2 in CakePHP 3, and do so on a separate subdomain (e.g. https://cake.example.com/).
What I want to do is allow the users to still login through https://app.example.com and then use https://cake.example.com/ if they are succesfully authenticated.
I was planning to allow my PHP sessions to work across multiple subdomains - as per Allow php sessions to carry over to subdomains
I'm not sure though how this would work within Cake 3 though. One idea I had was to set up https://cake.example.com/ without any of Cake's Auth functionality enabled. I was then going to use the AppController::beforeFilter() to check the session variables. If they were set appropriately, allow the user to use any Cake Controller method. If not, redirect them to https://app.example.com where they can login.
I was looking for some advice on whether there is a better way to do this, and if this is secure? I'm aware that doing this is essentially like developing the Cake app with no authentication, and just relying on the session vars being read in beforeFilter().
The login script at https://app.example.com also writes to a database where we have things such as the user ID, IP, user agent string and date/time. I can access this DB from my Cake application, but the idea of querying this database on every single request also seems wrong.
It's worth mentioning that https://cake.example.com cannot have it's own login page, even if it connected to the existing users database to lookup the credentials. This is because the users login through https://app.example.com which then acts as a dashboard for their applications. Essentially by the time they get to https://cake.example.com they either have to be authenticated, or sent back to the existing login page.
Checking the session manually can be just as secure as using CakePHPs auth component, as the component does exactly the same (given that you'd be using the session storage), just with data that you've set via AuthComponent::setUser(), ie it all depends on whether you implement things properly.
Checking the session value in AppController::beforeFilter(), and redirecting if necceesary should generally be fine, and as mentioned is pretty much the same as what the auth component does internally, it will check whether the configured session key is present and not empty.
You could possibly still leverage the CakePHP auth functionality if you wanted to, the flat u_id value in the session should suffice. For the auth component, just configure the login/logout options and the session storage key accordingly, ie if your login page is at https://app.example.com, and your login app writes auth data to $_SESSION['u_id'], configure the auth component like this:
$loginUrl = 'https://app.example.com';
$this->loadComponent('Auth', [
'loginAction' => $loginUrl,
'loginRedirect' => $loginUrl,
'logoutRedirect' => $loginUrl,
'storage' => [
'class' => 'Session',
'key' => 'u_id'
]
]);
That should be all that is needed (authentication wise), the component should pick up the possibly existing session key and treat you as authenticated, or otherwise redirect to https://app.example.com. Defining loginAction will prevent the component from whitelisting a controller/action, and logoutRedirect will be returned by AuthComponent::logout(), so you could easily implement a standard logout action in your CakePHP app if you want/need.
Of course this all depends on the u_id session value being accessible (ie you've configured your CakePHP app to pick up the existing session) and reliable in the first place.

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'];

Yii CDbHttpSession, where is it called?

I've been trying to locate where the CDbHttpSession is called after login, to rewrite it.
My problem is that I have 3 products in my App, and each are restricted on X simultaneous users, so I shouldn't allow any user to access these products until someone closes session or the admin manually removes the session from the DB.
Any help on how could I achieve it?
Thanks!
It's a fixed application component available from CWebApplication. So you can access the session instance through Yii::app()->session. You can configure your own session component in your main.php.
'components' => array(
'session' => array(
'class' => 'MyCustomSession',
),
),
But this is probably not the right place where you want to solve your problem. There is no "close session" event in PHP's session system. So you can not really find out, when a user has "closed" his session.
What could be helpful for you are maybe the afterLogin() and afterLogout() methods in CWebUser. Much like with the session component you can also override the user component with your own class. There you can at least get hold of every login/logout process and do something. Note though, that a log out does not necessarily always occur: The session can simply time out and you won't get hold if it.

How to destroy a specific PHP session

I am looking for insights into how to destroy a specific session in PHP. Through a partner website a user logs into the main website using a token and obtains a full session.
It is also possible for the partner website to call a destroy function if the user logouts from the partner website. We should then also log out our own user.
What is the best approach to this? The Zend_Session destroy method does not accept a parameter, similarly the PHP function session_destroy does neither.
I am considering two options:
Removing the session information directly from file/memcache but would prefer a "cleaner" approach than this.
Checking at every page request if this is a "token" user ; and if then check if their token was expired by maintaining a list. This adds overhead to a busy website, but might be my only option.
Or is there a third / better approach I am not seeing?
There's no need to roll-your-own session handling.
session_id() can take a parameter, the session id you want to work with.
So, when you pass the user off to the partner site, pass along their session_id (or some token, or whatever).
Then allow the partner site to hit a script like this:
kill-user-session.php
<?php
/**
* Destroy any active session identified by $_POST['sid']
*/
session_id($_POST['sid']);
session_start(); //this line may not even be necessary
session_destroy(); //destroys that session.
So when the user logs out on the partner site, the partner site POSTs the session_id (that you gave them) to your kill-user-session script, and the user's session is destroyed on your server.
Of course, you probably want to limit access to kill-user-session.php via some method or another.
If you wish to be able to 'kick' the sessions of a user(s), the only way you can do it is if you use MySQL (or someother db, sqlite even) for your session storage.
Then you can simply remove entries from the db to kill a session.
This also allows you do do things such as, 'take control' of a specific user's session and other stuff :)
See this for a very basic run through: http://www.devshed.com/c/a/MySQL/Custom-Session-Management-Using-PHP-and-MySQL/ (not the best example but good enough full example to start you).
EDIT
Also, if logging out through the partner site, another method I have used in the past (which was with O2 and other such sites) they were given a 'callback' (REST API call in most cases) which they would also need to call when the user logs out of their site.
The database solution means that the session database needs to be shared between mainwebsite and the partner site, which frequently isn't the case etc. Maybe something along these trivial lines would suffice?
<img src="mainwebsite/logout.php">
mainwebsite/logout.php:
<?php session_destroy(); ?>

Categories