Invalidating JSON Web Tokens

2019-01-01 11:44发布

问题:

For a new node.js project I\'m working on, I\'m thinking about switching over from a cookie based session approach (by this, I mean, storing an id to a key-value store containing user sessions in a user\'s browser) to a token-based session approach (no key-value store) using JSON Web Tokens (jwt).

The project is a game that utilizes socket.io - having a token-based session would be useful in such a scenario where there will be multiple communication channels in a single session (web and socket.io)

How would one provide token/session invalidation from the server using the jwt Approach?

I also wanted to understand what common (or uncommon) pitfalls/attacks I should look out for with this sort of paradigm. For example, if this paradigm is vulnerable to the same/different kinds of attacks as the session store/cookie-based approach.

So, say I have the following (adapted from this and this):

Session Store Login:

app.get(\'/login\', function(request, response) {
    var user = {username: request.body.username, password: request.body.password };
    // Validate somehow
    validate(user, function(isValid, profile) {
        // Create session token
        var token= createSessionToken();

        // Add to a key-value database
        KeyValueStore.add({token: {userid: profile.id, expiresInMinutes: 60}});

        // The client should save this session token in a cookie
        response.json({sessionToken: token});
    });
}

Token-Based Login:

var jwt = require(\'jsonwebtoken\');
app.get(\'/login\', function(request, response) {
    var user = {username: request.body.username, password: request.body.password };
    // Validate somehow
    validate(user, function(isValid, profile) {
        var token = jwt.sign(profile, \'My Super Secret\', {expiresInMinutes: 60});
        response.json({token: token});
    });
}

--

A logout (or invalidate) for the Session Store approach would require an update to the KeyValueStore database with the specified token.

It seems like such a mechanism would not exist in the token-based approach since the token itself would contain the info that would normally exist in the key-value store.

回答1:

I too have been researching this question, and while none of the ideas below are complete solutions, they might help others rule out ideas, or provide further ones.

1) Simply remove the token from the client

Obviously this does nothing for server side security, but it does stop an attacker by removing the token from existence (ie. they would have to have stolen the token prior to logout).

2) Create a token blacklist

You could store the invalid tokens until their initial expiry date, and compare them against incoming requests. This seems to negate the reason for going fully token based in the first place though, as you would need to touch the database for every request. The storage size would likely be lower though, as you would only need to store tokens that were between logout & expiry time (this is a gut feeling, and is definitely dependent on context).

3) Just keep token expiry times short and rotate them often

If you keep the token expiry times at short enough intervals, and have the running client keep track and request updates when necessary, number 1 would effectively work as a complete logout system. The problem with this method, is that it makes it impossible to keep the user logged in between closes of the client code (depending on how long you make the expiry interval).

Contingency Plans

If there ever was an emergency, or a user token was compromised, one thing you could do is allow the user to change an underlying user lookup ID with their login credentials. This would render all associated tokens invalid, as the associated user would no longer be able to be found.

I also wanted to note that it is a good idea to include the last login date with the token, so that you are able to enforce a relogin after some distant period of time.

In terms of similarities/differences with regards to attacks using tokens, this post addresses the question: http://blog.auth0.com/2014/01/07/angularjs-authentication-with-cookies-vs-token/



回答2:

The ideas posted above are good, but a very simple and easy way to invalidate all the existing JWTs is simply to change the secret.

If your server creates the JWT, signs it with a secret (JWS) then sends it to the client, simply changing the secret will invalidating all existing tokens and require all users to gain a new token to authenticate as their old token suddenly becomes invalid according to the server.

It doesn\'t require any modifications to the actual token contents (or lookup ID).

Clearly this only works for an emergency case when you wanted all existing tokens to expire, for per token expiry one of the solutions above is required (such as short token expiry time or invalidating a stored key inside the token).



回答3:

This is primarily a long comment supporting and building on the answer by @mattway

Given:

Some of the other proposed solutions on this page advocate hitting the datastore on every request. If you hit the main datastore to validate every authentication request, then I see less reason to use JWT instead of other established token authentication mechanisms. You\'ve essentially made JWT stateful, instead of stateless if you go to the datastore each time.

