This guide covers writing custom middleware for authentication purposes.
Overview
You can tell code.store to pass every request (query or mutation) marked with @auth directive via your custom middleware. In a nutshell, it looks like the following. Imagine, that you have two queries:
# This directive has to be added to the top of your GraphQL schemadirective@authonFIELD_DEFINITIONtypeQuery { allPosts: [Post] helloWorld: String!@auth}
Here is what happens on each request to one of those queries:
What happens is that all requests marked by @auth directive will pass via auth.handler.ts first and the context argument passed to the subsequent resolver is going to be concatenated with the return result of auth.handler:
That's not very complicated so let's build a simple example using this information!
Very simple example
For the sake of simplicity, we are going to use the code from our Quick Start guide but essentially any other working service (including the newly created one) will suffice.
We added a declaration of the directive to the top of our schema.
We marked two of our requests (query allPosts and mutation createPost) with @auth directive, which means that we will be able to create and see the posts only as authenticated users. Neat!
At the moment, this @auth directive won't do anything, so let's add middleware and implement a very basic authentication model.
First, generate the handler file by running the following command in your service directory:
csgenerate:handler-tauth# this will generate src/resolvers/auth.handler.ts
This is what you will see inside the file (I removed the comments to save screen-space):
The typical authentication flow is like the following:
the client (your front-end or another service) will send a GraphQL request by passing some sort of authorization token in a specific header (typically it would be 'Authorization' header);
your middleware will parse the token and validate it (against another service, or by interrogating the database with active sessions, etc);
in the case when the token is not valid, your middleware will throw an error;
otherwise, it will either do nothing or it will inject some information (like the role of the user or an array of permissions, in the case of RBAC) into the request context by returning an object with that information from the auth.handler.
Say no more, let's implement a very basic authorization, that is going to check the Authorization header and will compare it with two pre-defined tokens:
This is a very basic and insecure example of the authentication which is serving the demonstration purposes only and should not be used on production!
import { logger, AuthHandler } from'codestore-utils';constauthHandler:AuthHandler=async (context) => {// read the Authorization header from the requestconstauthHeader=context.request.header('Authorization');logger.log(authHeader,'auth.handler');// in this example we authorize and assign permissions based on two tokens// in reality these tokens should at least be encryptedif (authHeader ==='admin_token') {return { permissions: { read:true, write:true, } }; } elseif (authHeader ==='user_token') {return { permissions: { read:true, write:false, } }; }// nope, we can't let you use this query/mutation without a tokenthrownewError('You shall not pass!');};exportdefault authHandler;
I hope that it is really straightforward what we do in the above code: we react on two predefined tokens (it's really hard to call them tokens, to be honest 😬), based on which we will assign the permissions or in case if it's missing, we'll throw an error.
Next, we should somehow react to these permissions in our resolvers. Let's see how exactly we might do that:
// src/resolvers/queries/allPosts.tsimport { logger, Resolver } from'codestore-utils';import Post from'../../data/entities/Post';constresolver:Resolver=async (parent, args, context, info) => {// Here is how we check for permissionsif (context?.permissions?.read !==true) {thrownewError('You do not have enough permission to read this article!'); }logger.log('This is a allPosts resolver!','allPosts');constpostRepository=context.db.connection.getRepository(Post);returnpostRepository.find();}exportdefault resolver;
This is what has changed comparing to the code from our Quick Start, we added a small if condition that checks the permissions in the request context argument:
if (context?.permissions?.read !==true) {thrownewError('You do not have enough permission to read this article!');}
Let's do the same for createPost.ts resolver:
// src/resolvers/mutations/createPost.tsimport { logger, Resolver } from'codestore-utils';import Post from'../../data/entities/Post';constresolver:Resolver=async (parent, args, context, info) => {// Checking permissionsif (context?.permissions?.write !==true) {thrownewError('You do not have enough permission to create a Post!'); }logger.log('creating a new Post','createPost');constpost=newPost();post.title =args.title;post.authorName =args.authorName;post.body =args.body;post.createdAt =newDate().toISOString();logger.log(post,'createPost');// Getting a database connectionconstrepository=context.db.connection.getRepository(Post);// Saving our first post entityreturnawaitrepository.save(post);}exportdefault resolver;
That's it! We can now test how our changes impact the behaviour of the service.
Testing
Let's test this baby! First of all, quick go-through to see what we are actually expecting:
query helloWorld should work as before, without any Authorization header (or with it);
query allPosts should return an error if the header is not specified;
query createPost should return an error if the header is not specified, or if the write permission does not equal true.
For the sake of better readability, I will be formatting the JSON output of my curl commands with jq '.' command. That's why the way how JSON output looks like in the examples below may be different from what you see on your screen.
2. query allPosts
# This should normally fail$curl'http://localhost:3000/graphql' \-H"Content-Type: application/json" \ --data '{ "query": "{ allPosts { id title } }" }' 12.18.3
{"errors": [ {"message":"You shall not pass!","locations": [ {"line":1,"column":3 } ],"path": ["allPosts" ],"extensions":{"code":"INTERNAL_SERVER_ERROR","exception":{"stacktrace": ["Error: You shall not pass!","..." ] } } } ],"data":{"allPosts":null }}
You shall not pass indeed! Let's try to add the correct token header to our request:
$curl'http://localhost:3000/graphql' \-H"Authorization: user_token" \-H"Content-Type: application/json" \--data'{ "query": "{ allPosts { id title } }" }'{"data":{"allPosts": [ {"id":"1","title":"Our First Article" }, {"id":"2","title":"Our Second Article" }, {"id":"3","title":"Our Third Article" } ] }}
This time it worked as expected. Let's move on to the final part.
3. mutation createPost
Let's try without the token first (or with the user_token, the result should be the same:
$curl'http://localhost:3000/graphql' \-H"Authorization: user_token"-H"Content-Type: application/json" --data '{ "query": "mutation createPost($title:String!, $body:String!, $authorName:String!) { createPost(title:$title, body:$body, authorName:$authorName) { id title body authorName } }", "variables": "{ \"title\": \"Our Fourth Article\", \"body\": \"Body\", \"authorName\": \"Arthur Conan Doyle\" }" }'
{"errors": [ {"message":"You do not have enough permission to create a Post!","locations": [ {"line":1,"column":75 } ],"path": ["createPost" ],"extensions":{"code":"INTERNAL_SERVER_ERROR","exception":{"stacktrace": ["Error: You do not have enough permission to create a Post!" ] } } } ],"data":{"createPost":null }}
In this short tutorial, we wanted to show you how to put in place a basic authorization system in your service. The idea was to show the capabilities of our SDK on a simple example, but the same approach could be applied to authorization with external systems like AWS Cognito or Auth0, as well as with the dedicated service on code.store.