Best practices for server-side handling of JWT tok

2019-01-12 15:23发布

问题:

(spawned from this thread since this is really a question of its own and not specific to NodeJS etc)

I'm implementing a REST API server with authentication, and I have successfully implemented JWT token handling so that a user can login through a /login endpoint with username/password, upon which a JWT token is generated from a server secret and returned to the client. The token is then passed from the client to the server in each authenticated API request, upon which the server secret is used to verify the token.

However, I am trying to understand the best practices for exactly how and to what extent the token should be validated, to make a truly secure system. Exactly what should be involved in "validating" the token? Is it enough that the signature can be verified using the server-secret, or should I also cross-check the token and/or token payload against some data stored in the server?

A token based authentication system will only be as safe as passing username/password in each request provided that it's equally or more difficult to obtain a token than to obtain a user's password. However, in the examples I've seen, the only information required to produce a token is the username and the server-side secret. Doesn't this mean that assuming for a minute that a malicious user gains knowledge of the server secret, he can now produce tokens on behalf of any user, thereby having access not only to one given user as would be the fact if a password was obtained, but in fact to all user accounts?

This brings me to the questions:

1) Should JWT token validation be limited to verifying the signature of the token itself, relying on the integrity of the server secret alone, or accompanied by a separate validation mechanism?

  • In some cases I've seen the combined use of tokens and server sessions where upon successful login through the /login endpoint a session is established. API requests validate the token, and also compare the decoded data found in the token with some data stored in the session. However, using sessions means using cookies, and in some sense it defeats the purpose of using a token based approach. It also may cause problems for certain clients.

  • One could imagine the server keeping all tokens currently in use in a memcache or similar, to ensure that even if the server secret is compromised so that an attacker may produce "valid" tokens, only the exact tokens that were generated through the /login endpoint would be accepted. Is this reasonable or just redundant/overkill?

2) If JWT signature verification is the only means of validating tokens, meaning the integrity of the server secret is the breaking point, how should server secrets be managed? Read from an environment variable and created (randomized?) once per deployed stack? Re-newed or rotated periodically (and if so, how to handle existing valid tokens that were created before rotation but needs to be validated after rotation, perhaps it's enough if the server holds on to the current and the previous secret at any given time)? Something else?

Maybe I'm simply being overly paranoid when it comes to the risk of the server secret being compromised, which is of course a more general problem that needs to be addressed in all cryptographic situations...

回答1:

I've been playing with tokens for my application as well. While I'm not an expert by any means, I can share some of my experiences and thoughts on the matter.

The point of JWTs is essentially integrity. It provides a mechanism for your server verify that the token that was provided to it is genuine and was supplied by your server. The signature generated via your secret is what provides for this. So, yes, if your secret is leaked somehow, that individual can generate tokens that your server would think are its own. A token based system would still be more secure than your username/password system simply because of the signature verification. And in this case, if someone has your secret anyway, your system has other security issues to deal with than someone making fake tokens (and even then, just changing the secret ensures that any tokens made with the old secret are now invalid).

As for payload, the signature will only tell you that the token provided to you was exactly as it was when your server sent it out. verifying the that the payloads contents are valid or appropriate for your application is obviously up to you.

For your questions:

1.) In my limited experience, it's definitely better to verify your tokens with a second system. Simply validating the signature just means that the token was generated with your secret. Storing any created tokens in some sort of DB (redis, memcache/sql/mongo, or some other storage) is a fantastic way of assuring that you only accept tokens that your server has created. In this scenario, even if your secret is leaked, it won't matter too much as any generated tokens won't be valid anyway. This is the approach I'm taking with my system - all generated tokens are stored in a DB (redis) and on each request, I verify that the token is in my DB before I accept it. This way tokens can be revoked for any reason, such as tokens that were released into the wild somehow, user logout, password changes, secret changes, etc.

2.) This is something I don't have much experience in and is something I'm still actively researching as I'm not a security professional. If you find any resources, feel free to post them here! Currently, I'm just using a private key that I load from disk, but obviously that is far from the best or most secure solution.



回答2:

