Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

You should be able to revoke JWT's #1336

Closed
ekryski opened this issue Feb 22, 2016 · 12 comments · Fixed by feathersjs/docs#1420
Closed

You should be able to revoke JWT's #1336

ekryski opened this issue Feb 22, 2016 · 12 comments · Fixed by feathersjs/docs#1420

Comments

@ekryski
Copy link
Contributor

ekryski commented Feb 22, 2016

At some point we need to implement this I think. It's not a massive rush though as this can be left to the developer. It's also not super trivial but if you are using HTTPS, because we don't expose the JWT in the query string, it's not very likely that it would be maliciously stolen.

See https://auth0.com/blog/2015/03/10/blacklist-json-web-token-api-keys/ for details.

@ekryski
Copy link
Contributor Author

ekryski commented Jun 19, 2016

I did a bunch more reading this weekend about this and I think that keeping TTL's for our JWT access tokens short (ie. < 30 minutes) is better than managing a blacklist.

The problem with maintaining a blacklist of of revoked tokens is that you effectively need to have a centralized store for the blacklist (ie. most likely a redis cache or your db that all services or at least the auth service have access to), which is pretty much a session store and imho kind of removes the statelessness of JWT. Not to mention it's added complexity for new projects. However, we do need a solution, in the event that a JWT is stolen. In reality, the likelihood of this is low if you are using HTTPS but it could happen if you are susceptible to XSS.

Proposed Solution

I think this is how stuff should work:

  1. client authenticates and gets a JWT with short TTL < 30 minutes by default (but configurable)
  2. client stores token in localStorage (this already happens)
  3. on every request made to the server, the client checks the token expiry. If the token is still valid for a while proceed to step 4. If the token is about to expire (ie. in 1 minute - also configurable) the client first makes a request to refresh the token, sending the soon to expire JWT. The client gets a new token and repeats step 2 and then proceeds to step 4.
  4. The client makes the request passing the currently valid JWT

All this happens over HTTPS. If a request with an invalid token is made the client is directed to re-authenticate (ie. login again).

This effectively creates a rolling window, whereby the client is only authenticated for as long as they are actively making requests within the TTL of the JWT.

If you are concerned with a security breach within this window you can simply change the FEATHERS_AUTH_SECRET, which will invalidate all JWTs and force every client to re-authenticate.

How to refresh tokens fit in?

So I think our current setup is working pretty well (except that the 1 day TTL is too large) and with these changes will work just fine for web. Refresh tokens aren't that valid for web because they can't really be stored securely (I could be wrong here) but they are a good solution for mobile. It would be a shitty user experience to have to log in every time you open the app because you had it backgrounded for > 30 minutes. Since refresh tokens can be stored securely on the device I'm suggesting that we do support this or at least provide a guide for how to do it.

