RailsConf 2017

Portable Sessions with JSON Web Tokens

Portable Sessions with JSON Web Tokens

by Lance Ivy

In this RailsConf 2017 presentation, Lance Ivy discusses the use of JSON Web Tokens (JWT) and how they can serve as a flexible and secure method of managing sessions in applications. The session provides a comparative analysis between traditional session management via cookies and the token-based authentication system commonly used in APIs, highlighting the emergence of JWT as a standard for portable secure messages. Ivy begins by explaining the historical context of authentication systems, detailing the nuances between cookies and tokens and the problems that arise when managing separate authentication mechanisms.

Key points include:

- Understanding Authentication: The concept of logging in, involving user credentials (username/password or third-party auth providers), and how cookies and tokens operate to maintain sessions.
- JWT Structure: Introduction to the JWT framework which combines signatures and structured data for secure message communication, emphasizing how it retains claims essential for verifying token authenticity.
- Advantages of JWT over Traditional Methods:
- Flexibility: JWTs can function within both cookie and authorization headers, facilitating use in different architectures.
- Increased Performance: By allowing the API to validate tokens without requiring database queries, JWTs enhance scalability and response times.
- Uncoupling from Rails: JWT libraries are available in multiple programming languages and help in decoupling authentication from specific frameworks like Rails.
- Security Features: The use of asymmetric key signing methods like RSA to reduce the need for shared secrets, thus providing a better security posture in distributed systems.
- Practical Implementations: Ivy shares real-world applications of JWTs in password resets, session management via email, and handling user state changes effectively.

In conclusion, Ivy encourages developers to adopt JWTs in their applications, pointing out their versatility and ease of integration regardless of whether they run monolithic applications or services. The key takeaways emphasize starting with basic implementations of JWTs and exploring their potential as developers become more comfortable with the technology.

