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 schema
directive @auth on FIELD_DEFINITION
type Query {
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.
Let's take a look at our schema first:
type Post {
id: ID!
createdAt: String!
title: String!
body: String!
authorName: String!
}
type Query {
allPosts: [Post]
helloWorld: String!
}
type Mutation {
createPost(title: String!, body: String!, authorName: String!): Post
}
As you may see, we have restored the helloWorld query, we also have restored the src/resolvers/queries/helloWorld.ts file too.
Let's modify our schema by adding the directive declaration and by marking one of the requests as @auth:
directive @auth on FIELD_DEFINITION
type Post {
id: ID!
createdAt: String!
title: String!
body: String!
authorName: String!
}
type Query {
allPosts: [Post] @auth
helloWorld: String!
}
type Mutation {
createPost(title: String!, body: String!, authorName: String!): Post @auth
}
There are two things that happened here:
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:
cs generate:handler -t auth
# 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):
// src/resolvers/auth.handler.ts
import { AuthHandler } from 'codestore-utils';
const authHandler: AuthHandler = async (context) => {
// your code goes here
return {};
};
export default authHandler;
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';
const authHandler: AuthHandler = async (context) => {
// read the Authorization header from the request
const authHeader = 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 encrypted
if (authHeader === 'admin_token') {
return {
permissions: {
read: true,
write: true,
}
};
} else if (authHeader === 'user_token') {
return {
permissions: {
read: true,
write: false,
}
};
}
// nope, we can't let you use this query/mutation without a token
throw new Error('You shall not pass!');
};
export default 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.ts
import { logger, Resolver } from 'codestore-utils';
import Post from '../../data/entities/Post';
const resolver: Resolver = async (parent, args, context, info) => {
// Here is how we check for permissions
if (context?.permissions?.read !== true) {
throw new Error('You do not have enough permission to read this article!');
}
logger.log('This is a allPosts resolver!', 'allPosts');
const postRepository = context.db.connection.getRepository(Post);
return postRepository.find();
}
export default 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) {
throw new Error('You do not have enough permission to read this article!');
}
Let's do the same for createPost.ts resolver:
// src/resolvers/mutations/createPost.ts
import { logger, Resolver } from 'codestore-utils';
import Post from '../../data/entities/Post';
const resolver: Resolver = async (parent, args, context, info) => {
// Checking permissions
if (context?.permissions?.write !== true) {
throw new Error('You do not have enough permission to create a Post!');
}
logger.log('creating a new Post', 'createPost');
const post = new Post();
post.title = args.title;
post.authorName = args.authorName;
post.body = args.body;
post.createdAt = new Date().toISOString();
logger.log(post, 'createPost');
// Getting a database connection
const repository = context.db.connection.getRepository(Post);
// Saving our first post entity
return await repository.save(post);
}
export default 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.
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.
All queries/mutation marked by @auth will pass via auth.handler.ts first
The return result of auth.handler.ts is getting injected into the _context_ argument of subsequent resolvers.