DEV Community

Cover image for How I Fixed JWT Security Flaws in 3 Steps
Renato Byrro for Dashbird

Posted on

How I Fixed JWT Security Flaws in 3 Steps

There are just too many ways to do JWT wrong. 😒

And I fell for some... Don't panic, but it's likely to be your case as well.

Check these 3 commonly overlooked security areas on JWT implementations. It will take only a few minutes.


1) Broken libraries

There are +1,600 libraries matching "jwt" on npm. 😳

npm jwt libraries

And +300 on Pypi. 😲

Alt Text

Do we need them all? Certainly not. Are they all secure? I won't trust. πŸ˜–

There are several ways your JWT library of choice might be compromised.

Can we cut to a simple solution?

Yes, I am also bored with security blah, blah, blah. πŸ’€

Go to this resource and double-check which libraries follow practices proven to be safe. Most will by now. But better safe than sorry.


2) Unsafe token generation and distribution

The joyfull implementation: πŸ€

a. Frontend requests user authentication

b. Backend authenticates and generates JWT

c. JWT is sent in the response body payload

d. Frontend store JWT in the localStorage

Ah, yes... The world would be beautiful without bad guys and if ugly things could not happen. πŸ˜‡

Cupcakes

Well. Back to the real world. 😎

Avoid following the above outline.

To help with items (a) & (b), make sure you selected a JWT library that follows best practices. Or that you implemented correctly on your own. Not that difficult really. Just care enough.

It's also good practice to log every authentication attempt (success and failures) and all contextual data you may possibly have.

JWT is frequently used in Serverless environments (because both are stateless, niiice!).

If that's your case, make sure you have professionals monitoring your logs and alerting you proactively. If that's not your case, the advice still holds. πŸ˜‰

To address items (c) & (d):

Do not send JWT in the response body payload

Do not store JWT in localStorage

Problem is: any JavaScript code in your frontend is able to access the JWT. And do whatever it wants.

And that's dangerous.

scorpion

Imagine what can happen if someone manages to inject malicious code in your frontend... and get all your users' JWTs?... Hum... Houston...

No. Instead, the backend should set the JWT as a cookie in the user browser. Make sure you flag it as Secure and httpOnly cookie. And SameSite cookie. Boy, that's a multi-flavor cookie.

This way, the JWT will be available to the backend in all subsequent requests, while remaining outside the reach of potentially dirty JS hands.

In your response body payload, send only what's necessary for the frontend to provide the features expected by the user. Did I mention to not include anything sensitive here? Should not.

I know. A cookie is not as cool as localStorage. But, look, they can be colorful! And SAFE. He's our friend. Let's treat him well. Deal? πŸ™Œ πŸͺ

Cookies/Macarrons


3) Not handling secret keys securely

Any JWT implementation will involve some sort of secret data. Regardless of using a symmetric (one secret key) or an asymmetric (public/private keys) way to sign tokens.

Personally, I prefer symmetric implementations with HMAC. Simplifies things. But sometimes I use asymmetric RSA. Lately, I have been using the latter only. Well, they'll never know which one I really use. 😜

No one should ever know how YOU implement JWT either. Not to mention your secret or private keys.

Things you should avoid doing with your secret/private key when possible:

  • πŸ’» Storing in a config file and committing to your Git repo
  • πŸ“£ Sharing with your team on your Drive, Dropbox, Slack, whatever
  • ♻️ Having the same keys for local, test, and production environments

Instead:

  • ✌️ Distribute keys for your development team to use only in local and testing environments
  • πŸ‘ Store production keys in a safe place, only available to the production app
  • πŸ” Keep the production keys away from prying eyes, load them as environment variables, on-demand, protected against unintended access

Further reading:


Full disclosure: I work as a Developer Advocate at Dashbird.


Image credits:

Top comments (41)

Collapse
 
paragoniescott profile image
Scott Arciszewski

The problems with JSON Web Tokens are that it's an error-prone cryptographic design.

Most of (if not all of) the work you had to do to get JWT security flaws fixed should never have been necessary in the first place. Furthermore, there may still be as-of-yet undiscovered flaws in the JOSE standards since they do a lot of things that maximize the risk of implementation error.

You're better off implementing something less foot-bullety. I wrote PASETO to replace JWT. Maybe a good starting point?

Collapse
 
byrro profile image
Renato Byrro

Hey Scott, thanks for joining the discussion!

I very welcome this suggestion. Definitely am open to new implementations that would excel current ones, in any area. πŸ‘