(If your site receives a high volume of unauthorized requests, then JWT would deny them without hitting the datastore, which is helpful. There are probably other use cases like that.)

Given:

Truly stateless JWT authentication cannot be achieved for a typical, real world web app because stateless JWT does not have a way to provide immediate and secure support for the following important use cases:

User\'s account is deleted/blocked/suspended.

User\'s password is changed.

User\'s roles or permissions are changed.

User is logged out by admin.

Any other application critical data in the JWT token is changed by the site admin.

You cannot wait for token expiration in these cases. The token invalidation must occur immediately. Also, you cannot trust the client not to keep and use a copy of the old token, whether with malicious intent or not.

Therefore: I think the answer from @matt-way, #2 TokenBlackList, would be most efficient way to add the required state to JWT based authentication.

You have a blacklist that holds these tokens until their expiration date is hit. The list of tokens will be quite small compared to the total number of users, since it only has to keep blacklisted tokens until their expiration. I\'d implement by putting invalidated tokens in redis, memcached or another in-memory datastore that supports setting an expiration time on a key.

You still have to make a call to your in-memory db for every authentication request that passes initial JWT auth, but you don\'t have to store keys for your entire set of users in there. (Which may or may not be a big deal for a given site.)



回答4:

I would keep a record of the jwt version number on the user model. New jwt tokens would set their version to this.

When you validate the jwt, simply check that it has a version number equal to the users current jwt version.

Any time you want to invalidate old jwts, just bump the users jwt version number.



回答5:

Haven\'t tried this yet, and it is uses a lot of information based on some of the other answers. The complexity here is to avoid a server side data store call per request for user information. Most of the other solutions require a db lookup per request to a user session store. That is fine in certain scenarios but this was created in an attempt to avoid such calls and make whatever required server side state to be very small. You will end up recreating a server side session, however small to provide all the force invalidation features. But if you want to do it here is the gist:

