Are you confused about how authentication and authorization relate to GraphQL APIs? You’re not alone! It’s no secret that learning auth is hard on its own, let alone on top of GraphQL. Let’s demystify auth while learning how to use JSON Web Tokens (JWTs) with GraphQL APIs!
After you watch the talk, head over to Sam's website for slides, resources, sample code, and more.
Sam Julien: [0:01] Hello everybody and welcome. Thank you for watching. We're going to talk today about securing your GraphQL backend with JWTs. A little while ago, Hasura wrote this article called "The Ultimate Guide to Handling JWTs on the Frontend for GraphQL."
[0:19] It sparked a lot of conversation and a little bit of controversy and it showed that people are still working out off with GraphQL. This was for the frontend and that inspired me to build this talk all about the backend with GraphQL.
[0:33] Because the truth is, Auth in GraphQL can be confusing. I mean, Auth in general is already confusing, but in GraphQL can be especially confusing. For example, when we have a regular express backend, we have multiple endpoints. For each of these endpoints, we can add a piece of middleware that lets us protect each particular endpoint, but with GraphQL, we don't have that.
[1:00] We have this one endpoint that's Slash GraphQL that you hit through a post request. All of the other magic of GraphQL happens through resolvers and mutations and things like that. How do we do this? That's what we're going to talk about in this talk.
[1:15] I'm Sam Julien. I am the manager of the developer relations team at Auth0. We build authentication and authorisation platforms for developers. I wrote a book called "Getting Started in Developer Relations."
[1:29] I'm working on my next book called "The Guide to Tiny Experiments," which is a simple framework to help you finish what you start.
[1:35] I also write a weekly newsletter called "Developer Microskills," which are practical actionable ways to improve as a developer and developer advocate. You can find all of that stuff at my website samjulien.com.
[2:10] First, we're going to cover some Auth background. Then we're going to talk about the what and why of JWTs. Then lastly, we're going to talk about various authorization patterns and best practices in GraphQL. First, let's look at some Auth background.
[2:25] Let's define some terms. Auth can be confusing partially because it has so many terms, so much jargon. First, let's define authentication and authorization, because these are two different things. Authentication is, are you who you say you are? Whereas authorization is, do you have permission to access these resources?
[2:50] Let's look at that in the context of GraphQL. We have our user, and then we have our GraphQL server. That GraphQL server is like a layer in between the user and our database. The GraphQL server doesn't really care about whether you are who you say you are.
[3:09] It's trying to control access to the database. Because of that, the GraphQL server probably isn't going to be doing anything with authentication, but it will be doing things with authorization, because it cares about the user proving that they have access to the data they're trying to access.
[3:31] This is called an access problem. What we're concerned with here is access. Let's look at a couple of common scenarios for this. Here's a common scenario where we have the backend and the frontend on the same server. For example, we might have a GraphQL server but then also an XJS frontend, and they're living on the same server. This is a straightforward scenario.
[3:54] Might be using this for a hobby project, or a side project, or something like that, where the user puts in their login credentials, sends it through the browser, that gets sent back to your server. The server generates a cookie and a session ID, sends that cookie back to the browser and everything's logged in. That's a perfectly valid scenario. Often with GraphQL, we're using much more complicated architecture.
[4:20] For example, we might be using GraphQL as only one piece of our backend and then a bunch of other APIs in the cloud for the rest of our application. We might also be using GraphQL as an API gateway where that is the frontend, the facade for a bunch of other APIs in the cloud.
[4:39] Here what we have isn't just an access problem like what we have when we have a backend on the same server as a frontend. What we have here is something called a delegated access problem, because now we have multiple servers, multiple APIs. How do we make sure that the user has access to all of those different pieces of the backend?
[5:00] Rather than having this scenario where everything is on the same server, we have multiple APIs and a GraphQL server. Cookies don't work for this. Cookies are really designed for things living on the same server.
[5:13] We need something other than a cookie for this, but what is that? We need some sort of artifact. This artifact is going to need to do two things. It's going to need to contain useful information, and it's also going to need to be able to be signed and verified.
[5:29] We need to make sure that we can prove that whoever issued this artifact is in fact the person that we expect it to be, or the server that we expect it to be. What is this thing? How do we safely create it? This thing or this artifact is called a token. A token is what we can use for this process.
[5:54] Another vocab word that I want to throw at you is authorization server. This gets confusing, because we're talking about access, authorization, and authentication. An authorization server basically helps you make access control decisions in your app or your API.
[6:12] One of the ways it does this is by issuing this artifact called an access token. This access token informs the API that the bearer has been authorized. Basically, what we're able to do with an access token is say, "Hey, this person does in fact have the permission to access this piece of the API, or this data that they're trying to get to."
[6:35] What does this look like? We would have an authorization server. That's the thing in the top right corner there. It's going to issue the token. It's going to send it back to the frontend. The frontend is going to attach that token to the request and send it off to the API that the user is trying to access.
[6:53] This scenario also works where we have multiple APIs, and also works where we would have an API gateway. The access token is solving the problem of delegated access. Let's dig deeper. You've probably seen an access token before. You might have seen it on the header of a request.
[7:15] There's this authorization header. You might have seen this bearer word, and then a bunch of letters and numbers jumbled together that looks like nonsense. That's the token. Again, we said that the token needs to be able to contain useful information. It needs to also be able to be signed and verified.
[7:35] It turns out there's a convenient format for this that works well with access tokens. It's called a JSON Web Token. JSON Web Tokens are very commonly used for access tokens, but they're not mandatory. You don't have to use JSON Web tokens for access tokens.
[7:52] Let's dig deeper into JSON Web Tokens. They're sometimes known as jots. That's sometimes how people pronounce them, or JWTs. I like to just call them JWTs. I feel like it's clear. I know it's maybe not the most common way.
[8:07] This is what a JWT looks like. There are actually three different pieces to it. At first, it appears to be this jumbled mess of letters and numbers, but there's actually something going on here. What's happening here is that each piece of the JWT is just JSON that's been run through some encoding.
[8:26] The encoding spits out the mess of letters and numbers that you see. That's all there is to a JWT. A JWT has three parts. The first is the header, which is the algorithm and token type. You can see here, this is using HS256. This is JWT. Then we have the payload. The payload is the meat of the JWT. It's got all the data and the claims.
[8:51] Some of the claims are required. You can see this sub-claim over here that's basically like the user ID. Some of them are optional. Lastly, we have the signature. The signature is important, because we can do something here called asymmetric signing. Which means that the authorization server is going to use this private key that only it knows to actually sign the token.
[9:16] There's going to be a public key that we can use to verify and decode the token, and prove that it came from where we expect it to have come from. We can see here that the JWT fills both of these squares. It contains useful information, and it can be signed and verified.
[9:35] As an example of other information it can hold, we can do these custom claims. Let's say we were using Hasura somewhere in our backend, and we wanted to add these different roles, IDs, and things like that. We could add this custom namespace for Hasura, put it right into the token, and be able to use this information. That's how a JWT works.
[9:57] We have this situation where we have the token gets sent over to our frontend. Then that token gets attached to our header and sent over to the backend, which is our GraphQL server. The GraphQL server can do what it needs to do with it.
[10:13] We talked about how GraphQL isn't concerned about the authentication piece. It's concerned with the access piece. That usually means that your actual user management, your authentication side of things will pretty much always live outside of your GraphQL server.
[10:31] That leads us to another question, which is, should you build your own authorization server for issuing these tokens to be used with your GraphQL backend? You could. It can be an interesting educational process to do this. Beyond a hobby process, I recommend against it.
[10:49] You need to take into account a bunch of different things about password controls, recovery, error messages, preventing brute force attacks. It can be overwhelming to properly do a good job of creating this authorization server for these tokens and everything. I recommend outsourcing this piece of your app.
[11:06] I know what you're thinking. You're thinking, "Of course, this guy works for Auth0. Of course he's going to say this." Honestly, even aside from my day job, if you're working with a complex application, it's not worth trying to build this yourself. I would outsource it to a different service that could help with this.
[11:23] We've looked at these different architecture scenarios of multiple APIs and an API gateway. How do we implement this authorization in GraphQL? Let's look at a number of different patterns and best practices for authorization in GraphQL.
[11:38] Remember, as with any backend and database situation, these things are going to vary depending on your implementation. We all know that databases are messy and backends are nuanced. These things might not line up perfectly with what you're working on.
[11:55] These are ideal best practices that you can mix and match. Think of them like tools in your toolbox. First of all, we need to get the token from the request. We're going to have this request come in. It's going to have this authorization with the bearer word, and then JWT after it.
[12:12] The first thing we need to do is we need to verify the access token with the public key that I mentioned earlier. Then we can parse the token and get those claims for authorization. How do we do that? The first thing we need to do is we need to grab the token from the request and add it to our context for our GraphQL server.
[12:33] If we're using Apollo server, for example, Apollo lets you add this function for context, for creating the context. We could pull the token off of the request and then add it to our context by returning it as part of the object. That's only one very small piece of the puzzle. We still need to verify the token, add in some things to make sure that it is what it is.
[12:57] We'll write this function called verify token. That's going to take in the token and then return back to us the payload for the token. We can use that payload to go get the user and other things like that. I said that the request is going to come in with this authorization header.
[13:14] The first thing we need to do is a bit of string manipulation to pull off the actual JWT. That's what we're interested in here. We split out that string. I mentioned that we have this signature as the last part. We need to verify that using our authorization server's public key.
[13:31] There's a format called JSON Web Key Set. You can use some outside libraries. These are readily available in all kinds of ecosystems and frameworks. You can use a library called the JWKS client. Go ahead and put in the endpoint. All of your authorization servers will have an endpoint for the public key. You can put that in.
[13:55] Create this function that will go and get that public key. Once you've got the public key from that endpoint, then you can verify the token. There's some, again, libraries out there that will do this for you. You don't have to hand code the verification. You're going to pass in your token and your public key from the authorization server.
[14:19] You'll add in a little bit of configuration, such as the endpoint for your domain, for your authorization server, things like that. That's going to return the payload for you. You can wrap all of this in a try catch, return the payload if the token is legit, or throw an error if it's not. Now we've written our verify token function.
[14:44] Where do we use this? We could use this in each of our resolvers. We could get our token, do this process over and over again. That doesn't seem very efficient. It makes more sense, instead of doing that inside of our resolvers over and over again, to put that back in our context.
[15:01] What we can do is we can extract that context function out into its own function that we can refer back to. We can use this createContext function. First, we're going to initialize the token and the current user as null variables.
[15:15] The reason we do this is because most of the time, you're going to have some mix of public and private data in your GraphQL server. If it's all private, then you don't need to do this. Sometimes you're going to have a public endpoint, for example, or a public set of data. In those situations, you're not going to be getting a token along with the request.
[15:34] You want to be able to account for this. We're going to do a try catch here. We're going to go get our token. If we do have a token, then we're going to verify it and grab out the payload. Then we can go and get our user. In this example here, we're querying a database and getting the user.
[15:51] You can see that payload.sub is the user ID that we can use with our database. This might look different for your particular situation. If none of these things work, then we can throw an error that says we're unable to authenticate. Lastly, we add the token and the current user along with our database to our context.
[16:12] When we set up our server in Apollo, we can use our function for createContext, pass in our request, and get everything all set up. This lets us in our resolvers, use our context and our current user, and check for things like if we have a user. If not, we can throw an error.
[16:32] If we want to check for permissions, and check to see that the user has the create permission, then we can do that and then proceed with the rest of the resolver. Here, we've got a mutation that's letting us create a new event in our database.
[16:48] This is the basic pattern. We can expand this out, make it more elegant, more readable, things like that. One way we can do this is extracting these into higher order functions. We can create an isAuthenticated function that we can wrap a resolver in, that checks the context to see if we have the current user.
[17:08] If so, we move along. If not, it throws an error. We can wrap our resolver logic with this isAuthenticated higher order function. We wrap that around, then we can proceed with the rest of our logic.
[17:22] We could take it a step further, do the same thing for, has permissions. We could define a check permission higher order function. If we have the right permissions, then we can move along. Otherwise, we can throw an error.
[17:34] Now when we go back to our resolver, we have this nice little function here where we can wrap isAuthenticated and check permissions, and have the rest of our logic for our resolver. That's the fundamental pattern.
[17:49] There are a number of different libraries out there that help add a little bit of abstraction that makes this a little bit more friendly to write. One of these is called GraphQL resolvers by Lucas Constantino. This is authenticated or this pattern that lets you do something like create is an authenticated function, gives you this skip syntax that lets you move on or throw an error.
[18:14] The nice thing about this library is it provides you this combined resolvers function that lets you just stack up these different things in your pipeline here. We can do is authenticate it and then check permissions and then proceed with the rest of the logic.
[18:29] Another library for this that's very nice is GraphQL Auth by Kurt Kemple. It's a simple middleware that integrates with any GraphQL server. It provides this with Auth function. All that's basically entirety of the little library. What's useful is that it's this function that takes two parameters. You've got the scope for the Auth and then the second is the call back when the Auth checking is complete, which is basically the rest of the logic of the resolver.
[18:59] That's a nice option. Another one is GraphQL Modules. What's handy about this is that it lets you do this resolver composition. We have this mutation create event, and we can tell it to compose over it our is authenticated in check permissions functions. That's another handy way to do it.
[19:21] Another pattern we have for authorization in GraphQL is by using middleware. With middleware, for example, a popular library for this was a GraphQL middleware originally created by Prisma and then taken over by the community. GraphQL middleware lets you define some middleware. We've got a mutation. We're adding our is authenticated middleware to our create event mutation.
[19:48] Then when we build our server, when we create our server, we can pass in an array of middleware that we're going to use. This is nice, emulates what we would do in a normal express server, arrest server, or something like that. Nice pattern to use.
[20:02] This has become so popular that there's a very popular tool called GraphQl Shield maintained by the same person who has taken over the larger GraphQL middleware and library. GraphQL Shield has taken off since the time I first built this talk.
[20:18] It's almost, I think it's tripled or quadrupled in its download. It's really become one of the defecto tools for authorization in GraphQL. What GraphQL Shield lets you do is define these rules. These rules take some parameters and then let you define what the parameters are for these rules and define what is true or false for these rules.
[20:39] Then what you can do is use these rules along with some handy helper functions and use them to define your permissions. For example, here we've got our events page, our events query, and we can say that we only want somebody who is both authenticated and either an admin or an editor to be able to get to this query. Then you can define your middleware here and add in your permissions. It's a very handy model.
[21:09] Speaking of models, another pattern for authorization in GraphQL is to use models. Models are sets of functions to read and write data of a GraphQL type using various connectors. Models contain additional business logic like permissions checks and are usually application specific.
[21:29] For example, we could have a model for dog and that defines all the logic for getting the dogs, getting their ID, getting their group ID, things like that. What we can do is we can mix in our user logic to define a factory. Basically, we could create this generate dog model function that takes in the current user, and we can use that user to build out our dog model.
[21:51] For example, in the get Auth function, we can check to see if we have the current user, and if the role for the current user is in admin. If we don't have those things we can return null. Otherwise, we can go and fetch the dog model. I hope you like what I did there.
[22:13] Then we can go back to our context and basically the same thing that we did before. I've hidden some of that code. We can add this model section that's going to let us generate our dog model using the current user.
[22:27] The last pattern that I want to look at for authorization is custom directives. Custom directives are cool. You've probably seen them before and maybe didn't even notice it. One of them that's built into the GraphQL spec is the deprecated directive. Deprecated will let you replace one field with another and let the user, the consumer of the API know that that's happened.
[22:51] For example, if we had an event type and on that event type we had a deprecated field called location, we could tell the consumer of the API, "Hey, this location field has been replaced with event location."
[23:06] We can build these ourselves which is super handy for Auth. Directives have a number of things going for them. For example, they're part of the GraphQL's spec, which is nice, because that means you don't have to use any third party libraries, or anything like that.
[23:20] They also are great for handling changing behavior at runtime, because of this, they have a lot of use cases. One of them that's popular is internationalization, because you can change things on the fly based on some parameters.
[23:34] We can use custom directives to control access all the way down to the field level. Let's look at how to do that. The directives have some different pieces to them. The first piece is the name of the directive. We define it. We use an @ symbol and then write the name. We're going to call ours hasPermission.
[23:53] It takes an argument. We're going to create an argument called permission of the type string. Lastly, we define where it works. Here, we're saying it's on the field definition. You could also do it on the object. There are a couple different things you could do there.
[24:10] Now, we have our definition of our directive at the schema level, but we need to also tell the server what to do when it encounters this directive. This is where it gets sticky, gets complicated. Here's an example using Apollo.
[24:26] Apollo provides the schema directive visitor class. Basically, we create a class called hasPermission directive, and then we override some things from this schema directive visitor class to grab our field. We define this function called visit field definition. Then we pull off our permission from the arguments. This is the directive arguments.
[24:49] We grab our resolver. We redefine the resolve function of our field. In that resolve function, we're going to grab our current user, see if that's there. If we do have a user, we'll continue. If not, we'll throw an error. We're going to check our permissions. We're going to check if the user has the current permission.
[25:10] If they do, we're going to go ahead and continue on with the resolver. Otherwise, we're going to throw an error. This is how we define the logic of the directive. Finally, when we go to make our schema, then we're going to define our schema directives. We can add our hasPermission directive. That makes our directive ready to go.
[25:35] When we go to create a mutation, we can define our mutation. On our mutation, we can use our hasPermission directive, and parse in whichever permission we want. For example, here, we're going to use permissions.create, which is a nice way of using a variable instead of a string here.
[25:54] Custom directives are great. They do have a couple of downsides that you should be aware of. One of them is that custom directives couple your logic to your schema. You're putting your authorization logic, which is technically a type of business logic, and you're adding that to your schema.
[26:10] That's not necessarily a bad thing. It is something that you want to pay attention to, because it might not be appropriate for what you're doing. You want to keep that in mind for future maintenance. They also can be quite difficult to implement.
[26:24] As you can see, from that quick example, you've got a lot of meta programming going on there, where you're digging into some of the underlying tools that your GraphQL server is doing for you, which comes with a little bit of cognitive overhead and maintenance.
[26:38] Lastly, they do require some exhaustive testing. When you're down in that lower-level field definitions, objects, and things like that, you might miss some edge case because you're tinkering at this lower level. The schema directives are like the surgical tools of the authorization.
[26:56] They're very powerful, but require more precision in how you use them. Keep that in mind. I've hit you with a lot of information here. Let's do a quick review. We talked about how GraphQL is like an intermediary between the user and the database.
[27:14] GraphQL is more concerned with the permission, the access to the database that the user has. Do they have the permission to get that data? We also talked about how when you have a backend and a frontend living on the same server, a cookie and a session ID will work fine, but when you're dealing with multiple APIs in the cloud or an API gateway, that doesn't quite work.
[27:39] Instead, you have this delegated access problem that you need a token for. An access token will get sent from an authorization server to the frontend, and then attached to an authorization header to be sent over to the API.
[27:54] The API will then handle decoding and all of that stuff. We talked about how JWTs are a really useful format for access tokens. They're not required, but they are used a lot of the time for access token, and they have three different parts to them.
[28:10] JWTs can contain useful information and they can be signed and verified which makes them ideal for this purpose. In our GraphQL server, we want to create our context, grab our token, decode it, verify it, and then pull our user, add that to the context.
[28:27] Then we can use our context using our current user to check for permissions and things like that in our resolvers. We also looked at a number of different libraries. We looked at GraphQl resolvers that gives us this nice combined resolvers function.
[28:41] We looked at GraphQL Auth which gives us this nice with auth function to make this process a little bit easier. We also looked at GraphQL modules which gives us this resolvers composition option to stack these things together. We looked at GraphQL middleware which gives us a way to define middleware.
[29:00] Then lastly, we had looked at GraphQL Shield which was built on top of GraphQL middleware and is very popular and lets us define these rules and then use those rules in these permissions for our app.
[29:13] Next, we looked at models and using authorization in models. We talked about being able to generate a model with a user and use our permissions in our definition of the model when we generate it. Finally, we looked at custom directives which are the surgical tools of authorization in GraphQL. We looked at how we can define our own directive which has a name and has an argument.
[29:40] Then, how we can define that logic and then use it on our mutations. Just a reminder, while these custom directives are super powerful, they also couple your logic to your schema. They can be difficult, a little tricky to implement and will require some more thorough testing just because they're at that lower level of your server.
[30:00] Thank you so much for watching. I have a boatload of resources, references, sample code, D slides, all available at samj.im/graphql-auth. Thank you so much for watching. I hope you have an awesome rest of your day.