Never heard of PASETO until now. I read very quickly, but I promise I'll give proper time to familiarize myself as soon as I can. Maybe we can contribute on DEV with article about it? Let me know how the idea sounds to you.

At first glance, what I captured is:

  1. Prevents publicizing which algo is used in the token generation
    • This indeed must never be public, hence my heads-up in the article
    • I agree that having an attribute for it in the JWT original specs was a design mistake from a security standpoint
  2. Enforces asymmetric keys on public environments

Did I miss any important top-level aspect of it?

A few considerations came to my mind in order to validate the true value brought by the PASETO implementation.

Not questioning the value at this point. Sort of a peer reviewing in search for practical confirmation.

My rule - I trust it's a fair one - is: always confirm by experience before accepting something as truth. πŸ˜‰

I'll get back to you when I find myself ready for a productive and intelligent discussion on the topic! πŸ‘

Collapse
 
paragoniescott profile image
Scott Arciszewski • Edited

You mostly got the idea right.

The design of PASETO was influenced by a simple observation about real world cryptanalysis.

Let's say you were tasked with building a brick wall. Would you...

  1. Place each brick carefully and meaningfully and then use mortar to join them together once, OR
  2. Create a three-dimensional lattice so bricks can be hot-swapped to better fit the designer's goals and operational requirements?

Which design would you trust to hold up a roof?

Option 2 is what the JOSE standards (JWT, JWE, JWS) do. Option 1 is what PASETO does. PASETO doesn't merely hide the algorithms, it forces developers to one of two modes that have security turned up to 11, based on their specific requirements.

PASETO's design was inspired by the least controversial meta-observation in cryptography today: Cryptography vulnerabilities more often lie in the metaphorical mortar rather than the bricks.

Randal Degges gave a talk about PASETO recently. The slides are available here.

Thread Thread
 
byrro profile image
Renato Byrro

Looks good, thanks for clarifying. Will research more into it.

Collapse
 
byrro profile image
Renato Byrro • Edited

Hey Scott, it's been a long while, but I finally managed to get a bit more familiar with PASETO. Thought I could pick your brain to understand a bit more about the feasibility of the concept.

Disclosure: although I have interest in the topics as a developer, at this moment I'm far from a security or cryptography expert.

I've read this thread on IETF discussing the feasibility of adopting PASETO as a replacement for JWTs. From what I could understand, at that moment (2018), the overwhelming majority of experts in that group didn't find it a good idea to adopt PASETO as a replacement.

They seemed to value a few ideas from PASETO that could be incorporated in a new, stronger version for the JWT specification, while other parts of the PASETO specs did raise security concerns.

What is the current thought among the community experts with regards to this? Is PASETO at the point of adoption as an industry-standard?

Collapse
 
thet profile image
Johannes Raggam

Hi, I think you built your localstorage critique on a wrong claim:

"Problem is: any JavaScript code in your frontend is able to access the JWT. And do whatever it wants."

The localstorage is isolated from other domains and only accessbile to scripts from the same domain. Those can manipulate the localstorage, as they can manipulate cookies from the same domain.

Cookies on the other hand can be used for CSRF attacks if CORS headers are not set correctly (see: stackoverflow.com/a/37635977/1337474 ). They do have advantages though - you can set expiration/max-age times and they are sent also with links where you possibly cannot set Authorization headers like with file downloads.

Collapse
 
byrro profile image
Renato Byrro • Edited

Hi Johannes, those are very important concerns! I'm glad you raised them in this discussion!

You are absolutely right that other domains wouldn't be able to access your localStorage implementation. Nonetheless, this would be possible in case of an XSS attack to your site. This was the scenario I pointed out in the article:

"Imagine what can happen if someone manages to inject malicious code in your frontend... and get all your users' JWTs?"

CSRF is indeed a real threat to cookies. That's why I suggested setting them with the sameSite property:

"Make sure you flag it as Secure and httpOnly cookie. And SameSite cookie".

This protects your JWT against some CSRF attack vectors, but not your entire implementation. There are additional measures you should take, such as:

  • Implement a CSRF token
  • Avoid using the GET method for state-changing requests
Collapse
 
kirankumbhar profile image
Kiran Kumbhar

Keeping JWT token in localStorage is fine. The only concern is XSS which should be avoided at all cost.
Once your site is vulnerable to XSS you got more bigger problem rather than just stealing JWT token.
So store the JWT token in localStorage and make sure your website is battle tested against XSS.

Thread Thread
 
byrro profile image
Renato Byrro • Edited