00:00:11.780 All right, good morning! Let me get started. People can come in from the hallway if they're still out there.
00:00:17.609 My name is Lance. If you're here, you probably want to learn about JSON Web Tokens, so you're in the right place. We're going to talk a little bit about what they are, why they exist, and maybe who cares.
00:00:28.619 So, they're tokens that use JSON, and they're really for the web. Never mind, scratch that.
00:00:36.149 Now that you're all in the room, I'd like to offer you a special invite to the ground floor of my startup.
00:00:42.030 I've got this idea called Facepage. It's an application where you log in and see a face—probably your own face. It's going to fit somewhere in this 'faces as a service' vertical, and I think it’s going to be pretty big.
00:00:54.780 Welcome! You're all hired. You didn't know this was the interview, but you passed the code challenge when you registered for the conference, right? So, we're good.
00:01:01.070 You all have faces, so the qualifications are there. Let's go! Oh, I suppose you're wondering who I am. I'm Lance Ivy, and I'm based out of Portland, Oregon.
00:01:15.310 I started professionally tinkering on the web as an unpaid developer in 2003. I joined some friends in college to work on a startup idea with a small amount of like 30,000 people, and I started with frontend development.
00:01:36.850 The era of Netscape had just wrapped up. I opened it once, took a picture of a horribly broken rendering, and never looked back. I made it a nice table after that.
00:01:49.989 In 2006, I became a paid Rails developer and worked on Kickstarter. I made my fair share of mistakes, learned from most of them, and picked up a bunch of stories.
00:02:05.200 Currently, I'm working with Inputy Co and also filming an open-source project called Carriage of Austin, which is an authentication server. You can imagine devices that were rebuilt today as a standalone service, and this is the primary learning opportunity for our talk today.
00:02:31.900 Let's get back to Facepage. Obviously, we're going to run Facepage as a standard Rails application. So here we go! We've had team meetings, we've done our sprint, we've changed our task management systems.
00:02:49.650 We've generated faces and discussed what they mean to our users and how they're going to change the world, and then we deploy it out into the wild.
00:03:03.630 There's this one problem: people want licensing events on the go, and they're looking for us in native app stores. So, it's easy, right? We make an API, build out our iOS and Android apps, and maybe the app and API are intertwined.
00:03:16.449 They could be JSON endpoints in the same controller, or maybe they're namespaced, but they're still the same path. You know it's a majestic monolith.
00:03:28.960 Or maybe it’s a standalone deployment, but it doesn't matter. What matters is, it's left in the cloud. It's there, it's running, and it works.
00:03:42.940 But something has happened along the way: we couldn't use cookies for our API, so we invented a quick token authentication system.
00:03:57.790 Now we've got cookies over here and tokens over there, and there's an uneasy feeling that there's something similar, but we can't quite put our finger on it. We keep going, but it turns out we've implemented two separate authentication systems.
00:04:10.810 This is a case of technical debt that we figure we can manage. It's working, so we keep moving and just suppress those uneasy feelings until they become more prominent.
00:04:29.650 That’s when management might make the decision to pivot towards services. We try it out and build this API gateway. But is it written in Rails? Does it handle cookies or tokens or both?
00:04:42.850 Do we have to retrofit our authentication schemes into it? Are we on our own? Do we have help? What happens now?
00:04:55.510 I want to step back and consider the problem that cookies and tokens are actually trying to solve. How did we even get here? It's time to ask: what even is logging in?
00:05:07.710 What happens? What are the cookies and tokens doing? Let's dig in! We could say it starts here—with a username and password.
00:05:29.740 These days, that could also be a Facebook, GitHub, Google, or some other authentication provider, but it probably still involves at least passwords.
00:05:45.449 Turns out, that's not really the part we care about. What we care about is how we keep track of logged-in users.
00:06:03.960 In the classic scenario, when a browser logs in, we send back a cookie. Browsers know about cookies; they know to include them in every request back to our domain. So, every request for a face on a page is logged in, and we can show your face back.
00:06:27.759 I mean, requests for JavaScript, CSS, and images are also carrying these cookies, but that's a different talk.
00:06:39.440 The simplest explanation for cookies is that they are headers in HTTP responses. When the server responds with a 'set-cookie' header, the browser knows to include it in every future request.
00:06:58.970 But I think it's time to consider the elephant in the room: what kinds of cookies are we really dealing with here? When we diagram cookies, they usually look like this: chocolate chip or raisin.
00:07:17.759 But this is how we should think of them: as fortune cookies with a message inside.
00:07:32.380 Rails uses cookies to store bits of data in the browser, and we can crack one open to see what's inside. Here's one that you might see in an actual Rails application. It's an encoded message with a signature.
00:07:49.260 You can split it apart on the double dash. What's inside? There's a user ID, a CSRF token (because with cookie authentication, you need that kind of thing), and a signature to ensure that it's legitimate.
00:08:01.240 So there we go. Cookies are headers in HTTP responses and requests that transport a user ID back and forth, and that's the login story we care about right now.
00:08:10.800 So what's happening with the tokens on the API side? One common convention might look like this: the server responds to a login request with some random string in the JSON body.
00:08:35.890 Let's say the device sends it back on future requests, but this time in the Authorization header. Now, these tokens are opaque; they're random strings with no meaning until we use them to find something more interesting, like the user ID.
00:08:53.820 This is good, but it's not great. On the upside, we could delete these tokens to revoke access at any point, which gives us some control.
00:09:11.950 On the downside, every API query now involves a database query. This, by the way, is how Rails sessions used to work before switching to cookies, and it created performance problems.
00:09:32.820 Browsers submitted a session ID to actually find their session in the database. All right, let's put together what we've learned.
00:09:50.690 The Rails session cookie uses the cookie header, but our API tokens use the authorization header. The Rails session cookie contains structured data, while the API uses just an opaque random string.
00:10:01.250 The Rails session cookie can be verified with cryptography, whereas an API token relies on security through queries.
00:10:12.290 So can you imagine the best of both worlds? JSON Web Tokens are signed structured data. This is rather similar to Rails signed cookies. We've added a third segment—the header—which describes the format of the token.
00:10:35.280 What you're looking at here are called the token claims. Now, I call these claims with a bit of skepticism because you actually have to assert that these claims are true before you can trust them.
00:10:51.290 Here's a list of common claims: the issuer, which describes the party that generated and signed the token; the audience, which describes the party that the message is intended for.
00:11:06.750 These might be the same thing, or they might not. Issued at—that is when the token was created—and expiration, which is when the token should be ignored.
00:11:16.590 Claims can have a lifetime. The claims on the right are what I consider the payload, which contains the information that you probably want to extract for your business logic.
00:11:28.070 You can put anything you want in here, as long as the issuer and the audience agree on its meaning. A common claim that a lot of issuers and audiences generally agree on is the subject.
00:11:41.150 This is meant to identify the party that the message is about or the person who owns the token—the one who has it. And this is where we typically put the user ID.
00:11:54.150 The JSON Web Token standard is a pretty generic thing. It's just the specification for sending secure messages, but one of its primary uses is identity.
00:12:06.750 It actually evolved in the context of OAuth and OpenID Connect, and you can see a lot of that in the claims that are built into it.
00:12:18.390 So we can actually imagine it kind of like Rails cookies and API tokens. Think of it as an ID card. Just like an ID card, it makes a number of claims and contains some security features.
00:12:35.890 This ID card has an issuer from the internet, a subject name, expiration and issued dates, and this pretty sweet official stamp for security.
00:12:54.230 But it's actually up to you to check the card and detect forgeries to ensure that you can actually take this identification.
00:13:01.350 So here's how you do it: First, check if it's from someone recognized as an authority—check the issuer; then, was it intended for you? Check the audience.
00:13:18.690 Has it expired? Check the expiration; is it a forgery? Check the signature to see if you can recreate it based on the values provided.
00:13:27.330 Finally, confirm whether it was generated before or after the time we had to change our secret—like if we accidentally published it on GitHub.
00:13:42.240 If you can check those live questions, you're in pretty good shape, and the good news is you can get a library to handle this for you.
00:13:50.810 Okay, so we’ve learned that JSON Web Tokens are secure messages like Rails signed session cookies.
00:14:04.100 We’ve learned that they contain claims we need to verify, and we’ve discussed the most important claims and what they represent.
00:14:13.150 So let’s talk about what we can do with these tokens. We’ve already mentioned identity tokens, so let's continue from there.
00:14:28.490 One of the problems we had in our Facepage app was the multiple authentication systems that needed to be integrated, so let's see how JSON Web Tokens can help.
00:14:42.580 First, let's add JSON Web Tokens to our comparison table. The Rails sessions are tied to the cookie header, while the API tokens use the authorization header.
00:14:54.880 Our JSON Web Tokens are ready for either header; they don't care. Rails cookies are structured data signed with cryptography, and our JSON Web Tokens share those characteristics.
00:15:07.510 If we use an identity JSON Web Token for login, it might look like this: the browser submits the login, and Rails responds with a JSON Web Token and a cookie.
00:15:24.110 The JWT contains the user ID as a subject. Now, in future requests, the browser just sends it back in the cookie, and that looks pretty familiar, which is a good thing.
00:15:39.690 We actually haven't changed our headers or the relationship between the browser and the server; we're still using cookies.
00:15:51.350 We’ve just changed the format of the message inside the cookie. And on the API, we can drop it in here as well. There's no change to the client; they're still sending a string back and forth.
00:16:08.660 But now that string is the JSON Web Token. It has structured data; it has meaning; it’s not random, and the server can do something with it.
00:16:22.960 This is a JSON Web Token solution: one token, two headers, one authentication system.
00:16:35.170 It doesn't matter whether the server finds the token inside a cookie or inside an authorization header; it can still handle that value the same way.
00:16:49.200 Problem number two in our Facepage app: previously, the API had to execute a query on every request just to discover who was making it.
00:17:09.150 Now, our API can verify the JWT with the claims and the cryptography, replacing a network-bound database bottleneck process with a straightforward CPU-bound calculation.
00:17:23.640 This will perform faster, scale better, introduce less variation into response times, and generally just have fewer failure modes.
00:17:38.270 Problem number three: our cookies were implemented for Rails by Rails. Now, don't get me wrong—the default Rails session store is wonderful.
00:17:52.150 It works, it’s hidden, it's secure, it is very well designed, and it does the job it needs to do. However, it’s tightly coupled to Rails.
00:18:06.330 If any single thing doesn't work for you, you have to ask yourself, 'What's next?' JSON Web Token libraries are implemented in at least 20 languages.
00:18:21.120 They're decoupled from cookies and contain claims that you can use to build any kind of distributed architecture, so they're more flexible.
00:18:30.460 They provide a more generalized solution.
00:18:41.420 Problem number four: in a distributed architecture, you might find yourself sharing secrets. This means signing a message with one system to verify it in another.
00:19:02.250 This can involve trusted back channels like copy and paste or configuration management systems. And when the secret exists in multiple places, it creates a bigger attack surface.
00:19:14.700 If one of those locations is compromised, that secret can be used to attack the others. So what do JSON Web Tokens offer? They support asymmetric key algorithms like RSA.
00:19:34.800 The signature process used by Rails to sign a cookie is called HMAC. You give it a salt; it hashes the cookie. You take that salt, rehash it later, and verify it.
00:19:49.900 The required setup for RSA is a little bit different. The server signing the token needs a special RSA key, not just a random salt.
00:20:05.800 The server uses the private key to sign the token and can publish the public key on a free and open HTTP endpoint using a specification like JSON Web Keys.
00:20:27.750 When some other server receives the message, it can fetch the public key, use that to verify the token, and then cache it forever.
00:20:37.630 So one HTTP call automatically shares the secret. But it’s not really a secret; it’s the public key.
00:20:50.090 This investment means you don't need to share secrets. It’s a little bit more upfront, but the operational cost is lower.
00:21:05.180 This means there’s no 'copy and paste' between systems. You can just fetch the key over HTTP—there's no need for a highly coordinated 'lockstep' deployment.
00:21:21.890 This also reduces the attack surface: a lost secret can only attack the service that leaked it. This can even prepare you for nifty automatic key rotation.
00:21:42.960 I'm not going to get into that right now, but if you're curious about how that works, feel free to come find me at the conference.
00:21:56.530 Problem number five: you might think you know how password resets work. You generate and send a token; it's a nonce, a random string.
00:22:12.740 You verify it in your controller, then regenerate the token to expire the old one you sent out, making sure that people can't hack the system. When you think about it, this is actually a third authentication system.
00:22:36.730 It works a lot like the opaque API tokens, and again, it’s implemented as a one-off.
00:22:56.800 So here’s how I build a password reset JSON Web Token: First, start with a standard identity token. This contains the metadata claims and the subject.
00:23:10.010 Then, I add a scope claim. I looked around, and this one doesn't seem standard, but I couldn't find a better name, so I'll just call it scope-a.
00:23:22.620 The idea here is that I’ll configure my passwords controller to accept tokens with this scope and configure the rest of my application to reject tokens with this scope or any inappropriate ones.
00:23:38.090 Then, I add an optimistic lock. An optimistic lock is where you maintain some kind of version, and every time a field updates, you increment that version.
00:23:59.750 Anyone who wants to make a change must tell you the current version. This ensures they don't overwrite something that changed without their knowledge.
00:24:11.270 You can achieve this using a timestamp. So, that's what I've done here, maintaining a user's password change timestamp, which is also good for other features.
00:24:25.160 Then, verify the timestamp against its own. If it matches, you’re good to go. This effectively also expires the old reset tokens as soon as the password changes.
00:24:40.650 Once again, we can upgrade opaque tokens into structured signed data.
00:24:50.200 This is one less field in the user's table, one less index for your queries, but even better, we’ve absorbed a third authentication system by teaching our JSON Web Token back-end about scopes.
00:25:02.170 We’ve also educated our passwords controller about optimistic locks.
00:25:14.200 Problem number six: suppose you're sending emails with survey links or some kind of strong call-to-action where you need to know who's clicking.
00:25:26.270 If you don't help them at all, they'll hit a login wall. And if they’re on their phones, they might not want to type it in, so they could just come back later—or not!
00:25:40.420 This would hurt your conversion rate. So, you might implement random strings as opaque tokens, connecting them back to the user, similar to the API system. I mean, this is sounding pretty familiar, right?
00:25:59.610 We can generalize the password reset solution. All we need is a scope claim. If it sounds like I'm suggesting that we send user sessions through email, yes, that’s exactly what I'm suggesting.
00:26:16.180 Basically, that's what the randomly generated one-off strings are doing—they're giving a login session via email. But this one is built into our authentication system.
00:26:31.300 It’s not a one-off implementation that you’re going to forget about.
00:26:42.570 Problem number seven: your application is already doing a lot of stuff. You're including many common standard authentication features as if they're unique.
00:26:56.850 These features run from the same database, and they can be affected by every deployment, every upgrade. All this complexity is in one spot, making it harder to audit your attack surface.
00:27:16.510 Let's not forget the always-present user-god model. JSON Web Tokens can help with this because they were born ready for this.
00:27:33.470 This is why the issuer and audience claims exist: they can be different things. The issuer can take responsibility for the account.
00:27:45.700 This account could contain details like the username, the password, the last time the password was changed, and the number of failed login attempts.
00:27:58.050 This shifts responsibility for account tracking away from your application and allows it to maintain a simpler user model. It merely relates the user to an account.
00:28:14.890 This is actually what I learned from working on Keratin Auth, which is what you would get if Devise was rebuilt as a standalone authentication service.
00:28:30.200 It removes complexity from your application and relies heavily on JSON Web Tokens. Every trick I’ve mentioned here, and then some. The core technology is stable.
00:28:44.080 I've got ideas for some advanced features, so if anyone's interested, I’d love to chat about them.
00:29:01.600 In conclusion, here are the takeaways from this presentation: 1. You can use JSON Web Tokens right now. It doesn't matter if you have a monolith or services. You may recognize one or more of these problems.
00:29:15.510 2. JSON Web Tokens have a low learning curve and a high skill ceiling. There's much more you can do with them as you gain confidence, so start somewhere.
00:29:31.480 If you leave this conference with a head full of ideas for things you wish you could use at your day job, perhaps JSON Web Tokens are among them. Pick something, learn it by doing it, and make your knowledge real.
00:29:49.570 All right! Do we have time for questions?
00:30:03.640 So the question is, if the user has been disabled by an admin since they logged in, how can you immediately log them out and ensure they don't return?
00:30:13.220 You can create a blacklist and maintain a temporary cache of invalidated tokens. If you choose, for critical actions, you can implement revalidation.
00:30:25.520 The trade-off is real. If you keep authentication in a token that lives for 30 minutes, I recommend that those tokens have a short lifespan and that you regenerate them frequently.
00:30:36.800 This way, the time window for such a problem is small.
00:30:48.090 So again, the comment is that checking this blacklist takes time. I recommend determining which endpoints are critical and revalidating only for those.
00:30:59.320 There are probably many endpoints where it's acceptable for the user to continue reading data for a few more minutes.
00:31:06.520 Alternatively, you could protect that data and choose to revalidate where necessary.
00:31:19.640 You can try JSON Web Tokens in an existing app by finding any kind of opaque token and exploring how you can replace it.
00:31:32.730 If you aim to try an authentication server, it helps to have an app with some kind of login scheme where you can swap out where the form submits.
00:31:47.540 The form might submit to your back-end, while another form submits to a different back-end. Those are a couple of ideas.
00:32:02.490 Is there a drop-in replacement?
00:32:10.570 There’s a library called Knock, and Knock works like Devise but with JWT. It’s built into the monolith.
00:32:30.570 However, an authentication server will run as a separate deployment, while Knock integrates JWT as part of the same model.
00:32:44.800 It's refreshing because it uses more tokens. The way I've seen it, you maintain what's called an access token and a refresh token.
00:33:03.890 The access token is what you send to the API, while the refresh token is more secure because it's not used very much. Its sole purpose is to fetch new access tokens.
00:33:21.090 For instance, imagine your access token is valid for an hour, and you choose to regenerate it every 30 minutes.
00:33:39.680 You can build revocation into that refresh system. The refresh could query Redis or MySQL to check if it's still valid to generate new access tokens.
00:33:58.570 This dual token system allows for different security properties while maintaining a lightweight protocol for chatty requests.
00:34:09.160 The idea is that if you attempt to use a token that's expired, you realize you need a new one.
00:34:23.770 The recommendation is to aggressively refresh before that expiration occurs, creating a smoother user experience.
00:34:37.280 If you refresh efficiently enough, you may not need to deal with that last-minute 'let's get a new one' situation.
00:34:51.580 In some dire situations, which we all hope to avoid, there's truly no going back.
00:35:01.940 You generate a new key and switch to using it. Then you would implement an epoch to state that any token generated before this point in time was using an old key and is no longer trusted.
00:35:20.290 For example, if the issue date was set before last Friday, when the compromise occurred, just discard it. This means users would be logged out.
00:35:32.630 The comment was that you'd need multiple keys and an epoch for each key. JSON Web Tokens have a claim called key ID.
00:35:48.830 You can embed within each JWT a signature reflecting the specific key used, allowing you to monitor which were used and if still reliable.
00:36:03.420 For basic login sessions, it's straightforward—the user ID. If you choose to store this in a cookie, you'll need to manage CSRF.
00:36:17.930 You might also include the CSRF token within the JWT because you can add more information freely.
00:36:27.960 I'm actually not a big fan of storing extensive data in sessions, so I recommend keeping your JWT payload concise. Yes, it may be a bit longer than a Rails equivalent cookie, but it's a trade-off for having more information accessible.
00:36:42.450 Some advocates promote saving more user information in sessions, such as admin roles, permissions, and basic contact info. It could save on queries, but take caution—think about how stale that information might become.