How this might work

  1. User signs up
  2. They verify their account
  3. The server generates an access token (JWT) and a refresh token (a different JWT or UUID) and stores the refresh token with:
    • the date it was created
    • the user or device id, or device name
    • the expiry
  4. The server sends back the access token and the refresh token
  5. The client automatically stores the access token and securely stores the refresh token (possibly encrypted using the user's password?)
  6. The JWT access token is used the same way as above, but if it is expired the client can get a new one by using the refresh token.

When refreshing the access token using the refresh token the server would look up the refresh token, ensure it exists and is not expired. If it is invalid return an error to re-authenticate. If not, return a new access token.

Some notes:

  • If a user resets their password, automatically replace their old refresh token with a new one, thereby rendering an old token useless
  • If a token is revoked it is simply removed from the store
  • This could work on the browser if you can encrypt the token. Maybe by prompting the user to enter their password, but since you'd be limited to localStorage for a storage location this probably isn't a good option for web because localStorage just isn't secure.

Changes Required

  • Shorten default token TTL to 30 minutes
  • Add subject as the user id to the JWT claims by default
  • Generate a client id and put it in the config
  • Generate a UUID for the jti claim param
  • Add audience as the client id to the JWT claims by default
  • Set up refresh endpoint properly
  • Add support in feathers client to inspect the TTL of token and refresh it accordingly

Additional Questions/Concerns/Thoughts

  • Can't an attacker continually just refresh the token to keep the window alive? Yes. I don't think we can prevent this if someone gets a malicious script on their site via XSS but maybe we can detect this behavior? I'm not sure that we could differentiate this from normal use so it's probably out of scope for the framework and just up to the developer to keep an eye out for malicious activity.

  • An attacker could quickly hit the refresh endpoint a bunch and get a lot of valid JWTs that they could distribute to others. Is this a valid concern? Can we prevent this? Maybe we can detect multiple refreshes within the authentication window?

  • In theory even with a low TTL, if an attacker is fast enough they can cause damage. No way around that but there is a tradeoff between the performance overhead of making requests to refresh tokens vs the security. I think allowing the the TTL to be configurable strikes a good balance. IMHO this is an acceptable risk and something that almost every token solution is susceptible to.

  • We can and should store user scopes/access permissions in the token so that we don't have to hit the DB to lookup the user. Something to this effect, inside the JWT payload:

    "scopes": ["user:read", "user:write"]
    
  • If we do store permissions/scopes this would mean that if a permission is revoked that revocation would only take effect the next time an access token is issued (ie. at the end of the access window).

  • we could emit a "logged-out" event from the server when the JWT access token is checked and it is expired. Maybe not a big deal.

Resources

Would love to hear thoughts @BigAB @feathersjs/core-team

@daffl
Copy link
Member

daffl commented Jun 19, 2016

I'd like to support something like app.service('auth/token').remove() to emulate a logout. Through after hooks for the auth services we could then add something like app.on('login', user => {}) and app.on('logout', user => {}). This has been asked for many times already and is probably useful.

app.service('auth/token').remove() wouldn't really do anything (except for maybe disconnecting the socket which we are currently doing through a separate logout event). We could however also support a storage provider like we are using on the client already that one can (optionally) implement it with their Redis/MongoDB/whatever storage to blacklist the token if they wanted to. Since it is optional it wouldn't add too much overhead I think.

I don't think that scopes should be stored in the token. If we want to avoid hitting the database we can cache the users by id in memory (or another cache where we could again use our key-value store interface) and listen to patched, updated and removed events on the user service to keep them up to date.

Everything else sounds good to me.

@BigAB
Copy link

BigAB commented Jun 29, 2016

Sorry for the late reply to this.

Here are my thoughts.

The rolling window

The "rolling window" part of this idea strikes me as a problem. It's this idea of the "lottery" request, where if you happen to make your next request between 29:00 and 29:59 you'll be refreshed, but otherwise you'll be logged out, honestly in practice this may as well just log out users every 30 minutes, because it will feel like that to them anyway for how many times this will happen. The configuration doesn't matter, it's the base concept that seems flawed.

To keep it stateless, and keep an active user logged in, you may as well issue a new token with every single valid request (you could even debounce for 1 min maybe). I'd be willing to bet that that would be "performant enough" and keep the basic outline of what you want. You still have the same weakness though: attacker who gets JWT can eternally refresh by hitting the API every 29 mins, just to keep it valid.

The refresh-token

So basically, as I understand it, the point of the refresh-token is to extend the effective TTL of the access-token, by allowing request for an new access-token to succeed without credentials for the lifetime of the refresh-token. (so like for example your mobile app example, where mobile apps stay logged in for a long time, or until revoked/blacklisted).

If a token is revoked it is simply removed from the store

This part you've written about refresh-tokens seems a little weird to me.

Presumably you wouldn't "revoke" your refresh-token from the client that is storing the token, that would be exactly like "logging out" (which should delete both access and refresh-tokens). The need for revoking or black listing would be from the server, were you think for whatever reason a token was compromised, or you are preventing from being compromised in the future. I wouldn't confuse this with a client choosing to log out though.

Actually, I start to question the value of the refresh-token if it cant be revoked, doesn't it end up being just like having a longer lived access-token at that point? (maybe it's quicker to read out of the insecure storage, so there is a slight benefit for client side performance, but would that be worth it?)

I don't know.

In short

I really dislike the idea of the "window of opportunity" to refresh automatically, it sounds like it would never work. Maybe a new token every request would work, but what would that do to performance.