It can be "ok" and acceptable in some cases, but definitely not the best practice from a security standpoint.

A good analogy here would be our house. We need to secure doors and windows against unauthorized access. If a malicious actor gets in, we've got big problems, yes. But that doesn't mean we shouldn't hide our valuables. We may still store jewelry, money and other values in a safe. That practice can mitigate the losses in case someone breaks in the house.

Collapse
 
ryansmith profile image
Ryan Smith

Awesome post, I have been looking into authentication like this and this answered most of the pending questions I had around how to keep it secure. Some other resources that I have looked at were vague on this.

I had a question on this:

No. Instead, the backend should set the JWT as a cookie in the user browser.

Would it just use the Set-Cookie Response Header or is there a different method?

Thanks!

Collapse
 
byrro profile image
Renato Byrro

Hey, Ryan! Thank you for the kind words, I'm glad to have added some value!

That's the HTTP standard to set the cookie on the response request. It's likely that your backend has a wrapper for this - as in nodejs. This way we don't have to worry about HTTP standards, use a more friendly API instead.

Some suggest sending the JWT in the Authorization header:

Authorization: Bearer <token>

This helps to prevent CSRF attacks but is exposed to frontend JS. Using the SameSite in your cookie will help against CSRF anyway.

In case you're worried, use both. You can have your main JWT set as a cookie, and a second JWT set in the Authorization header (may even use a different secret). The second one doesn't even have to contain the same info, perhaps only the user ID.

Then your backend can decode and validate both on each request. Doesn't add too much overhead and comes with an extra security layer. πŸ˜‰

Collapse
 
ryansmith profile image
Ryan Smith

That makes sense, thank you!

Collapse
 
kvsm profile image
Kevin Smith 🏴󠁧󠁒󠁳󠁣󠁴󠁿

What about a microservice architecture, where one service may handle authentication and issuing tokens, but those tokens can then be used to authenticate to other services? If you use a cookie to store the token in this case, you can't rely on the browser forwarding it to other services due to privacy settings. πŸ€”

Collapse
 
byrro profile image
Renato Byrro • Edited

That's a good question!

What I would do is:

  • The public endpoint extracts the JWT from the cookie (sent with the HTTP request)
  • Process the JWT to extract the User object
  • When invoking other microservices internally, pass the User object with the requests
  • If I'm implementing an event-driven architecture, I would maybe include only the User ID (to avoid bloating the messages too much), and each microservice can retrieve the entire User object from a database storage

That's actually the same thing if the JWT was coming as a header or in the request body payload. The public endpoint would need to extract and pass around to internal services anyway...

Does that make sense? πŸ˜‰

Collapse
 
kvsm profile image
Kevin Smith 🏴󠁧󠁒󠁳󠁣󠁴󠁿

So you're suggesting to have a single public service which proxies requests to other backend services? This is actually how we have just implemented this at my work. πŸ™‚

However, my question was how you would approach it if you had multiple services your frontend could communicate with directly. One of the advantages of JWT is that any service with access to the signing secret can verify the token and trust the claims within. So, in theory, if an auth service issues the token to the frontend, then another service can receive and decode the token without involving the auth service again. But how can we secure the token on the frontend in this case?

Thread Thread
 
byrro profile image
Renato Byrro • Edited

In that case, I can see two options:

  1. Each public endpoint has the secret and can decode the JWT by itself
  2. A single internal service is dedicated to decoding the JWT, which would serve multiple public endpoints whenever they need
Implementation Pros Cons
Distributed secret Reduced complexity and latency Secret is more exposed, repeated functionality across different services reduces maintainabilityΒΉ
Dedicated JWT decoder Reduces attack surface, more maintainable May increase services coupling, increased latency

ΒΉ Some architectures have resources to mitigate that. The JWT decoding could be implemented as a Layer, if you're using AWS Lambda, for example.

Side comment: I would be very interested in reading more about the implementation you and your team are using! Do you have any plans to write about it? I see pros and cons about proxying requests from a single endpoint.

Collapse
 
ale_jacques profile image
Alexandre Jacques

Hi! Nice article!

Regarding #2, you're assuming that it's a browser on the client side (so that you can use a cookie). In the case of a mobile app, what would be a secure alternative not to have the token on the response body? Use a HTTP response header?

Regards!

Collapse
 
byrro profile image
Renato Byrro • Edited

Hey Alexandre, thanks for stopping by!

That's correct, the advice assumes the client is a web browser.

I don't have experience with native mobile apps at all. But I found potentially useful references:

