Securing your JS apps w/ Stateless CSRF #krakenjs

Hey there! You might have stumbled upon this post because you’re interested in securing your JS apps, or maybe you’ve heard about the other things we have open sourced.

Today we’re releasing jwt-csrf, a stateless CSRF solution for securing your JavaScript apps!

It’s something we’ve built and battle tested over the last year while building PayPal Checkout. In addition to talking about jwt-csrf, I’d like to talk about our journey of re-architecting PayPal Checkout and share our learnings and discoveries.

If you’ve checked out with PayPal in the last year or so, woohoo! You know what we work on :) By the way, if any of this is interesting to you, we’re hiring! DM me at @mark_stuart

Why stateless?

About a year ago, we were stuck in a jam where we had way too much state on our server-side and caused us to take a step back and re-think many aspects of our architecture.

If you’re familiar with Express, you know about req.session, which is basically a global variable that is scoped to a user’s session and hangs off of the request object. This usually isn’t a problem for solo developers or really small development teams, but as soon as you add more developers to the mix, you will reach a point where most of your issues are around maintaining state.

“Shared mutable state is the root of all evil”. — Someone smart

Just a couple examples of our pain —

  • Slow experimentation Our client-side was unable to evolve without code changes in our server-side.
  • Brittle middleware — Our common middleware functions had to be stacked in a particular order, otherwise they wouldn’t work. Some of these middleware would depend on some values in req.session that were randomly set by other middleware. If you use Express, this one should ring home.

Okay, so global mutable state is bad. But, what’s the alternative?

We decided that we were going to go completely stateless. Rather than building “god” endpoints that do a lot of orchestration and rely on stateful flags in req.session, we built atomic APIs that do 1 thing, and 1 thing only. Ex: Get cart details, Add a credit card, etc.

Along with that, we had to make changes to the way we authorize users with CSRF. If you’re unfamiliar with CSRF, here’s a quick crash course.

What is CSRF?

CSRF (or “cross site request forgery”) ensures that requests made to your server-side are legitimate and originate from your app. That last part is key.

If a user is currently logged in with PayPal, they have session cookies dropped in their browser that are scoped for paypal.com. If that user then visits a site that has been compromised, the site can make requests to paypal.com on the user’s behalf (with the user’s cookies). This could be really dangerous… imagine an attacker adding their bank account to the user’s PayPal account, then transferring the user’s PayPal balance to the attacker’s account. Yikes!

Luckily, this attack is avoidable with CSRF protection.

The most common CSRF pattern is the synchronizer pattern, where a CSRF token is generated server-side, dumped on a page’s first render, and passed back to the server-side on subsequent requests (usually as a hidden form param or AJAX request header). When the token’s validated on subsequent requests, it’s validated against a secret key that hangs off of req.session.

By the way, there’s nothing wrong with this! We open sourced lusca under the krakenjs umbrella. It’s an excellent option for CSRF protection if you’re building an Express app with sessions enabled. But, we’re going stateless.

Okay, so now you’re thinking… how do you provide CSRF protection without a session? You said you went stateless! Enter jsonwebtoken (JWT).

jsonwebtoken

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.

Since there’s no state on the server-side to compare the token against, we’re going to store our state inside of the token.

Encrypted Sample JWT —

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

Decrypted Sample JWT —

{alg:'HS256',typ:'JWT'}.{time:timestamp,logged_in:true}.signature

When this token is decrypted, it contains a header, payload, and a signature. The most important being the payload and signature.

The payload contains the user’s claims that they are who they say they are. You can define whatever you want inside the payload. But it’s usually a good idea to add a timestamp, some uniquely identifiable information like a user’s account ID, and some indication of if they’re logged in or not. That way, when the token is validated, you can check if the token is expired, check if the user account ID matches, and they’re logged in (Ex: if they’re hitting an API that requires authentication).

The signature verifies that the sender is who they say they are and the message wasn’t tampered along the way. It’s constructed by digitally signing (via a secret) the header, payload, and secret.

If the signature is valid, the payload is legitimate.

Good news! jwt-csrf takes care of generating and verifying JWTs using various CSRF patterns. Here’s how that works.

jwt-csrf

jwt-csrf provides server-side and client-side (optional) code to do all of the heavy lifting of generating/validating JWTs using CSRF patterns.

On the server-side, if you’re using Express, you can use it as middleware. And even if you’re not using Express, it’s available programatically! We baked in 3 strategies for generating and validating tokens. It defaults to the double submit pattern.

On the client-side (optional), we provided some code that patches XHR to send the token along with each request.

If you’re wondering if this is safe to use in production, yes it is! We’ve been serving millions of users every day for over a year now.

Thank you!

Big thanks to Praveen Gorthy, the original author of jwt-csrf!

If you have any questions/issues, please file an issue. Contributions are welcome too; of all kinds! Docs, code, tests, etc. If you’re new to open source, no worries. I’ll help you through it!

Let’s keep the conversation going! — Tweet me @mark_stuart