Help with concurrent login

Discussions of secure PHP coding. Security in software is important, so don't be afraid to ask. And when answering: be anal. Nitpick. No security vulnerability is too small.

Moderator: General Moderators

User avatar
peetz
Forum Newbie
Posts: 14
Joined: Mon Oct 10, 2005 1:49 pm

Post by peetz »

Confused (?)


Anyway .. back to this fingerprint idea.

I've implemented the fingerprint so that I store a hash of the session id and a hash of the (password . user_agent). I'm hoping this will give a fairly unique couplet.

My problem is now almost what I started with:

When a user logs out properly the session id and fingerprint hashes are cleared from the database (i.e. user has logged out). However, if the user does not log out properly or their browser crashes, etc. I am left with the data in the database - which makes it look like the user is still logged in.

If I wait for a set period this could mean that the user is simply reading the current web page (and is a bit slow) or they have not logged out correctly. If they continue to use the site they will still have the same hashes (therefore no problem). However, if they do not log out correctly and try to access via another browser, workstation, etc. their hashes will be different.

Do I:

1. Log the user out whenever I receive a "bogus" log in (and request they log in again)?
2. Log the user out only after a set period from last log in (e.g. 5 minutes)?

As I see it, there are pro's and con's for both methods. I would really appreciate any advice here.

Thanks
User avatar
Maugrim_The_Reaper
DevNet Master
Posts: 2704
Joined: Tue Nov 02, 2004 5:43 am
Location: Ireland

Post by Maugrim_The_Reaper »

Its a choice...

Take a CMS. It's a simple application where two people would see no utility at all in sharing a single session, or accessing the same account. Hence fingerprinting is not required (and can be detrimental for "Remember Me" features which can operate for the same account on multiple PCs).

Now take a Game, or other application where account sharing is not only dangerous - but detrimental - and where a remember me feature is pure lunacy (since users rarely actually logout properly - or bother). In this instance use fingerprinting - and force a logout on ANY change.

It might be useful to let the fingerprint itself timeout [Edit: timing the fingerprint isn't necessary - it should never be used on any login - only on non-login requests!] - i.e. is your session expiry is 5 minutes, than set a similar timeout for the fingerprint. This would solve the problem of users switching PC's (as they login delete old fingerprints and create a new one - remember if a login occurs, ensure only that user has access - not any prior logged in person on the same account). You can also regenerate the session ID - that will surely mess up people trying to use the same account at once. If a second person logs in, the account session will change, and the original user will be forced to re-login.

This will still help discourage same-time usage - but I'm afraid little can prevent subsequent usage (one user logs in, and another logins into the same account after a logout, or session timeout, or just forces a logout on the other user).

As you can see none of this is fool-proof. We're acting to discourage not completely block this. By making it inconvenient - you can certainly prevent a lot of this misuse.

I think even /me getting confused and off point...;)

Long and short -

- On a login regenerate the session id (session_regenerate() in PHP), create a new fingerprint, and delete any old fingerprint. Result - any current logged in person sharing the account will face a forced logout.
- On all requests, compare fingerprints and force a logout if it changes for any reason (a sign someone may be sharing the session, i.e. using a copied session cookie) per Chris's article - this should not apply to a LOGIN.
- Force a session expiry...it's not quite a full logout (the session data remains) but anyone with that session id who visits after the expiry will be silently logged out (session data deleted) and requested to login again.
- When fingerprint missmatches are found - create a log entry (maybe a database entry to store the username, and time of this event - might let you map who's involved in shared account use) - maybe even inform people such things are logged (more discouragement!).
- Add a clause to your terms of use that shared accounts are forbidden, misuse of that kind will result in account banning (and deletion) and that you log such activity (no need to explain what sort of logging - let them imagine how bad it could be...lol).

Keep in mind that this is not foolproof - it exists to discourage, and make such misuse inconvenient. A truly persistent set of users will probably just ignore all this and start subsequent logins. It might be useful to track the time between logins, and see if some users are logging in a few times in succession from different PCs (i.e. alternating requests between PCs within short timespans...)

I think logging is pretty useful - lets you make judgement calls where users are outwitting the system.
User avatar
peetz
Forum Newbie
Posts: 14
Joined: Mon Oct 10, 2005 1:49 pm

Post by peetz »

Thanks for this fantastically comprehensive post.

You've given me some very interesting points to think about.
User avatar
Maugrim_The_Reaper
DevNet Master
Posts: 2704
Joined: Tue Nov 02, 2004 5:43 am
Location: Ireland

Post by Maugrim_The_Reaper »

You mean it made sense ;) I hope it did - I face the same issue too...
User avatar
peetz
Forum Newbie
Posts: 14
Joined: Mon Oct 10, 2005 1:49 pm

