DEV Community

Cover image for Nodejs GraphQl Authentication with JWT, Apollo-server, MySql and Sequelize ORM.
Arnav Chauhan
Arnav Chauhan

Posted on • Originally published at Medium

Nodejs GraphQl Authentication with JWT, Apollo-server, MySql and Sequelize ORM.

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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}`);
  });
Enter fullscreen mode Exit fullscreen mode

After this we define our Apollo server which we have to pass an object which contains:

  1. typeDefs: which is the schema for the graphQL API, it defined the query and mutations we can call on the API.

  2. resolvers: these are functions which are responsible for returning a result for respective API calls.

  3. 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.

  4. introspection: it defines whether we can query the schema for information about what queries it supports and their structure.(usually false in production)

  5. 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!
    }
`
Enter fullscreen mode Exit fullscreen mode

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.
Breaking down a mutaion

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;
  };
Enter fullscreen mode Exit fullscreen mode

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)
            }
        }

    },
}
Enter fullscreen mode Exit fullscreen mode

Resolvers contain the functions that are called for respective query and mutation. They take in 4 arguments

  1. root contains the result returned from the resolver on the parent field.
  2. args arguments passed into the field in the query.
  3. context object shared by all resolvers in a particular query.
  4. 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โ€
}
Enter fullscreen mode Exit fullscreen mode

Top comments (3)

Collapse
 
moonseeker profile image
Moon Seeker

Out of curiosity, why did you opt for jsonwebtoken library and not for Jose.

Collapse
 
foxy17 profile image
Arnav Chauhan

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.

Collapse
 
moonseeker profile image
Moon Seeker

Makes sense. Thanks