Jump to content
Sign in to follow this  
mds1256

Access Tokens

Recommended Posts

Hi

 

I have a question about generating a unique access token.

 

I have read a lot on the internet about just using the php 'random_bytes' function.

 

However I have found a scenario (although highly unlikely) where a session could potentially be hijacked.

 

User 1 logs in and gets an access token of 'abcdef' (simplifying things).

 

User 1 uses the system for a period of time but the token expires but doesn't get refreshed yet (as user 1 is idle) (so client still has access token stored on client).

 

In the mean time User 1 decides to use a different device to login to their account which generates a new access token 'zxcvbn'.

 

But then User 2 logs in and they get an access token of 'abcdef' as it is unique (like I say highly unlikely but still possible).

 

Then User 1 goes back to their original device and tries to resume session (client still has 'abcdef' as an access token so it sends that to the server which then it finds it but is actually now against a different user), and then hijacks User 2's session unknowingly.

 

What is the best way around this, what about always adding the internal user ID number to the token to make sure it will always be against the same user (and always truly unique for that user)?

 

So we would end up with 'abcdef1' for user 1 and User 2s access token would be 'abcdef2' if the above scenario was to occur so we would never get a potential hijack of sessions?

 

I know this sounds convoluted but just wanted peoples thoughts?

Share this post


Link to post
Share on other sites

What is the best way around this, what about always adding the internal user ID number to the token to make sure it will always be against the same user (and always truly unique for that user)?

User ID? Isn't this access token replacing the need for user credentials? You wouldn't be able to know if the user ID changed because you're using the access token to identify the user.

 

Anyway, an access token is just a special form of session identifier. Protect it the same way.

Share this post


Link to post
Share on other sites

Assuming you have sufficiently random and long tokens, your scenario isn't just 'highly unlikely', it's 'borderline impossible'. However, ...

 

If your tokens are being used for login purposes then your token is basically a password. That means you need to store it in your database as you would a password - as a hash value. Use password_hash and password_verify

 

Now, since your token is stored as a hash you can't just do something like select * from users where token=? to compare it, you need some other lookup value to first find the user then compare the hash with the submitted token. One such identifier could be the user's ID if you wanted, just include it with the token in some way that you can extract it properly.

 

What I've done in the past is instead give each token it's own ID. I treat them like their own entity in my system rather than just as a property of something else. Tokens then have arbitrary context data that the system uses to determine what the token is for. My tokens then take the form of id-token. The token is split into it's ID and Token components then verified.

 

So if by some miracle that situation were to occur, the ID would ensure they are in fact separate tokens. As a bonus using arbitrary token ID's your also not leaking any information about the user that token is associated with.

  • Like 1

Share this post


Link to post
Share on other sites

User ID? Isn't this access token replacing the need for user credentials? You wouldn't be able to know if the user ID changed because you're using the access token to identify the user.

 

Anyway, an access token is just a special form of session identifier. Protect it the same way.

 

Not user name. It's the user id...... Internal DB incremental primary key ID

Edited by mds1256

Share this post


Link to post
Share on other sites

Assuming you have sufficiently random and long tokens, your scenario isn't just 'highly unlikely', it's 'borderline impossible'. However, ...

 

If your tokens are being used for login purposes then your token is basically a password. That means you need to store it in your database as you would a password - as a hash value. Use password_hash and password_verify

 

Now, since your token is stored as a hash you can't just do something like select * from users where token=? to compare it, you need some other lookup value to first find the user then compare the hash with the submitted token. One such identifier could be the user's ID if you wanted, just include it with the token in some way that you can extract it properly.

 

What I've done in the past is instead give each token it's own ID. I treat them like their own entity in my system rather than just as a property of something else. Tokens then have arbitrary context data that the system uses to determine what the token is for. My tokens then take the form of id-token. The token is split into it's ID and Token components then verified.

 

So if by some miracle that situation were to occur, the ID would ensure they are in fact separate tokens. As a bonus using arbitrary token ID's your also not leaking any information about the user that token is associated with.

That sounds like a good idea, so give the token its own id, with a token I didn't think you had to hash the value as it changes frequently and is not like a password.

 

I will be authenticating the user with a username and password to then gain an access token and refresh token (both will have expiry dates/times), I thought I would just need to generate randomness and then use this value for a limited time and I would be ok?

Share this post


Link to post
Share on other sites

You are right it does sound convoluted. Your example tokens are very short. Why? Tokens aren't typically meant to be exposed to users or necessary for them to understand or type.

 

How about simply guaranteeing that the session id is unique in the database through a unique constraint?

 

Or you could use a GUID you hash with SHA2, which solves the same concern that a random_bytes routine solves for you in this context without disclosing the essential nature of the token, and at the same time being guaranteed unique.