Post by peetz »

Yep, made complete sense. In fact, I'd already implemented some of the points you made. Just so that I can straighten my thoughts i'll spout them.

1. When a user logs on, I regenerate a new session id, create a new fingerprint and store the hashes of these (using the users password as a seed) along with their log in time.
2. When user restricted pages are loaded, I check session id and fingerprint and force a logout if it changed - if they haven't changed I allow the user to view and update last activity time in database.
3. When session id and/or fingerprint checks fail, I log this entry in the database, clear session id and fingerprint in database (which means that user is effectively logged out).


Things I hadn't considered though
- Force a session expiry...it's not quite a full logout (the session data remains) but anyone with that session id who visits after the expiry will be silently logged out (session data deleted) and requested to login again.
- Add a clause to your terms of use that shared accounts are forbidden, misuse of that kind will result in account banning (and deletion) and that you log such activity (no need to explain what sort of logging - let them imagine how bad it could be...lol).
If i'm right, the session expiry (silent logout) idea is simply to check for the last log-in time and, if greater than a set time period (e.g. 1 hour), clear the session id and fingerprint in the database and therefore bounce the user to the log in page so that a new session id and fingerprint would be generated and stored (along with their last activity time).

T&C will be updated when I get all this sorted - low priority at the mo but a definate alteration I need to make.

Fantastic!

One more idea to run past you. To catch users who don't log out correctly (i.e. leave site, close browser, etc.) is it feasible to use the onUnload event?
User avatar
Maugrim_The_Reaper
DevNet Master
Posts: 2704
Joined: Tue Nov 02, 2004 5:43 am
Location: Ireland

Post by Maugrim_The_Reaper »

One more idea to run past you. To catch users who don't log out correctly (i.e. leave site, close browser, etc.) is it feasible to use the onUnload event?
Not needed - just use a session expiry. ;)
User avatar
feyd
Neighborhood Spidermoddy
Posts: 31559
Joined: Mon Mar 29, 2004 3:24 pm
Location: Bothell, Washington, USA

Post by feyd »

onunload fires for every page change.. FYI

As maugrim stated, simply use the session expiration (and garbage collection) routines to do it.
User avatar
peetz
Forum Newbie
Posts: 14
Joined: Mon Oct 10, 2005 1:49 pm

Post by peetz »

So, it looks like I have to do what I was trying to stay away from - write my own session handler.

If I am to do this, is it possible to just write the garbage collection part of this or do I have to write the entire handler. Also, is it possible to get hold of the defualt php session handler and modify this (i.e. add the database connection, insert, update and delete functionality into this).

I think writing the entire handler from scratch is just beyond me at the moment. I'm sure I could manage it given time, but it seems like a *huge* learning curve.

Any tips?
User avatar
Maugrim_The_Reaper
DevNet Master
Posts: 2704
Joined: Tue Nov 02, 2004 5:43 am
Location: Ireland

Post by Maugrim_The_Reaper »

You can just start off simple - nothing here requires a full blown session handler (yet...). You can easily set up some basic functions mixing the session/db logging and fingerprinting checking with the basic PHP file sessions.

If you know OOP you could add it to a class (not essential though).

What makes you think you needed a specialised session handler?
User avatar
peetz
Forum Newbie
Posts: 14
Joined: Mon Oct 10, 2005 1:49 pm

Post by peetz »

use the session expiration (and garbage collection) routines to do it
The way I had envisaged doing this was to check on log-in or on authorised page requests and deal with the session/fingerprints checking then. Allowing users through or kicking them back to login page.

Forgive my ignorance but is this "garbage collection"? I have always assoc. garbage collection with the php session handler.