Here are some things to consider when implementing JWT's in your application:

  • Keep your JWT lifetime relatively short, and have it's lifetime managed at the server. If you don't, and later on need to require more information in your JWTs, you'll have to either support 2 versions, or wait until your older JWTs have expired before you can implement your change. You can easily manage it on the server if you only look at the iat field in the jwt, and ignore the exp field.

  • Consider including the url of the request in your JWT. For example, if you want your JWT to be used at endpoint /my/test/path, include a field like 'url':'/my/test/path' in your JWT, to ensure it's only ever used at this path. If you don't, you may find that people start using your JWTs at other endpoints, even ones they weren't created for. You could also consider including an md5(url) instead, as having a big url in the JWT will end up making the JWT that much bigger, and they can get quite big.

  • JWT expiry should be configurable by each use case if JWTs are being implemented in an API. For example, if you have 10 endpoints for 10 different use cases for JWT's, make sure you can make each endpoint accept JWTs that expire at different times. This allows you to lock down some endpoints more than others, if for example, the data served by one endpoint is very sensitive.

  • Instead of simply expiring JWTs after a certain time, consider implementing JWTs that support both:

    • N usages - can only be used N times before they expire and
    • expire after certain amount of time (if you have a one use only token, you don't want it living forever if not used, do you?)
  • All JWT authentication failures should generate an "error" response header that states why the JWT authentication failed. e.g. "expired", "no usages left", "revoked", etc. This helps implementers know why their JWT is failing.

  • Consider ignoring the "header" of your JWTs as they leak information and give a measure of control to hackers. This is mostly concerning the alg field in the header - ignore this and just assume that the header is what you want to support, as this avoids hackers trying to use the None algorithm, which removes the signature security check.

  • JWT's should include an identifier detailing which app generated the token. For example if your JWT's are being created by 2 different clients, mychat, and myclassifiedsapp, then each should include it's project name or something similar in the "iss" field in the JWT e.g. "iss":"mychat"

  • JWT's should not be logged in log files. The contents of a JWT can be logged, but not the JWT itself. This ensures devs or others can't grab JWT's from log files and do things to other users accounts.
  • Ensure your JWT implementation doesn't allow the "None" algorithm, to avoid hackers creating tokens without signing them. This class of errors can be avoided entirely by ignoring the "header" of your JWT.
  • Strongly consider using iat (issued at) instead of exp (expiry) in your JWTs. Why? Since iat basically means when was the JWT created, this allows you to adjust on the server when the JWT expires, based on the creation date. If someone passes in an exp that's 20 years in the future, the JWT basically lives forever! Note that you automatically expire JWTs if their iat is in the future, but allow for a little bit of wiggle room (e.g 10 seconds), in case the client's time is slightly out of sync with the servers time.
  • Consider implementing an endpoint for creating JWTs from a json payload, and force all your implementing clients to use this endpoint to create their JWTs. This ensures that you can address any security issues you want with how JWTs are created in one place, easily. We didn't do this straight off in our app, and now have to slowly trickle out JWT server side security updates because our 5 different clients need time to implement. Also, make your create endpoint accept an array of json payloads for JWTs to create, and this will decrease the # of http requests coming in to this endpoint for your clients.
  • If your JWT's will be used at endpoints that also support use by session, ensure you don't put anything in your JWT that's required to satisfy the request. You can easily do this if you ensure your endpoint works with a session, when no JWT is supplied.
  • So JWT's generally speaking end up containing a userId or groupId of some sort, and allow access to part of your system based on this information. Make sure you're not allowing users in one area of your app to impersonate other users, especially if this provides access to sensitive data. Why? Well even if your JWT generation process is only accessible to "internal" services, devs or other internal teams could generate JWTs to access data for any user, e.g. the CEO of some random client's company. For example, if your app provides access to financial records for clients, then by generating a JWT, a dev could grab the financial records of any company at all! And if hacker gets into your internal network in anyway, they could do the same.
  • If you are are going to allow any url that contains a JWT to be cached in any way, ensure that the permissions for different users are included in the url, and not the JWT. Why? Because users may end up getting data they shouldn't. For example, say a super user logs into your app, and requests the following url: /mysite/userInfo?jwt=XXX, and that this url gets cached. They logout and a couple of minutes later, a regular user logs into your app. They'll get the cached content - with info about a super user! This tends to happen less on the client, and more on the server, especially in cases where you're using a CDN like Akamai, and you're letting some files live longer. This can be fixed by including the relevant user info in the url, and validating this on the server, even for cached requests, for example /mysite/userInfo?id=52&jwt=XXX


回答3:

I don't think I'm an expert but I'd like to share some thoughs about Jwt.

  • 1: As Akshay said, it's better to have a second system to validate your token.

    a.: The way I handle it : I store the hash generated into a session storage with the expiricy time. To validate a token, it needs to have been issued by the server.

    b.:There is at least one thing that must be checked the signature method used. eg :

    header :
    {
      "alg": "none",
      "typ": "JWT"
    }
    

Some libraries validating JWT would accept this one without checking the hash. That means that without knowing your salt used to sign the token, a hacker could grant himself some rights. Always make sure this can't happen. https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/

c.: Using a cookie with a session Id would not be useful to validate your token. If someone wants to hijack the session of a lambda user, he would just have to use a sniffer (eg : wireshark). This hacker would have both information at the same time.

  • 2: It is the same for every secret. There is always a way to know it.

The way I handle it is linked to the point 1.a. : I have a secret mixed with a random variable. The secret is unique for every token.

However, I am trying to understand the best practices for exactly how and to what extent the token should be validated, to make a truly secure system.

If you want the best security possible, you should not blindly follow best practices. The best way is to understand what you're doing (I think it's ok when I see your question), and then evaluate the security you need. And if the Mossad want to have access to your confidential data, they 'll always find a way. (I like this blog post : https://www.schneier.com/blog/archives/2015/08/mickens_on_secu.html )



回答4:

Lots of good answers here. I'll integrate some of the answers I think are most relevant and add some more suggestions.

1) Should JWT token validation be limited to verifying the signature of the token itself, relying on the integrity of the server secret alone, or accompanied by a separate validation mechanism?

No, because of reasons unrelated to the compromise of a token secret. Each time a user logs in via a username and password, the authorization server should store either the token that was generated, or metadata about the token that was generated. Think of this metadata as an authorization record. A given user and application pair should only have one valid token, or authorization, at any given time. Useful metadata is the user id associated with the access token, the app id, and the time when the access token was issued (which allows for the revocation of existing access tokens and the issuing of a new access token). On every API request, validate that the token contains the proper metadata. You need to persist information about when each access tokens was issued, so that a user can revoke existing access tokens if their account credentials are compromised, and log in again and start using a new access token. That will update the database with the time when the access token was issued (the authorization time created). On every API request, check that the issue time of the access token is after the authorization time created.

Other security measures included not logging JWTs and requiring a secure signing algorithm like SHA256.

2) If JWT signature verification is the only means of validating tokens, meaning the integrity of the server secret is the breaking point, how should server secrets be managed?

The compromise of server secrets would allow an attacker to issue access tokens for any user, and storing access token data in step 1 would not necessarily prevent the server from accepting those access tokens. For example, say that a user has been issued an access token, and then later on, an attacker generates an access token for that user. The authorization time of the access token would be valid.

Like Akshay Dhalwala says, if your server-side secret is compromised, then you have bigger problems to deal with because that means that an attacker has compromised your internal network, your source code repository, or both.

However, a system to mitigate the damage of a compromised server secret and avoid storing secrets in source code involves token secret rotation using a coordination service like https://zookeeper.apache.org. Use a cron job to generate an app secret every few hours or so (however long your access tokens are valid for), and push the updated secret to Zookeeper. In each application server that needs to know the token secret, configure a ZK client that is updated whenever the ZK node value changes. Store a primary and a secondary secret, and each time the token secret is changed, set the new token secret to the primary and the old token secret to the secondary. That way, existing valid tokens will still be valid because they will be validated against the secondary secret. By the time the secondary secret is replaced with the old primary secret, all of the access tokens issued with the secondary secret would be expired anyways.