Share this post


Link to post
Share on other sites

You hash them so that if your database is accessed in some way (sql injection, server hack, etc) the attacker can't gain access to any account by simply copying the access token. The same reason you hash passwords to protect them from a database theft.

Share this post


Link to post
Share on other sites

You are right it does sound convoluted. Your example tokens are very short. Why? Tokens aren't typically meant to be exposed to users or necessary for them to understand or type.

 

How about simply guaranteeing that the session id is unique in the database through a unique constraint?

 

Or you could use a GUID you hash with SHA2, which solves the same concern that a random_bytes routine solves for you in this context without disclosing the essential nature of the token, and at the same time being guaranteed unique.

My example was for quickly getting the point across without typing an example token that actually wouldnt be anymore helpful.

 

You have missed the point regarding the unique constraint. As my example the tokens were different in the database. It was the client that had the other persons token.

Edited by mds1256

Share this post


Link to post
Share on other sites

You hash them so that if your database is accessed in some way (sql injection, server hack, etc) the attacker can't gain access to any account by simply copying the access token. The same reason you hash passwords to protect them from a database theft.

True. Never thought of it being used that way.

Share this post


Link to post
Share on other sites

You have missed the point regarding the unique constraint. As my example the tokens were different in the database. It was the client that had the other persons token.

No I didn't miss anything. It seems you don't understand the idea of a unique constraint. A unique constraint is essentially in most databases a unique index. It doesn't have to be on a single column but in your case it would be.

 

With the constraint there is no way that user2 could get the same token allocated - ever. It is not possible for 2 people to be allocated the same id and have that stored. The attempted insert/update would cause an exception and you would probably write code to handle that and simply regenerate another token.

 

With that said again, a solution that solves token concerns (unpredictable/guessable, guaranteed unique):

 

 

 

// Just generate 20 guids and 20 tokens to show the idea. Obviously it's simpler...
// $token = hash('sha256', uniqid('', true);
<?php

$count = 0;
while ($count < 20) {
    $tokenInput = uniqid('', true);
    $tokenHashed = hash('sha256', $tokenInput);
    echo "Token: $tokenInput | Hashed: $tokenHashed \n";
    $count++;
}

Share this post


Link to post
Share on other sites

No I didn't miss anything. It seems you don't understand the idea of a unique constraint. A unique constraint is essentially in most databases a unique index. It doesn't have to be on a single column but in your case it would be.

 

With the constraint there is no way that user2 could get the same token allocated - ever. It is not possible for 2 people to be allocated the same id and have that stored. The attempted insert/update would cause an exception and you would probably write code to handle that and simply regenerate another token.

 

With that said again, a solution that solves token concerns (unpredictable/guessable, guaranteed unique):

 

 

 

 

// Just generate 20 guids and 20 tokens to show the idea. Obviously it's simpler...
// $token = hash('sha256', uniqid('', true);
<?php

$count = 0;
while ($count < 20) {
    $tokenInput = uniqid('', true);
    $tokenHashed = hash('sha256', $tokenInput);
    echo "Token: $tokenInput | Hashed: $tokenHashed \n";
    $count++;
}

Sorry but you are missing what I am saying.

 

The scenario above meant that in the database the users still had unique tokens at that point in time as the token had changed for user 1 but then User 2s authentication caused it to pick a token for user 2 that was the same as what user 1s token was before it was changed, so any tokens that was stored on the stale client would then now authenticate against user 2s access token instead of user 1 (as user 2s token is the same as what user 1s token was before it was changed, so a unique constraint wouldn’t help here as the two tokens are different)

Share this post


Link to post
Share on other sites

The scenario above meant that in the database the users still had unique tokens at that point in time as the token had changed for user 1 but then User 2s authentication caused it to pick a token for user 2 that was the same as what user 1s token was before it was changed, so any tokens that was stored on the stale client would then now authenticate against user 2s access token instead of user 1 (as user 2s token is the same as what user 1s token was before it was changed, so a unique constraint wouldn’t help here as the two tokens are different)

I'm just repeating myself. Are you trying to understand?

 

If you place a unique constraint on the COLUMN that stores tokens (let's assume that the table is named 'users' and the column name is 'token'....

 

Then it is IMPOSSIBLE for you to store the SAME TOKEN for 2 different users, because ALL token values in the 'users' table have to be unique. Once there is one token of 'abc123', even if you were to "generate" the same token again (via some algorithm that creates strings of random characters) once your code was to try to perform a database UPDATE that sets the value of users.token to 'abc123' the database would generate an ERROR and the UPDATE query would FAIL.

 

This is basic database 101. The indexes that are created for Primary keys have this exact property. If you have a user.id = 1, you will not be able to INSERT another user with user.id = 1. That query FAILS.

 

Now let's assume you don't want to even concern you with this problem. How might you solve the problem of token allocation so that no 2 users can get the same token --- EVER IN THE FULL LIFETIME NOT ONLY OF YOUR APP BUT ALSO WITHIN THE LIFETIME OF OUR SOLAR SYSTEM GIVEN CURRENT and within our lifetime COMPUTATIONAL ABILITIES.

 

 

$token = hash('sha256', uniqid('', true);
Problem solved.

 

Now before you reply to this for the 3rd time stating that I don't understand your post, make sure that you investigate the functions provided and give proper consideration, and then explain how, given either of the 2 solutions I provided, that 2 users could possibly have the same token value generated.

Share this post


Link to post
Share on other sites

I believe his hypothetical situation isn't so much about token duplication as it is token reuse + caching, which a unique constraint doesn't really help with.

 

User A logs in with Firefox on their desktop and is assigned token abcdef:

   User   |  Token
----------+----------
 UserA    |  abcdef
User A leaves the house, and signs in later on their phone. They are now assigned token zxcvbn. Their DB record is updated

   User   |  Token
----------+----------
 UserA    |  zxcvbn
User B logs in and is assigned token abcdef

   User   |  Token
----------+----------
 UserA    |  zxcvbn
 UserB    |  abcdef
UserA comes home and reloads the page. Firefox still thinks their token is abcdef and send that. Server looks up abcdef and pulls User B's account info.

 

Even though the above should never happen with proper tokens, the question is how can you guarantee it never happens. A unique constraint would only work if you saved a record of every token ever produced

Edited by kicken

Share this post


Link to post
Share on other sites

I believe his hypothetical situation isn't so much about token duplication as it is token reuse + caching, which a unique constraint doesn't really help with.

 

User A logs in with Firefox on their desktop and is assigned token abcdef:

 

User   |  Token
----------+----------
 UserA    |  abcdef
User A leaves the house, and signs in later on their phone. They are now assigned token zxcvbn. Their DB record is updated

User   |  Token
----------+----------
 UserA    |  zxcvbn
User B logs in and is assigned token abcdef

User   |  Token
----------+----------
 UserA    |  zxcvbn
 UserB    |  abcdef
UserA comes home and reloads the page. Firefox still thinks their token is abcdef and send that. Server looks up abcdef and pulls User B's account info.

 

Even though the above should never happen with proper tokens, the question is how can you guarantee it never happens. A unique constraint would only work if you saved a record of every token ever produced

This is exactly the question but better explained :) which like you say that a unique constraint doesn’t help.

 

@gizmola I wasn’t been funny about saying you don’t understand, but you just weren’t getting what I was trying to get across where as @kicken has got it spot on

Edited by mds1256

Share this post


Link to post
Share on other sites

So I have been thinking about this a little more.

 

If when generating the token I prefix the token with userid.

 

E.g. ‘2.abcde’ would be returned to the client as the token

 

I then store the hash of the ‘abcde’ actual token in the database, then substring the numbers to the first period and make sure that when verifying the token that the user id and period is stripped and run what is left of the actual token through password verify but also make sure the user id also matches?

Edited by mds1256

Share this post


Link to post
Share on other sites

I then store the hash of the ‘abcde’ actual token in the database, then substring the numbers to the first period and make sure that when verifying the token that the user id and period is stripped and run what is left of the actual token through password verify but also make sure the user id also matches?

More or less. For example, from an application system I wrote some time ago:

 

private function ValidateRequest(){
    $HasError = false;
    if (!isset($_GET['application'], $_GET['key'])){
        $HasError = true;
    }

    $this->mApplication = $app = Application::GetDetails($_GET['application']);
    if (!$app || !password_verify($_GET['key'], $app['keyHash'])){
        $HasError = true;
    }

    return !$HasError;
}
When users begin an application, they are emailed a URL they may use to resume their application if they can't complete it in one sitting. That url looks like example.com/application/11-1234567890. I use mod_rewrite to split the token and rewrite the URL into example.com/application/index.php?application=11&key=1234567890, but you could do that in code instead.

 

The application record is then found using the application's ID then the hash compared with the given token.

Share this post


Link to post
Share on other sites

I finally understand the disconnect here. There are some basics that are involved with tokens that I took for granted. A token does not and should not identify a user.

 

Username + token is always required.

 

Typically people are using tokens because this is a REST api.

 

The username AND the token are required for every request.

 

If the system was designed to only trust tokens, then I can see the problem being described, but that problem ceases to exist once you include username in the request.

 

If you really need want to roll your own token system, then using JWT is a best practice and has additional advantages over something primitive like generating a random string.

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Sign in to follow this  

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.