Is the refresh token worth it if it can't be revoked? Could you just give non-web clients longer lived JWTs and store them securely?

@dijonkitchen
Copy link

Any updates on the "state of the art" way to do this?

@daffl
Copy link
Member

daffl commented Jan 22, 2018

Create a service for removed tokens and create a JWT verifier that checks the token against that service. The existing verifier is a good base, all you should have to do is additionally do a find against the revoked-token service.

@jacktuck
Copy link

Isn't local-storage more vulnerable than a cookie?

@amaury1093
Copy link

@jacktuck Not really, they just aren't vulnerable to the same type of attack:

  • localStorage can be compromised via an XSS injection
  • cookies are vulnerable to CSRF attacks

If your webapp uses either of those, it needs to be protected against the relevant attack vector.

@jacktuck
Copy link

jacktuck commented Mar 29, 2018

@amaurymartiny Thanks

On another note, has anyone implemented revocation without storing the tokens themselves. I'm personally not a fan of storing long lived tokens (access token or refresh token) on the server. It's kind of like storing passwords isn't it?

How about storing an integer on the user which corresponds to an integer stored in the JWT (like a jwt version). So when you validate the JWT you check the claim for this counter is equal to what you have stored on the user. Then whenever you want to revoke a user's tokens you increment the counter on the user profile so they no longer match. For example when user changes their password, logs out of all devices or user is misbehaving.

Pros:

  • Fetching user is usually 'free' since you will need the user object anyway.
  • No need to store tokens
  • No session affinity as tokens are not looked up
  • Likely faster since you do not have to lookup the token

Cons:

  • You can't say, hey server, forget this token; it's all or nothing.
  • Due to the above, logout would rely on clients clearing the old token (which could be long lived). But if the old token was leaked, the user could still use the 'Log out of all devices except this one' to revoke it - which is discussed below.
  • For "log out of all devices except the current one" you could reissue the user a new token but require them to also send you the current password here. (This looks like how slack does that)
  • Strictly speaking this breaks statelessness since this integer is carrying state of the jwt version

Would be interested on thoughts on this approach, are there any cons/caveats i have missed?

:)

@arash16
Copy link

arash16 commented Feb 9, 2019

Here's another "state of the art" approach with more flexibility for revokation:
https://github.com/feathersjs/authentication/issues/22#issuecomment-461794803

@daffl daffl transferred this issue from feathersjs-ecosystem/authentication May 8, 2019
@averri
Copy link

averri commented May 9, 2019

This is a good discussion. There is nothing in the JWT specification, the RFC 7519, that describe the revocation mechanism.

It's relatively easy to implement such revocation mechanism, and this implies in loosing the statelessness aspect of JWT, as the server will have to maintain a list of the revoked JWTs.

A common approach for revoking JWTs is to store the user ID, the jti claim, the exp claim and the device ID (an unique fingerprint of the device - a browser in the desktop, or a mobile device). The server needs to store these data until the expiration, which is controlled by the exp claim. So it's possible to calculate the amount of data to be stored in the back-end, possibly a key-value distributed database, considering a particular statistical distribution of the revocations. The amount of data will be stable for a particular statistical distribution depending on the value of the exp. The use of the device ID is optional, but this introduces a fine control of the logout mechanism, it's possible to logout a user from a particular device or all devices.

It worth mentioning: using this mechanism invalidates the benefits of JWT. You'll have to check the JWT signature and also the database in order to see if the JWT is revoked. If the revoking mechanism is needed, it could be simplified by using a simple string token.

@KidkArolis
Copy link
Contributor

It worth mentioning: using this mechanism invalidates the benefits of JWT.

By default in Feathers, and in general any framework really, the user object is already fetched from the datastore on each request. This is done because most often the authorization logic requires looking at the user object (e.g. organisationId, role, etc.). Checking some extra column for expiration details (either to disallow invalidated token or to force user to relogin every 30 days) comes at no extra cost in that case.

This is just a default behaviour of course and you could avoid querying the user on each request if you have a high load api.

@daffl
Copy link
Member

daffl commented Jan 18, 2020

Documentation for this has been added to the Cookbook.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants