1

I've been reading a lot about stateless authentication systems, and I want to implement something with JWT for the first time, but now I'm concerned about how to properly (and securely) consume a RESTful service with this authentication mechanism. So I'll write my approach hoping that someone can detect possible pitfalls, and probably perceive security holes (or just bad practices). Please, don't judge my English :)

But before continuing, assume these claims:

  • MUST be implemented along with HTTPS and a valid SSL certificate
  • The API is RESTful
  • I don't want to issue extra tokens for refreshing, just refresh expired tokens as long as they are not "too expired" (ttl)
  • Need to allow users to be authenticated in many devices at the same time
  • Don't want to bloat the JWT payload with another lengthy CSRF-TOKEN string, use the JWT itself (thus splitting the token) in a double cookie submission fashion (CSRF and XSS defenses).
  • To be able to invalidate compromised tokens.
  • JWT management (encoding/decoding/validation) is leveraged to a well-tested third party library

Login strategy

Upon user successfully logged in (provided email and password), a JWT is created for that user and split into two different cookies in order to mitigate CSRF attacks (no server): The token's header and payload goes to a secure flagged cookie (so I can access its claims from JS) that expires after jwt.ttl (more on this claim below), while the token's signature goes to a secure/http-only cookie (thus mitigating XSS attacks) that should be used only to set the Authorization header.

Server side

  1. User signs in successfully with email and password and a JWT is generated
  2. The token's split in two: {header}.{payload} / {signature}
  3. The first part (header and payload) goes into a secure cookie with expiration depending on jwt.ttl (from now on, let's refer to it as the "public cookie", which is not used by the server)
  4. The second part (signature) goes into a secure / httpOnly cookie

JS client (SPA)

Once the app is loaded, the authentication service looks for the public cookie, if found then it sets the Authorization header with its value (first part of JWT) for the forthcoming API requests. Then a request is performed against some endpoint like /auth/load in order to check if the JWT is still valid, renew it (more on this below) and bring some user details.

Authentication check and JWT validation

First layer of middlewares

Following some OWASP recommendations:

  1. Verify the origin header to be present and match the referer (the implementation is actually more complex) (CSRF defense middleware)
  2. Check X-Requested-With header to be XMLHttpRequest (CSRF defense middleware)
  3. Check the origin is an allowed origin (CORS Middleware)

Authentication middleware

Now the server expects the request to contain a Authorization: Bearer {header}.{payload} header, along with the httpOnly cookie carrying the JWT's signature, which appended to said header, should form the whole JWT.

Quick example:

JWT = req.headers.Authorization.split('Bearer ')[1].concat('.' + req.cookies.jwt_signature)

This way I refrain from creating an extra token for CSRF protection and use something like a "re-built" version of the jwt. (Idea taken from this post).

Token validation and refresh logic

Each token (signature) validation is handled by third-party library. Before going into further details, let me explain how I set some JWT claims used for authentication purposes:

  • uid: user public_id in persistent storage
  • iat: token's issued_at timestamp
  • exp: short-lived tokens, they expire 30 minutes after iat
  • ttl: token's time-to-live claim (timestamp), its the maximum time window since iat a token's allowed to be refreshed ( default 5 days). A JWT_TTL env. variable / server config. can be used instead.
  • jti: token's UUID for blacklisting purposes (I'm considering using a combination of {jwt.uid}_{jwt.iat} instead, because of shorter in-memory cache key and jwt length)