OR

Create a cronjob which executes a server-side php script (returning to null device) which cleans up all expired sessions from the database. Get this to trigger every so often (i.e. as often as the session timeout - default is 24mins I think - 1440secs). I know this is possible but is it not the way to go here?
timvw
DevNet Master
Posts: 4897
Joined: Mon Jan 19, 2004 11:11 pm
Location: Leuven, Belgium

Post by timvw »

peetz wrote:Not sure I understand what you mean.

The thing I am trying to prevent is having a user log-in more than once at a time.
Well, if you allow each user whenever he wants (but expire his previous sessions/access tokens) a user can log in only once...
User avatar
peetz
Forum Newbie
Posts: 14
Joined: Mon Oct 10, 2005 1:49 pm

Post by peetz »

Well, if you allow each user whenever he wants (but expire his previous sessions/access tokens) a user can log in only once...
True, but not exactly what I want this to do. Basically I want to discourage/prevent concurrent logins from the same user. This means that if a user is already logged in, and someone else tries to access using this account, they will be prevented from doing so but the original user will be unaffected.
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Post by Ambush Commander »

I was thinking about this problem, and going to suggest exactly what timvw said.
True, but not exactly what I want this to do. Basically I want to discourage/prevent concurrent logins from the same user. This means that if a user is already logged in, and someone else tries to access using this account, they will be prevented from doing so but the original user will be unaffected.
Yes, timvw's method works for that too. Let's consider.

1. USER issues a request for STATUS of gameworld
2. SERVER sends TOKEN and STATUS. TOKEN is redeemable for ACTION.
3. USER, being sneaky, issues another request for STATUS of gameworld.
4. SERVER sends TOKEN and STATUS. TOKEN is redeemable for ACTION. All Previous tokens are INVALID
5. USER tries to commit actions from both STATUS TOKENS. In this case, only the second instance would commit, the first would get an invalid token error.
the original user will be unaffected.
This is a Pessimistic lock (which is what you've been discussing and grappling with the various issues).
User avatar
Maugrim_The_Reaper
DevNet Master
Posts: 2704
Joined: Tue Nov 02, 2004 5:43 am
Location: Ireland

Post by Maugrim_The_Reaper »

Like a transaction chain?

With every request, you issue a new token to validate the next request...and so on. Maybe I'm too tired to think (late night)...but what happens when another person logs into the same account, won't that break the chain? Because it really should unless you're entering the territory of denying logins. IMHO you shouldn't deny any valid logins because that could be something misused to lock out the actual owner of the account, by another concurrent user.

Of course there's probably some exception for a login request...
User avatar
peetz
Forum Newbie
Posts: 14
Joined: Mon Oct 10, 2005 1:49 pm

Post by peetz »

Got it all working now - thanks all :D

The way i've set it up is so that when a user logs in I regenerate a new session id and store hashes of session id and fingerprint (hashed password combined browser info).

When someone else tries to access this account a check takes place against the stored sessionid and fingerprint and newly constructed ones from the login process. If these are the same then it lets the user through (i.e. it is the same user revisiting within their timeout period). If these are not the same it locks the new user out for the rest of the timeout period, allowing the first user to continue uninterupted - this attempt is logged for admin to browse later.

When the user is locked out they are sent to the login page and given a message about account security and logging out properly at the end of their session. There is also an option to "override" this by forcing a login - effectively chucking out the currently logged in user (this too is logged for admin to browse). I only added this feature to overcome the problems mentioned by Maugrim.
IMHO you shouldn't deny any valid logins because that could be something misused to lock out the actual owner of the account, by another concurrent user.
This balance, I hope, will make it a complete pain for users trying to login under the same username more than once (they get thrown out first, then must override - other users using same account must do the same for every new page request - and so it goes on) but also gives geniune users the ability to log in and override the "lockout" if they have accidentally closed their browser, recovered from crash with timeout period, etc.

I have also set a cronjob to run every so often to clean up all timed-out sessions from the database - more for housekeeping and a "who's online section" sake than anything else.


So, there we have it. I'm sure it's not perfect, and i'm sure there are loads of ways that I could have done this better - but it works for me at the moment. :wink:


Thanks again.
Post Reply