For past few years we have witnessed the growth of Microservice architecture at a whole different level.It focuses on developing software systems that tries to focus on building single-function modules with well-defined interfaces and operations. Along with it we have also seen a massive growth of Agile, Devops and APIs. Until few years back REST APIs were the industry standard and hot topic but in 2015 Facebook introduced GraphQL and in 2018 they released the first stable version of it.
Github Repo - GraphQL Authentication
In this article, weโll focus on local authentication with JWT token.For database you can use any MySql database.Apollo-server is an open-source GraphQL server that is compatible with any kind of GraphQL client.I will use apollo to expose the API insted of express.
We will be making a simple authentication in which a user will have a first name, last name, email, password, company and unique employeeId. Company will be stored on another table so that we can explore fetching associations with GraphQL. Lets install the necessary packages first:
npm i apollo-server bcrpytjs dotenv jsonwebtoken sequelize mysql2 graphql
npm i -D sequelize-cli nodemon
const getUser = token => {
try {
if (token) {
return jwt.verify(token, JWT_SECRET)
}
return null
} catch (error) {
return null
}
}
This is the first line after the imports, this is how we have defined out JWT middle-ware which will verify if our JWT token is valid.
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const token = req.get('Authorization') || ''
return { user: getUser(token.replace('Bearer', ''))}
},
introspection: true,
playground
: true
})
server.listen({ port: PORT || 8080 }).then(({ url }) => {
console.log(`๐ Server ready at ${url}`);
});
After this we define our Apollo server which we have to pass an object which contains:
typeDefs: which is the schema for the graphQL API, it defined the query and mutations we can call on the API.
resolvers: these are functions which are responsible for returning a result for respective API calls.
context: it is an object shared by all the resolvers of a specific execution.This is where we retrieve the JWT token from header and run the getUser function we defined earlier to check if its valid and store the result in user variable which can be accessed by any resolver.
introspection: it defines whether we can query the schema for information about what queries it supports and their structure.(usually false in production)
playground: is a graphical, interactive, in-browser GraphQL IDE we can use to run queries.
Lets checkout our typeDefs or Schema.
const typeDefs = gql`
input Pagination {
page: Int!
items: Int!
}
input UserFilter {
employeeId: Int
firstName: String
lastName: String
}
type User {
employeeId: Int!
firstName: String!
lastName: String!
password: String!
email: String!
company: String!
}
type AuthPayload {
token: String!
user: User!
}
type Query {
getUserList(search:UserFilter, pagination:Pagination, sort:String): [User]
}
type Mutation {
registerUser(firstName: String!, lastName: String!, employeeId: Int!, email: String!, password: String!, company: String!): AuthPayload!
login (email: String!, password: String!): AuthPayload!
}
`
The gql
template literal tag can be used to concisely write a GraphQL query that is parsed into a standard GraphQL AST. type
defines a object with its parameters. The !
mark means that the parameters are compulsory and cannot be undefined or null. There are two distinct types, query and mutation. In simple words the query is SELECT statement and mutation is INSERT Operation.
Apart from scalar types of String, Int, Float, Boolean, and ID
which we can directly assign as a type to argument or parameter we can have our own defined complex types as input. For that we use the input tag. The UserFilter
input is a custom input which is being passed to get user list query. The [User]
means that an array of type Users will be returned.
All this was the main crust of GraphQL whats left now is the database models which will change according to your database choice and the resolver functions which are just like functions you define for REST API on a specific route.Lets look at the sequelize models.
//User.js
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
firstName: { type: DataTypes.STRING, allowNull: true },
lastName: { type: DataTypes.STRING, allowNull: true },
email: { type: DataTypes.STRING, allowNull: false, unique: true },
password: {type: DataTypes.STRING,allowNull: false},
employeeId:{ type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
}, {timestamps: false,
hooks: {
beforeCreate: async (user) => {
if (user.password) {
const salt = await bcrypt.genSaltSync(10, 'a');
user.password = bcrypt.hashSync(user.password, salt);
}
},
beforeUpdate:async (user) => {
if (user.password) {
const salt = await bcrypt.genSaltSync(10, 'a');
user.password = bcrypt.hashSync(user.password, salt);
}
}
}
});
User.associate = function (models) {
User.hasOne(models.Company, { foreignKey: "employeeId" });
};
User.validPassword = async (password, hash) => {
return await bcrypt.compareSync(password, hash);
}
return User;
};
//Company.js
module.exports = (sequelize, DataTypes) => {
const Company = sequelize.define('Company', {
company: {type: DataTypes.STRING,allowNull: false},
employeeId:{ type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
}, {
timestamps: false,
freezeTableName: true,
});
Company.associate = function (models) {
Company.belongsTo(models.User, { foreignKey: "employeeId" });
};
return Company;
};
beforeCreate
is a hook is called when create query is being called. The hook contains logic to hash the password with a salt so that we donโt store un-encrypted password in database. beforeUpdate
this hook is called when update query is being called on user table. Just as before it hashesh the updated password. User.validPassword
is a class Methods which users bcrypt to compare the hash stored in db against a string to check if both are same. User.associate
is one-to-one association with company table with employeeId foreign key.Timestamp:false
by default sequelize includes a createdAt
and updateAt
record in SQL table but this sets that to false. freezeTableName
by defaults sequelize makes the table name plural which results in errors unless we have them set like that by default. Since I am not doing that freezeTableName
helps me keep the table names exactly what I have defined and not changed the User to Users or Company to Companies. Index.js is just the default seqelize files to connect to database. It also takes all the models defined in the models folder, and it applies them to the โdbโ object.
const resolvers = {
Query: {
async getUserList(root, args, { user }) {
try {
if(!user) throw new Error('You are not authenticated!')
const {search,pagination,sort} =args;
var query={
offset:0,
limit:5,
raw: true,
//this is done to flaten out the join command
attributes: ['firstName','lastName','email','employeeId','Company.company',],
include: [{ model: models.Company,attributes:[]}]
}
//by defaults query is paginated to limit 5 items
if(pagination){
query.limit=pagination.items;
query.offset=pagination.items*(pagination.page-1)
}
if(search){
query.where={
[Op.or]: [
search.firstName?{ firstName: search.firstName }:null,
search.lastName?{ lastName: search.lastName}:null,
search.employeeId?{ employeeId: search.employeeId}:null
]
}
}
if(sort){
query.order= [
[sort, 'ASC'],
];
}
return await models.User.findAll(query);
} catch (error) {
throw new Error(error.message)
}
}
},
Mutation: {
async registerUser(root, { firstName, lastName, email, password, employeeId,company }) {
try {
const userCheck = await models.User.findOne({
where: {
[Op.or]: [
{ email: email },
{ employeeId: employeeId }
]
}})
if (userCheck) {
throw new Error('Email or Employee id already exists')
}
const user = await models.User.create({
firstName,
lastName,
employeeId,
email,
password
})
const companyModel = await models.Company.create({
employeeId,
company
})
const token = jsonwebtoken.sign(
{ employeeId: user.employeeId, email: user.email},
process.env.JWT_SECRET,
{ expiresIn: '1y' }
)
let createdUser={
company:companyModel.company,
employeeId: user.employeeId,
firstName: user.firstName,
lastName: user.lastName,
email: user.email
}
return {
token, user:createdUser, message: "Registration succesfull"
}
} catch (error) {
throw new Error(error.message)
}
},
async login(_, { email, password }) {
try {
const user = await models.User.findOne({ where: { email }})
if (!user) {
throw new Error('No user with that email')
}
const isValid = await models.User.validPassword(password, user.password)
if (!isValid) {
throw new Error('Incorrect password')
}
// return jwt
const token = jsonwebtoken.sign(
{ employeeId: user.employeeId, email: user.email},
process.env.JWT_SECRET,
{ expiresIn: '1d'}
)
return {
token, user
}
} catch (error) {
throw new Error(error.message)
}
}
},
}
Resolvers contain the functions that are called for respective query and mutation. They take in 4 arguments
-
root
contains the result returned from the resolver on the parent field. -
args
arguments passed into the field in the query. -
context
object shared by all resolvers in a particular query. -
info
contains information about the execution state of the query.
The query
object in getUserList
is a dynamic object which changes values based on arguments passed to the query. All arguments are optional. All queries require an Authorization header with valid jwt token. This is being validated by the
if(!user) throw new Error(โYou are not authenticated!โ)
This is user variables is being retrieved from context which we passes earlier in server.js. If we donโt want a route to be authenticated we just have to get rid of this line. Lets get the basic query explained. offset
and limit
are the pagination parameters. raw
is used to return a JSON object instead of a sequelize object so that it is easier to parase. Attributes lets us define what columns we want to be returned from SQL. Include is how we apply join between Company and User table so that we cant fetch the company name for a particular user. You will notice that we have set the attributes for include as empty. This means although they will be returned in query they will not be displayed. They would look something like this if returned {Company.company:"name",Company.employeeId:2}
and this throws and error when we try to parase it using graphQL schema since there we have defined the User to have company key and not Company.company
as the key. Thus to solve this we select โCompany.companyโ
as an attribute of user which gets mapped to the company.
{
โAuthorizationโ:โeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbXBsb3llZUlkIjoyLCJlbWFpbCI6ImJAZ21haWwuY29tIiwiaWF0IjoxNjIyNTMwNTAyLCJleHAiOjE2MjI2MTY5MDJ9.3qQOHPzhKOjM6r5JNRWoMsvyt2IzwX8aa7Bj7s1zEZwโ
}
Top comments (3)
Out of curiosity, why did you opt for jsonwebtoken library and not for Jose.
There was not any specific reason for choosing jsonwebtoken, since this was more of a project to explore graphQL , I chose the other packages which I was comfortable with so that I could maximize the time spend on exploring and understanding graphQL. Of-course Jose is better if you want better browser support and security for a production ready project.
Makes sense. Thanks