In the previous article we created a simple GraphQL api from scratch and in today's article I will explain how we can implement a simple authentication and authorization system.
In today's article we are going to work with the user's permissions, first we will create the account, then we will go to the login where we will get the Json Web Token and finally we will protect some queries and mutations (so that only the users with tokens can perform these operations).
For this we will make some changes in the code of the previous article but first of all we will install the necessary dependencies for today's article.
Required Libraries
-
jsonwebtoken
- this dependency will be responsible for creating the json web tokens, as well as checking their readability -
argon2
- this dependency will hash and compare the passwords -
graphql-middleware
- this dependency will allow us to add additional functionality to various resolvers efficiently -
graphql-shield
- this dependency makes it possible to establish roles and permissions in our api in an easy and intuitive way -
dotenv
- this dependency loads environment variables from a.env
file intoprocess.env
Installation
Now let's proceed to install the dependencies that were mentioned earlier:
# NPM
npm install jsonwebtoken argon2 graphql-middleware graphql-shield dotenv
# YARN
yarn add jsonwebtoken argon2 graphql-middleware graphql-shield dotenv
# PNPM
pnpm add jsonwebtoken argon2 graphql-middleware graphql-shield dotenv
User Database Model
Our first step will be to create the user entity in our database. In this article I didn't establish any relationships between the models (User and Dog), but if you want to do so it's completely valid.
// @/src/db/models/User.js
import Sequelize from "sequelize";
import { databaseConnection } from "../index.js";
export const UserModel = databaseConnection.define("User", {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
username: {
type: Sequelize.STRING,
allowNull: false,
},
password: {
type: Sequelize.STRING,
allowNull: false,
},
});
As you may have noticed, our model only has four properties (id, username and password), now just add it to the input file of our models:
// @/src/db/models/index.js
export * from "./Dog.js";
export * from "./User.js"; // <- This line was added
Now with our updated models, let's move on to the next step which will be to create some utilities.
Create Utilities
The reason for creating these utilities is due to the fact that we are going to use them several times during the development of our api and it is not productive to be constantly decorating them, it is better to create a simple standardization.
In the same way that if in the future we want to change a certain dependency, instead of going to all the resolvers, just change it in the util.
Enough talk, let's now create the following utilities (each one corresponds to a different file):
// @/src/utils/hashPassword.js
import { hash } from "argon2";
export const hashPassword = async (password) => {
return await hash(password);
};
// @/src/utils/verifyPassword.js
import { verify } from "argon2";
export const verifyPassword = async (hash, password) => {
return await verify(hash, password);
};
// @/src/utils/signToken.js
import jwt from "jsonwebtoken";
export const signToken = (data) => {
return jwt.sign(data, process.env.JWT_SECRET);
};
// @/src/utils/verifyToken.js
import jwt from "jsonwebtoken";
export const verifyToken = (token) => {
return jwt.verify(token, process.env.JWT_SECRET);
};
With our utilities created, we can create their entry file:
// @/src/utils/index.js
export * from "./hashPassword.js";
export * from "./verifyPassword.js";
export * from "./signToken.js";
export * from "./verifyToken.js";
Now with the utilities created, we can move on to defining our graphql type definitions.
Types and Resolvers
Similar to what we did in the previous article, we will now define our type definitions, however in this case we will only define the user's.
# @/src/graphql/typeDefs/Mutations/User.gql
type User {
id: ID
username: String
password: String
token: String
}
input userInput {
username: String!
password: String!
}
type Mutation {
register(input: userInput): User
login(input: userInput): User
}
As you may have noticed, we created the login and registration mutations. Now let's go to the processinger to create the corresponding resolvers.
First we will work on the registration, for that we will import the model from the user's database, as well as we will import the util of signToken
and hashPassword
. Then we will get the values of the input object from the arguments and we will hash the password sent by the user.
Finally we will add the data in the database table and we will return the necessary properties in the response, such as the user id, username, password and token. In the token payload we will only store the user id.
// @/src/graphql/resolvers/Mutations/register.js
import { UserModel } from "../../../db/models/index.js";
import { signToken, hashPassword } from "../../../utils/index.js";
export const register = async (parent, args, context) => {
const { password, ...rest } = args.input;
const hashedPassword = await hashPassword(password);
const result = await UserModel.create({ ...rest, password: hashedPassword });
return {
id: result.id,
username: result.username,
password: result.password,
token: signToken({ userId: result.id }),
};
};
Then we can start working on the user login and similar to the previous solution, we will first import the user's database model and the necessary utils, such as signToken
and verifyPassword
.
Then we will get the data from the input object of our mutation arguments and we will check if the user exists in the database. After that, we will check if the password entered by the user is the same as the one stored in the database. Finally, we will return in the response only the user id, as well as the username and the token.
// @/src/graphql/resolvers/Mutations/login.js
import { UserModel } from "../../../db/models/index.js";
import { signToken, verifyPassword } from "../../../utils/index.js";
export const login = async (parent, args, context) => {
const { password, username } = args.input;
const result = await UserModel.findOne({ where: { username } });
const isValidPassword = await verifyPassword(result.password, password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
return {
id: result.id,
username: result.username,
token: signToken({ userId: result.id }),
};
};
With our resolvers finished, we can add them to the mutations entry file.
// @/src/graphql/resolvers/Mutations/index.js
export * from "./addDog.js";
export * from "./updateDog.js";
export * from "./deleteDog.js";
export * from "./register.js"; // <- This line was added
export * from "./login.js"; // <- This line was added
Now we can move on to the next step, which is to create the permissions for our api.
Create Rules/Permissions
In this article I will create just one permission, in which we will check whether or not the user is sending the token in the headers and if it is valid.
First, we are going to create our permission, in which we are going to import the rule
function from graphql-shield
and we are going to check if the authorization header is being sent, if not, the user will not be able to perform any action on the resolver.
Otherwise we will get the authorization header token and we will verify it using the verifyToken
util. If the user is legible, he can perform the desired operation.
// @/src/guards/rules/isAuthorized.js
import { rule } from "graphql-shield";
import { verifyToken } from "../../utils/index.js";
export const isAuthorized = rule()(async (parent, args, ctx, info) => {
const { authorization } = ctx.request.headers;
if (!authorization) {
return false;
}
const token = authorization.replace("Bearer", "").trim();
const { userId } = verifyToken(token);
return !!userId;
});
Now we can create the entry file of our rules and let's import the one we just created.
// @/src/guards/rules/index.js
export * from "./isAuthorized.js";
We still need to specify which queries and mutations we want to add permissions and which rules we want to associate in the resolvers. In this article I decided to protect some mutations, such as:
// @/src/guards/index.js
import { shield } from "graphql-shield";
import { isAuthorized } from './rules/index.js'
export const permissions = shield({
Query: {},
Mutation: {
deleteDog: isAuthorized,
addDog: isAuthorized,
updateDog: isAuthorized,
},
});
Now with our rules created, we can make some adjustments to some files.
Small Adjustments
First we need to make some small changes to our Apollo Server instance, like implementing the middleware in our schema.
For this we will import the applyMiddleware function from the graphql-middleware dependency, which will have our schema and the various middlewares that can be added as arguments.
In the createApolloServer function, we only receive an argument to which we destruct to get the app and the schema. However this time we are going to add a new argument, called middleware and this argument will be an array.
Then we will create a variable called schemaWithPermissions
to which the value of the applyMiddleware function will be associated. Finally, just associate the schemaWithPermissions
variable to the ApolloServer schema
property. Like this:
// @/src/apollo/createApolloServer.js
import { ApolloServer } from "apollo-server-fastify";
import { ApolloServerPluginDrainHttpServer } from "apollo-server-core";
import { applyMiddleware } from "graphql-middleware"; // <- This line was added
// midlewares argument was added to the createApolloServer function
export const createApolloServer = (midlewares, { app, schema }) => {
const schemaWithPermissions = applyMiddleware(schema, ...midlewares); // <- This line was added
return new ApolloServer({
schema: schemaWithPermissions, // <- This line was changed
context: ({ request, reply }) => ({
request,
reply,
}),
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer: app.server }),
{
serverWillStart: async () => {
return {
drainServer: async () => {
await app.close();
},
};
},
},
],
});
};
Last but not least, we need to go to our startApolloServer
function and make some final changes. First let's import dotenv
so we can load the environment variables as well as our permissions.
Then let's initialize dotenv and pass the permissions inside an array as the first argument of the createApolloServer
function. Thus:
// @/src/server.js
import { makeExecutableSchema } from "@graphql-tools/schema";
import fastify from "fastify";
import dotenv from "dotenv"; // <- This line was added
import { typeDefs, resolvers } from "./graphql/index.js";
import { permissions } from "./guards/index.js"; // <- This line was added
import { createApolloServer } from "./apollo/index.js";
import { databaseConnection } from "./db/index.js";
export const startApolloServer = async () => {
dotenv.config(); // <- This line was added
const app = fastify();
const schema = makeExecutableSchema({
typeDefs,
resolvers,
});
// Our permissions are passed in the middleware array argument
const server = createApolloServer([permissions], { app, schema });
await server.start();
await databaseConnection.sync();
app.register(server.createHandler());
await app.listen(4000);
};
Our implementation has been completed and now you can use GraphQL Playground or Apollo Studio to perform your queries and mutations, not forgetting that you will need to get the token at login or register so that it can be sent in the headers so that it is possible to perform an operation (such as inserting a dog into the database).
If you made it this far, you can access the Github repository by clicking on this link.
The End
I hope you enjoyed this little series, I've tried to keep everything as simple as possible so that it's easy to implement more things from here or just adjust it to suit your needs. I hope it was helpful. 👊
Top comments (1)
Graphql-Shield is currently unmaintained. What should I use now. Do you have any idea?