Token refreshing process

  1. If the token has expired (JWTExpiredException), check for ttl to decide whether we can proceed or not.
  2. ttl is still valid, but now check for some user.updated_at (or something like that) value in database (which is updated after each password/email update) and compare it against jwt.iat. Example: user.updated_at > jwt.iat, if that condition is true, then all of that user's tokens are not valid anymore. Useful for cases where a token could be compromised and the user was asked to update its password, or a new role/scope/permission has been assigned to that user and needs to be updated in the JWT payload claims.
  3. Check for the token's jti to be missing from the blacklist (more on this below), if token is not blacklisted, proceed...
  4. Finally, if this step is reached, then token can be refreshed on-the-fly (hence, setting new cookies after request has been processed). If not (a previous step failed), manual re-authentication is needed. At this point, you may want to invalidate the expired token (hence, make it fall in point #3) by pushing its jti to the blacklist.

Token renewing

Every time the application client starts, it may hit something like an /auth/load, /auth/me or even /auth/refresh api endpoint to get some of the current user details, in that moment, the JWT is always refreshed.

I prefer this instead of setting a new token on each authenticated request.
Some security challenges can be implemented at this moment, in order to check whether the user-agent / ip-range is suspicious or not, for account compromised flagging (i.e: send warning / confirmation e-mails and forcing the user to set up a new password).

Compromised JWTs and black-list service

In order to invalidate tokens, an in-memory cache layer (i.e: redis) can be used as a JWT blacklist (I know it breaks the RESTful philosophy, but the stored documents are really short-lived, as they are blacklisted for as long as their remaining ttl is left -if the token is provided, if not, then JWT_TTL constant is used-).

There are some cases when you should need to blacklist a single token:

  • Token was successfully refreshed, thus previous one invalidated
  • User deliberately logged out (also remove the cookie)

For cases when I need to invalidate all tokens for a specific user, I simply update its updated_at field in DB automatically, or better yet, block the account (user.state) and ask him via mail to set a new password. It is good to provide an option such "log me out from all devices" for stolen devices scenarios.

Questions

  1. Does the refreshing approach looks secure enough regarding to possible identity theft?
  2. Does all this middleware logic "computing work" adds enough overhead (and complexity) to consider a statefull approach instead? i.e.: server sessions.
  3. Since I'm "rebuilding" the JWT in the server, wouldn't it be good to "hardcode" (yet still configurable) the {header} part of the token instead of sending it back to the client, thus not exposing the algorithm used?

I argument my approach based on these posts:

Frondor
  • 111
  • 1
  • 5

1 Answers1

1

Lots here. Responses to the questions:

  1. Does the refreshing approach looks secure enough regarding to possible identity theft?

    Maybe, but it seems complicated and fragile. Three of the items that seem overcomplicated-

    Split-cookie- I'm not sure what the split-cookie approach adds over always just using XHR generating an Authorization header (which it sounds like these scheme requires). Keeping the signature in an httponly cookie can prevent exfiltration of the full JWT, but it doesn't prevent rogue injected client code from issuing requests to the server that are indistinguishable from legitimate requests, and/or communicating with its own C&C. The key there is to ensure that there is no way for rogue javascript to make it into the browser, and for any communication that could occur to be blocked (e.g. Content Security Policy).

    Blacklisting- if there is going to be a dependency on a user.updated_at check against a database on every request- sounds like there is- just updating the updated_at on any kind of change (like a token refresh) removes a lot of the token blacklisting logic. An updated_at check is expensive, but if you're going to do it, leverage it to keep the rest simpler.

    Header checks- validating Origin and X-Requested-With and so forth is a heuristic, nothing more, with lots of false positives/false negatives. Handling of those has to be weighed against user ergonomics. Enforcing a rule like- every API request has to be XHR issued- can be done by requiring the use of a custom XHR library, scanning code at build/CI time, etc.

  2. Does all this middleware logic "computing work" adds enough overhead (and complexity) to consider a stateful approach instead? i.e.: server sessions.

    The documented scheme does have what seems to be some over-complicated bits. But I don't know that the tradeoff is using sessions. That complexity is not removed by using sessions. Sessions state/storage solves a different problem- managing a user's work in progress in whatever the application is. Use of a server-side session doesn't automatically solve for authorization concerns. And in an XHR app, when a lot of application logic is on the client, there almost has to be some way for the client to have access to data that ordinarily would only live server-side in a session.

  3. Since I'm "rebuilding" the JWT in the server, wouldn't it be good to "hardcode" (yet still configurable) the {header} part of the token instead of sending it back to the client, thus not exposing the algorithm used?

    Hardcoding the algorithm in one way or another is probably a good idea.

Hope that all helps.

Jonah Benton
  • 3,359
  • 12
  • 20
  • Thanks! This helps indeed! Just some clarifications: "if there is going to be a dependency on a `user.updated_at` check against a database on every request..." There is not actually, only when a token has expired (which happens quite often, since they are meant to be short-lived). Split-cookie: the httpOnly is the only one used by the server, the other one is used by the client to set the `Authorization` header, so if that turns into a valid token once both processed by the server, then CSRF has been mitigated (there's no way the header would have been set). – Frondor Feb 11 '18 at 16:06
  • I'll wait a few more days before marking one answer as the most helpful one ;) – Frondor Feb 11 '18 at 16:07
  • re: updated_at- sure, it just seemed like the blacklisting piece could go away through the use of updated_at. So perhaps there is a simplicity/performance trade off there. re: split_cookie- right, the point was that splitting the cookie doesn't really help. It can prevent exfiltration by rogue js, but cookie splitting does nothing to prevent direct calls by rogue js (since the sig still goes to the server anyway). And always using XHR, setting an Authorization header (with signature) can be sufficient for CSRF. – Jonah Benton Feb 11 '18 at 16:16
  • Actually, the blacklist being handled by a redis server is much cheaper than checking the user record in db, on every JWT refresh (not every request). Splitting the cookie doesn't really help, but it allows you to use it as a CSRF protection mechanism so you don't need to generate tokens for that as well. And finally, if you got a rogue script doing requests on the user's behalf can only be mitigated by a proper CSP as you said. P.S: the signature doesn't go withing Authorization header, those are header and payload sections of the token. – Frondor Feb 12 '18 at 00:33