Goals:

  • Mitigate use of a data store (state-less).
  • Ability to force log out all users.
  • Ability to force log out any individual at any time.
  • Ability to require password re-entry after a certain amount of time.
  • Ability to work with multiple clients.
  • Ability to force a re-log in when a user clicks logout from a particular client. (To prevent someone \"un-deleting\" a client token after user walks away - see comments for additional information)

The Solution:

  • Use short lived (<5m) access tokens paired with a longer lived (few hours) client stored refresh-token.
  • Every request checks either the auth or refresh token expiration date for validity.
  • When the access token expires, the client uses the refresh token to refresh the access token.
  • During the refresh token check, the server checks a small blacklist of user ids - if found reject the refresh request.
  • When a client doesn\'t have a valid(not expired) refresh or auth token the user must log back in, as all other requests will be rejected.
  • On login request, check user data store for ban.
  • On logout - add that user to the session blacklist so they have to log back in. You would have to store additional information to not log them out of all devices in a multi device environment but it could be done by adding a device field to the user blacklist.
  • To force re-entry after x amount of time - maintain last login date in the auth token, and check it per request.
  • To force log out all users - reset token hash key.

This requires you to maintain a blacklist(state) on the server, assuming the user table contains banned user information. The invalid sessions blacklist - is a list of user ids. This blacklist is only checked during a refresh token request. Entries are required to live on it as long as the refresh token TTL. Once the refresh token expires the user would be required to log back in.

Cons:

  • Still required to do a data store lookup on the refresh token request.
  • Invalid tokens may continue to operate for access token\'s TTL.

Pros:

  • Provides desired functionality.
  • Refresh token action is hidden from the user under normal operation.
  • Only required to do a data store lookup on refresh requests instead of every request. ie 1 every 15 min instead of 1 per second.
  • Minimizes server side state to a very small blacklist.

With this solution an in memory data store like reddis isn\'t needed, at least not for user information as you are as the server is only making a db call every 15 or so minutes. If using reddis, storing a valid/invalid session list in there would be a very fast and simpler solution. No need for a refresh token. Each auth token would have a session id and device id, they could be stored in a reddis table on creation and invalidated when appropriate. Then they would be checked on every request and rejected when invalid.



回答6:

An approach I\'ve been considering is to always have an iat (issued at) value in the JWT. Then when a user logs out, store that timestamp on the user record. When validating the JWT just compare the iat to the last logged out timestamp. If the iat is older, then it\'s not valid. Yes, you have to go to the DB, but I\'ll always be pulling the user record anyway if the JWT is otherwise valid.

The major downside I see to this is that it\'d log them out of all their sessions if they\'re in multiple browsers, or have a mobile client too.

This could also be a nice mechanism for invalidating all JWTs in a system. Part of the check could be against a global timestamp of the last valid iat time.



回答7:

I\'m a bit late here, but I think I have a decent solution.

I have a \"last_password_change\" column in my database that stores the date and time when the password was last changed. I also store the date/time of issue in the JWT. When validating a token, I check if the password has been changed after the token was issued and if it was the token is rejected even though it hasn\'t expired yet.



回答8:

You can have a \"last_key_used\" field on your DB on your user\'s document/record.

When the user logs in with user and pass, generate a new random string, store it in the last_key_used field, and add it to the payload when signing the token.

When the user logs in using the token, check the last_key_used in the DB to match the one in the token.

Then, when user does a logout for instance, or if you want to invalidate the token, simply change that \"last_key_used\" field to another random value and any subsequent checks will fail, hence forcing the user to log in with user and pass again.



回答9:

  1. Give 1 day expiry time for the tokens
  2. Maintain a daily blacklist.
  3. Put the invalidated / logout tokens into the blacklist

For token validation, check for the token expiry time first and then the blacklist if token not expired.

For long session needs, there should be a mechanism for extending token expiry time.



回答10:

Late to the party, MY two cents are given below after some research. During logout, make sure following things are happening...

Clear the client storage/session

Update the user table last login date-time and logout date-time whenever login or logout happens respectively. So login date time always should be greater than logout (Or keep logout date null if the current status is login and not yet logged out)

This is way far simple than keeping additional table of blacklist and purging regularly. Multiple device support requires additional table to keep loggedIn, logout dates with some additional details like OS-or client details.



回答11:

Why not just use the jti claim (nonce) and store that in a list as a user record field (db dependant, but at very least a comma-separated list is fine)? No need for separate lookup, as others have pointed out presumably you want to get the user record anyway, and this way you can have multiple valid tokens for different client instances (\"logout everywhere\" can reset the list to empty)



回答12:

I did it the following way:

  1. Generate a unique hash, and then store it in redis and your JWT. This can be called a session
    • We\'ll also store the number of requests the particular JWT has made - Each time a jwt is sent to the server, we increment the requests integer. (this is optional)

So when a user logs in, a unique hash is created, stored in redis and injected into your JWT.

When a user tries to visit a protected endpoint, you\'ll grab the unique session hash from your JWT, query redis and see if it\'s a match!

We can extend from this and make our JWT even more secure, here\'s how:

Every X requests a particular JWT has made, we generate a new unique session, store it in our JWT, and then blacklist the previous one.

This means that the JWT is constantly changing and stops stale JWT\'s being hacked, stolen, or something else.



回答13:

Keep an in-memory list like this

user_id   revoke_tokens_issued_before
-------------------------------------
123       2018-07-02T15:55:33
567       2018-07-01T12:34:21

If your tokens expire in one week then clean or ignore the records older than that. Also keep only the most recent record of each user. The size of the list will depend on how long you keep your tokens and how often users revoke their tokens. Use db only when the table changes. Load the table in memory when your application starts.



回答14:

Unique per user string, and global string hashed together

to serve as the JWT secret portion allow both individual and global token invalidation. Maximum flexibility at the cost of a db lookup/read during request auth. Also easy to cache as well, since they are seldom changing.



回答15:

what if you simply generate a new token from the server with expiresIn: 0 and give it back to the Client and store it in the cookie ?



回答16:

I just save token to users table, when user login I will update new token, and when auth equal to the user current jwt.

I think this is not the best solution but that work for me.