Again. Not my area of expertise. These are just what seemed to be relevant to me from a quick web search. Use with caution! πŸ˜‰

(saudaçáes de Minas, Brasil! πŸ˜„)

Collapse
 
sandrinodimattia profile image
Sandrino Di Mattia

No. Instead, the backend should set the JWT as a cookie in the user browser. Make sure you flag it as Secure and httpOnly cookie. And SameSite cookie. Boy, that's a multi-flavor cookie. This way, the JWT will be available to the backend in all subsequent requests, while remaining outside the reach of potentially dirty JS hands.

This type of advice will sadly not help the reader. Here's why:

  • If the reader is building a SPA hosted on a static site (S3, Netlify, ...) there is no way to set a cookie. The only option is to store the token in memory, which still has a risk for XSS attacks.
  • If the reader has a backend which sets cookies, then then you can skip JWTs all together. Just create a session. No need to reinvent the wheel. Sessions are a solved problem and have battle tested tooling across every platform. JWTs provide zero value here. If your backend then needs to call APIs it could store any token it needs in the session and extract it from there.
Collapse
 
byrro profile image
Renato Byrro

Hi Sandrino, thanks for taking the time to contribute to the discussion!

The whole point of the discussion is when we have a backend securely authenticating a user. JWT won't apply to Frontend-only scenarios.

A large portion of web apps nowadays have a backend, so I wouldn't agree that the discussion here is useless...

About your second topic, it's a discussion beyond the purpose of my article. It's fair for you to prefer using Session IDs, but many people have a different view.

On whether JWT is a good authentication solution or not. I'm going to need to differ on this statement:

"JWTs provide zero value"

First, sessions are just cookies, they're no different. Storing a Session ID or a JWT as a cookie in the browser is essentially the same thing.

The advantage of a JWT is that, once the backend receives it in the request header as a cookie, it doesn't need to go anyplace get more info. It's self-contained.

This is a property called stateless and it plays very well in distributed environments designed for variable demand and smoother scalability.

Whereas with a Session ID, the backend still needs to ask a storage engine to authenticate who is the owner of that session. It's a design that is more difficult to scale.

Collapse
 
sandrinodimattia profile image
Sandrino Di Mattia

The whole point of the discussion is when we have a backend securely authenticating a user. JWT won't apply to Frontend-only scenarios.

I'm not referring to frontend only scenarios, there is indeed no such thing as frontend/client-side authentication. What I'm referring to is modern applications where the frontend is hosted on a static file server and the backend is an API often running on a different (sub)domain.

An application could be running on AWS S3 on acme.com which then needs to call an API (eg: Heroku) an api.acme.com. In that case a JWT can be used to serve as a bearer token, but the site running on acme.com has no backend which can set cookies. A backend and an API can be the same or different concepts.

About your second topic, it's a discussion beyond the purpose of my article. It's fair for you to prefer using Session IDs, but many people have a different view.

The advantage of a JWT is that, once the backend receives it in the request header as a cookie, it doesn't need to go anyplace get more info. It's self-contained.

This is a property called stateless and it plays very well in distributed environments designed for variable demand and smoother scalability.

I'm not talking about Session IDs, I'm talking about sessions. Session use cookies and they can contain a session ID where a backend data store is used or they can be self contained, where the data is stored in a cookie and then encrypted. express-sessions is a good example of this.

Sessions in every platform already have all the required tools to make them work for many different use cases:

  • Session lifetime (absolute and sliding expiration)
  • Destroying a session
  • Session stores (cookie - self contained, Redis, MongoDB, ...)

First, sessions are just cookies, they're no different. Storing a Session ID or a JWT as a cookie in the browser is essentially the same thing.

It's not:

  • JWTs are signed and not encrypted. Anyone can inspect the data by decoding the token. You would need JWE to support this. Sessions are often encrypted if the data is persisted in a cookie.
  • JWTs don't have support for sliding expiration, they only have the notion of absolute expiration. You would need to reissue new tokens each time someone is active if you want support for sliding expiration. Sessions have this out of the box.
  • Browsers like Safari have cookie size limitations on the domain level. A JWT often provides lots of overhead and can take up a significant amount of data (1KB is big if you need to consider Safari limitations). With a sessions this can be addressed with a backing data store.
Thread Thread
 
byrro profile image
Renato Byrro • Edited

We're getting way way beyond the purpose of my article, but let's cover it quickly.

I see there are maybe some conceptual confusions.

When api.domain.com sets a cookie, it doesn't need to be available for domain.com. When domain.com fires a subsequent request to API, the cookie will be sent along with it. It's the API that needs the JWT, not domain.com. This can work if you set the API to accept CORS.

What I mean is that a session uses a cookie the same way its proposed for JWT to use it. Obviously they are different implementations.

The problem with sessions is not encryption, but what can be extracted from them. JWT can store minimal user object, so that API doesn't necessarily have to reach a database for that.

Collapse
 
kazepis profile image
Dr. Nikolaos Kazepis

Great read! I have a question for you. Since storing credentials in the source code or cofig files is a big NO for all sorts of reasons, what do YOU do in cases where there is a SPA application that needs to authenticate with the backend (via JWTs) without any user interaction? I mean, lets say that we want only this app to be able to access specific back end resources, hence we must somehow provide the sourcecode of this app with some credentials to communicate to the back end to gain access (retrieve a JWT for future calls). For example, you have an application that accesses a backed web api and you want only this application to be able to access that particular API.

Collapse
 
byrro profile image
Renato Byrro

Hi Nikolaos, a critical topic you raised! This could easily render a dedicated article. But, as a quick overview:

It really depends on your infrastructure, environment and required security level.

Loading secrets on demand from an external, secure place, is recommended. Once it's loaded, the app can share it internally as an environment variable or keep it short-lived in a contextual variable to avoid leakage inside the app.

Once a secret is in the machine RAM and unencrypted, it's already exposed, though. Depending on how much security you need, it might be worth having secrets for different application areas or endpoints. If one secret is compromised, the damaged area is reduced.

Observe that the article indicates things we should avoid. In some cases (sole-projects, proofs-of-concept or quick MVPs, etc), a sub-optimal approach might be acceptable.

There are basically two routes for implementation:

1. Managed services

This is my preferred choice, when financially feasible.

AWS, for instance, provides Secrets Manager:

"... enables you to easily rotate, manage, and retrieve database credentials, API keys, and other secrets throughout their lifecycle. Users and applications retrieve secrets with a call to Secrets Manager APIs, eliminating the need to hardcode sensitive information in plain text"

It's a great service. There are two downsides:

  • Cost: $0.05 per 10,000 requests (depending on your traffic and profitability, it's unfeasible)
  • Scale limitation: up to 700 RPS by default; I believe you can ask for a limit increase, though

Similarly, Azure has Key Vault and GCP has Key Management Service.

2. In-house secrets management

This will be tricky and I don't have enough expertise to advise on the best implementation from a security standpoint.

Nevertheless, one solution is using AWS Lambda to isolate my secrets and serve other parts of the application stack. A plugin for the Serverless framework makes it a bit easier.

Instead of calling a service like AWS Secrets Manager, I'd call my own AWS Lambda function, which will provide the secrets on-demand. It will be 10x cheaper: $0.004 per 10,000 requests.

Using IAM to control who has access to the secrets Lambda will provide reasonable level of security.

If I would work in a team using this approach, I'd completely isolate the Secrets Lambda from the rest of my stack (repo, infra-as-code, CI/CD workflow, everything). This would contribute to preventing leakage of secrets used in production.

You can get way fancier with this implementation, but it's already getting too lengthy... Hope this helps shed some light. πŸ’‘ πŸ˜‰

Collapse
 
byrro profile image
Renato Byrro

I believe this has already been discussed in the comments.

I'm not here to convince you of anything or prove anyone wrong. It's obvious that anyone can store anything in localstorage. If you're confident that storing sensitive credentials in it is perfectly fine, then it's not my responsibility to prove anything wrong or right... Just go for it.

Collapse
 
bitmilanpavic profile image
bitmilanpavic

Awesome post, thanks for sharing!

Question if i may:
I am working on legacy project that has sessions already in use. I have to add authorization part and i decided to use JWT, to avoid making extra request to session store.

Is it a good idea to use sessionId as secret when ever is needed to verify/sign JWT.
What you think about this idea, just to use sessionId as secret which is always unique for each user. In theory, this should make things more secure?

Collapse
 
byrro profile image
Renato Byrro

Hi, glad the article was helpful!

Two of the main advantages of JWT are:

  1. A signed hash to validate that the token was not modified
  2. A unique user identified is embedded in the JWT

How do you plan to implement #1 only with a session ID?

Unless your session ID is unique for each user and is permanent across time, you will still need to map each session ID to a real user in your database, defeating #2.

Collapse
 
danikademers profile image
DanikaDemers

how to add layers of security, which will protect the integrity and, optionally, the content of the message? Vasiyam Specialist in Kerala