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
- User signs in successfully with email and password and a JWT is generated
- The token's split in two:
{header}.{payload}
/{signature}
- The first part (header and payload) goes into a
secure
cookie with expiration depending onjwt.ttl
(from now on, let's refer to it as the "public cookie", which is not used by the server) - 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:
- Verify the origin header to be present and match the referer (the implementation is actually more complex) (CSRF defense middleware)
- Check
X-Requested-With
header to beXMLHttpRequest
(CSRF defense middleware) - 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 storageiat
: token's issued_at timestampexp
: short-lived tokens, they expire 30 minutes afteriat
ttl
: token's time-to-live claim (timestamp), its the maximum time window sinceiat
a token's allowed to be refreshed ( default 5 days). AJWT_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
- If the token has expired (
JWTExpiredException
), check forttl
to decide whether we can proceed or not. ttl
is still valid, but now check for someuser.updated_at
(or something like that) value in database (which is updated after each password/email update) and compare it againstjwt.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.- Check for the token's
jti
to be missing from the blacklist (more on this below), if token is not blacklisted, proceed... - 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
- Does the refreshing approach looks secure enough regarding to possible identity theft?
- Does all this middleware logic "computing work" adds enough overhead (and complexity) to consider a statefull approach instead? i.e.: server sessions.
- 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: