Episode #581

Generating and Authenticating with JWTs

Series: Build a Vapor Backend

26 minutes
Published on July 26, 2024

This video is only available to subscribers. Get access to this video and 584 others.

In this episode we will explore supporting JSON Web Tokens, or JWTs. These are a common standard for use in authentication tokens which allows you to support 3rd party authentication providers, token expiration, user metadata and more. These are cryptographically signed and can be verified by the server with a secret. There is also the option to use RSA public/private key pairs to allow clients to verify tokens without going back to the server that signed them. For these reasons JWTs are a really powerful option. Here we will use the Vapor JWT package to provide support for generating and authenticating with HS256 tokens.

Episode Links

  • Vapor Docs - Note that these were slightly out of date when I recorded this video, but they were still very helpful in understanding the general idea.
  • Vapor JWT Package
  • JWT.io - Useful for seeing the structure of tokens
  • Boop App - This is the app I like to use for quick things like decoding JWTs, base64 encoding/decoding. (Mac App Store Link)
  • Apple's Public JWKS - This is an example of a JWKS resource you can fetch to validate tokens. This is only if using RS256 tokens. We'd use this if we supported Sign in with Apple.

This episode uses Xcode 16.0-beta3, Vapor jwt-5.0.0-beta3, Vapor 4.92.

In this episode, we will add support for generating and validating JSON Web Tokens, or JWTs.

JWTs are a common practice for authentication tokens because they can contain various claims, they can expire, they can encode custom data, and be a solution for validating authentication from third-party services such as Sign in with Apple.

All of this can be done securely by signing the token with either a one-way hash or a public-private key pair.

The basic structure of a JWT looks like this.

There are three sections.

You can see that each one is separated by a dot.

The first section is the header, and the header looks like this.

On the left-hand side is what the token looks like when it's Base64 encoded, but the header actually looks like this.

And the header is going to have some different header fields.

This one denotes the type of algorithm, and this one denotes the type of token.

The next section is the payload, and this contains various claims and data.

So this is the subject, so who the token refers to.

This is the name, so this is just random data that we can add to the token.

And then IAT is IssuedAt in a number of seconds.

And so this is how we can encode whatever data we want about who the user is and maybe some attributes we want to include.

And then the last section is a signature because all of this is readable by the client.

This is just Base64 encoding.

You can decode this and see exactly this plain text on the right.

So you shouldn't include any secrets in here.

But what's important about this is the last part of this is the signature, which says that once you've encoded all this data, you sign it with the signature, and then the server can validate that it hasn't been tampered with.

So for instance, if I try to take a token like this and change this identifier, it will no longer be valid.

So here I could say that-- say my secret is secret.

Now, this is just going to sign that token.

But let's say if I change something in the encoded signature-- or sorry, in the header or in the payload, it's no longer going to validate.

So I can mess with this all I want to, and it's no longer going to match the signature.

So that's how you can have a frozen-in-time but readable token that includes a lot of data.

If we take a look at the top here, these algorithms, there's two types of algorithms that you'll encounter.

The most common is this one, HS256.

This means HMAC-SHA256.

And this will be signed with a secret on the server.

And the server is the only one that will be able to validate that the signature matches.

The client will not be able to.

So what we can do here is have a trusted server give you a token.

You give the token back to the server with every request.

And the server can validate that that token has not been tampered with and therefore came from itself at some prior point in time.

The second type you'll find is RS256.

RS256 is a little bit more complex because it includes public and private key pairs.

So this is used for independent validation.

Some services will employ RS256.

And when this is used, you will typically see a KID parameter here, which denotes some sort of key identifier to use.

If we take a look over here in the JSON Web Token Wikipedia page, you can see that there's the header, payload, and signature sections.

And if we scroll down, this will show us all of these things.

So the commonly used header fields, this key identifier, determines which key to use.

And we will have to go look up that key using some well-known place.

So for example-- and all of this will come from the documentation of whatever authentication service you want to support.

So in the case of Apple, Apple has a well-known URL that's publicly accessible.

AppleID.apple.com/auth/keys.

And your application on the server side can fetch this in order to look up that key ID and grab the public key to use for that.

So let's say the key ID in use for the token you received was this one.

Then you're going to use this public key to validate that that token was indeed signed by this service.

Now, these rotate.

So it's a good practice to cache this response so you don't have to go fetch it every single time for every single request that you need to validate, because the user may be sending hundreds of requests to your service.

But you shouldn't cache it for too long.

So in my applications, I typically will cache this for 24 hours.

And these keys will eventually rotate.

They'll probably last much longer than that.

But that way, my application only fetches it once to look up this key set.

And this is called a JWKS, or a JSON Web Key Set.

So this is also a standard.

And depending on an authentication service you use, like, say, Google or Firebase or Auth0, they will all look like this.

The URLs are going to be slightly different.

But you can go look them up in their documentation.

So the benefit of using an RSA or RS256 token is that it can be independently validated on the client.

And that can sometimes be useful.

In our case, we're just going to do HS256.

OK, so that is the overview of JWTs.

Let's go ahead and implement this in our server.

So I'm going to go over to Xcode.

And let's go over to our package.swift file.

And I want to add a new package here.

Sorry, that's dependencies.

Let's go add a new package.

This one is going to be vapor.

And it's going to be JWT.git.

And the version number is going to be 5.0.0-beta.

Now, it's worth noting that this is beta.

The documentation on the vapor website is slightly out of date.

But it wasn't too far from what Xcode sort of guided me.

So that's what we're going to cover is sort of the latest here.

Then on our app, we need to give this a dependency on the JWT module from the package JWT.

Once I've done that, I can hit Save.

And if we take a look over here, we now have the JWT beta here, this package.

And we can start using it.

So the next thing I need to do is set up our signing secret that we want to use.

So I'm going to go up here.

And we're going to add import JWT.

And here we have this configured database.

And the database is using an environment for these secrets.

So I'm also going to do the same thing for the signing secret.

So we're going to go into our .env file, where we have the database username, password, and name.

And I also want to say JWT signing secret.

And let's just call this super secret.

OK.

So now that I have that-- and this will not be checked in, which is good.

So we're going to go over here back to the application code.

And I'm going to say that I want to get the signing secret equals environment.get.

And we will use JWT signing secret.

OK.

I'm going to guard.

Otherwise, I need to throw some error saying that you're missing this.

Let me fix this environment.

And I have this error, missing database credentials.

Let's make another one called missing JWT signing secret.

And now that I have two errors here, I kind of want to make sort of an enum called app errors or something.

So I can do something like missing database credentials.

And then another case for missing JWT signing secret, just to make adding errors a little bit easier.

So now I can say throw app error dot missing JWT signing secret here.

And then I'll change this one to throw app error dot missing database credentials.

And then I can call await app dot JWT, which is an extension on application, which we get by importing that package.

And here I can say dot keys dot add.

And here we can see JWKS and JW key.

And this is if we're using the RS256 token with the public private key pair.

We are not.

We're going to use the HMAC with a digest algorithm.

So here I need to create an HMAC key.

And it implements the custom string literal-- expressible by string literal protocol.

So if you were to pass in a string directly, this would work.

However, we have a variable that's a string, and it's dynamic.

So that will not work.

So we're going to have to create an HMAC key and pass in the string here, signing secret.

And then the digest algorithm here is going to be SHA256.

So that's how the application is going to know what secret to use.

And from this point onward, we don't need to worry about the secret.

So we're going to create a new file here called user plus token.

Here we will import JWT and import vapor.

We're going to extend our user type and then create a token, which is going to be a JWT payload.

Now, the JWT payload is going to have all of the things that we're going to put inside of the JWT payload section.

If we recall here, that would be all of the stuff here in purple.

If I just refresh, all the stuff here in purple is what we're going to be encoding.

And so we're going to define a Codable struct here.

So now the Codable struct is going to have some claims that we have.

So one of them is going to be subject.

And I want to call it subject instead of sub.

So we'll have to customize the coding keys.

So I'm going to say subject is a subject claim.

And because this is a known standard, there's already a type for this.

And so for that, we're going to have to add our coding keys, which is string and coding key.

And then we'll have case subject equals sub.

And that lets our token be readable, but we still encode the correct format here.

We're also going to have an expiration, which is going to be an expiration claim.

We're going to have an issuer, which will be an issuer claim.

We can have an issued at, which will be an issued at claim.

And then just to show that we can have some kind of custom data, I'm going to also add the user ID.

Now, the user ID is going to be put in the subject field, because that's the standard.

But I want to show that we can just encode whatever data we want alongside this.

So here I'm going to say case expiration equals exp, case issuer equals ISS, case issued at equals IAT, case user ID equals UID.

We can call that whatever we want.

OK, so once we have that, we can implement the verify method.

And here is our chance to validate that this is correct.

And we really don't, at this point, have anything to validate other than did it come from us, and is it expired?

So I'm going to create a static let issuer equals GigBuddy server.

So we're going to use this static constant to validate that this came from us.

We don't want to accept a token that came from anyone else.

We can say guard issuer dot value is equal to, and we'll say token dot issuer.

Otherwise, we're going to throw some error.

And there are some errors we can use JWT error here.

And we can find one that makes sense here.

We can say claim verification failed.

And the failed claim is going to be our issuer.

And then the reason is not a valid issuer.

Now, the other thing we can do is validate that it's not expired.

So we can say that try expiration, and expiration actually has a verify not expired.

So we can just pass that along.

And so now we know that this token passes the signing check.

So it hasn't been tampered with.

It came from us, and it's not expired.

OK, so that is the structure of our JWT.

So now let's go over to the user's controller.

And we're going to create a new login endpoint.

So let's do that right here.

Sendable func login.

Our login is going to take a request.

It's going to async throws.

And then it's going to return just a dictionary.

And this will get converted to JSON for us.

We can say let login payload equals-- and now we need to create a login payload.

And I think we had that extended down here on our user.

We did.

So I'm going to create another payload here called login payload, which is content and validatable.

And then we can say email string, password string.

And maybe we just make sure that they're not empty.

OK, so now we have a login payload.

If we go back over here, we can try to decode this.

So we'll say request.content.decode user.login payload.self.

Then we can grab the user.

So we can say guard let user equals try await user.query on request.db.filter.

And we've done this a few times now.

So it's $email custom I like for case insensitive compare.

We're going to grab it from the email in the payload.

And then we're going to grab the first one.

If there is no user, we're going to throw abort.unauthorized.

And if there is, we now need to check the password.

Try bcrypt.verify login payload.password.

And then the password that came from the user is the password hash.

If the password doesn't match, we're also going to do the same error here.

OK, so finally, we have a logged in user or we have a validated user.

Let's add our try here.

We have a logged in user.

So let's create a new JWT.

So I'm going to create a new user token.

And now I need to pass in all of these different things.

So let's give ourselves some room.

So the subject claim is going to be init from string.

Well, init with a value rather.

And this is going to be user.email.

Let's use the email as a subject.

This can be anything you want to identify the user.

The expiration, let's add a new expiration claim with a value of .now.addingTimeInterval.

And let's just say we're going to have this valid for 50 seconds, just so that we can show expiration.

The issuer claim is going to be init with a value of user.token.issuer.

And then the issued at claim is going to be now.

And then the user ID is going to be try user.requireID.

Let's see, issued at claim needs to be init.now.

OK, so now we have a JWT and we can return.

Let's look at the encoded JWT.

That's going to be try await request.jwt.sign.

And we're going to sign this JWT that we created.

So now we have it encoded JWT and we can return that as token encoded JWT.

OK, let's give this a build, make sure it's all building.

Now I'm going to run it.

Our server is starting.

So now I'm going to run curl -x post.

We're going to run that on localhost port 8080/users/login.

And I just realized I forgot one thing.

We have our login route here, but we never registered it at the top.

So we're going to do that outside of our protected group.

So this is just going to be users.post use, sorry, user.post login use login.

Now we have that route registered.

So we're going to do curl -x post -login.

And then we're going to pass in this data.

The data is going to be email is ben5@nsscreencast.com.

And the password will be asdf asdf.

And that needs to be capital.

And I forgot one other thing here.

We need to tell it that we're passing JSON.

So we're going to pass in a header of content type application JSON.

And now we're given a token.

And you can see that this token follows that same pattern.

We've got the header section, a dot, we've got another section, a dot, and then we've got our signature at the end.

Now you can take this into any JWT tool.

So one of the ones that I like to use, just if I want to take a quick look.

You can paste it into here, but I would just caution against pasting JWTs into random websites because this is an authentication token.

And you never know what is going to happen here.

JWT is a trusted resource, jwt.io is, but I still think it's better just to do things local only.

So I'm going to paste my token into this application called Boop, which is, I think it's free.

I can't remember, but it's super handy and it has a JWT decode thing.

Another way you can do this if you use Raycast is there's a view decoded JWT.

You have to have it on your clipboard already.

And this, I believe we'll send the JWT to jwt.io and then come back with the answer here.

Which again, it takes longer.

I don't really like doing that.

I like doing things client side.

So what we can see here is the type is JWT, the algorithm is HS256.

The payload has exactly what we specified.

We've got our custom UID.

This is our custom field.

The subject is my email and the issuer is our server.

It's got an issue dot and an expiration.

So what we can do now is I think that 50 seconds will have passed by the time I'm able to make this request.

So I'm going to go over back here and what I want to do is we want to run users/me.

We no longer have any data to return.

But we do want to have an authorization header here.

So we're going to say authorization is a bearer token.

And pass that in here.

Now this is not going to work because we have not told our controller how to authenticate this user.

So let's go ahead and stop our server.

So let's go over to our users controller.

And then over here we have this user auth token authenticator.

I'm going to call this a user JWT authenticator.

And this one is going to use request dot JWT dot verify.

And the interesting thing here is we can just say verify JWT as user dot token dot self.

That's going to be try await.

That is going to give us our payload.

Then we can say guard let user equals try await user dot find payload dot user ID on request dot db.

If we can't find the user then we can just return.

We don't need to go any further.

And if we can then we can say request dot auth dot login user.

Now what I find great about this is we've baked in how to validate this token in the token itself.

So we just have to say verify here.

We get back the payload that is defined by this token.

We can validate our custom field here and get the user based on that.

And then log in the user.

So now if we go down here I change the name of this to user JWT authenticator.

So now it'll go through basic then JWT then guard it to stop it from going any further unless you are authenticated through one of those means.

So let's go back over here after running the application.

And now let's try to run this again.

Now granted this token is long expired.

So at this point we see unauthorized.

But if I log in again I'll get a new token.

And let's try to go to users dot me again error paste that in and now it works.

So this will continue to work as long as the expiration hasn't passed.

And so we can make that expiration as long as we want.

We could say this token is valid for 30 days after which case the user would have to log in again.

And that would ensure that you know the maximum time that somebody would have with the token is 30 days if that token were ever sort of breached which is an added point of security.

You could also look into having a refresh token so that you have a way of getting a new one provided that you had the old one.

And there are many strategies to do there.

But this is a common standard and I think this is a good thing to add to your application.

So I encourage you to take a deeper look at JWTs for your application authentication needs.