Let’s Create Our Own Authentication API with Nodejs and GraphQL
Publikováno: 13.10.2020
Authentication is one of the most challenging tasks for developers just starting with GraphQL. There are a lot of technical considerations, including what ORM would be easy to set up, how to generate secure tokens and hash passwords, and even what HTTP library to use and how to use it.
In this article, we’ll focus on local authentication. It’s perhaps the most popular way of handling authentication in modern websites and does so by requesting the user’s email and password … Read article “Let’s Create Our Own Authentication API with Nodejs and GraphQL”
The post Let’s Create Our Own Authentication API with Nodejs and GraphQL appeared first on CSS-Tricks.
You can support CSS-Tricks by being an MVP Supporter.
Authentication is one of the most challenging tasks for developers just starting with GraphQL. There are a lot of technical considerations, including what ORM would be easy to set up, how to generate secure tokens and hash passwords, and even what HTTP library to use and how to use it.
In this article, we’ll focus on local authentication. It’s perhaps the most popular way of handling authentication in modern websites and does so by requesting the user’s email and password (as opposed to, say, using Google auth.)
Moreover, This article uses Apollo Server 2, JSON Web Tokens (JWT), and Sequelize ORM to build an authentication API with Node.
Handling authentication
As in, a log in system:
- Authentication identifies or verifies a user.
- Authorization is validating the routes (or parts of the app) the authenticated user can have access to.
The flow for implementing this is:
- The user registers using password and email
- The user’s credentials are stored in a database
- The user is redirected to the login when registration is completed
- The user is granted access to specific resources when authenticated
- The user’s state is stored in any one of the browser storage mediums (e.g.
localStorage
, cookies, session) or JWT.
Pre-requisites
Before we dive into the implementation, here are a few things you’ll need to follow along.
- Node 6 or higher
- Yarn (recommended) or NPM
- GraphQL Playground
- Basic Knowledge of GraphQL and Node
- …an inquisitive mind!
Dependencies
This is a big list, so let’s get into it:
- Apollo Server: An open-source GraphQL server that is compatible with any kind of GraphQL client. We won’t be using Express for our server in this project. Instead, we will use the power of Apollo Server to expose our GraphQL API.
- bcryptjs: We want to hash the user passwords in our database. That’s why we will use bcrypt. It relies on Web Crypto API‘s
getRandomValues
interface to obtain secure random numbers. - dotenv: We will use dotenv to load environment variables from our
.env
file. - jsonwebtoken: Once the user is logged in, each subsequent request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token.
jsonwebtoken
will be used to generate a JWT which will be used to authenticate users. - nodemon: A tool that helps develop Node-based applications by automatically restarting the node application when changes in the directory are detected. We don’t want to be closing and starting the server every time there’s a change in our code. Nodemon inspects changes every time in our app and automatically restarts the server.
- mysql2: An SQL client for Node. We need it connect to our SQL server so we can run migrations.
- sequelize: Sequelize is a promise-based Node ORM for Postgres, MySQL, MariaDB, SQLite and Microsoft SQL Server. We will use Sequelize to automatically generate our migrations and models.
- sequelize cli: We will use Sequelize CLI to run Sequelize commands. Install it globally with
yarn add --global sequelize-cli
in the terminal.
Setup directory structure and dev environment
Let’s create a brand new project. Create a new folder and this inside of it:
yarn init -y
The -y
flag indicates we are selecting yes to all the yarn init
questions and using the defaults.
We should also put a package.json
file in the folder, so let’s install the project dependencies:
yarn add apollo-server bcrpytjs dotenv jsonwebtoken nodemon sequelize sqlite3
Next, let’s add Babeto our development environment:
yarn add babel-cli babel-preset-env babel-preset-stage-0 --dev
Now, let’s configure Babel. Run touch .babelrc
in the terminal. That creates and opens a Babel config file and, in it, we’ll add this:
{
"presets": ["env", "stage-0"]
}
It would also be nice if our server starts up and migrates data as well. We can automate that by updating package.json
with this:
"scripts": {
"migrate": " sequelize db:migrate",
"dev": "nodemon src/server --exec babel-node -e js",
"start": "node src/server",
"test": "echo \"Error: no test specified\" && exit 1"
},
Here’s our package.json
file in its entirety at this point:
{
"name": "graphql-auth",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"migrate": " sequelize db:migrate",
"dev": "nodemon src/server --exec babel-node -e js",
"start": "node src/server",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"apollo-server": "^2.17.0",
"bcryptjs": "^2.4.3",
"dotenv": "^8.2.0",
"jsonwebtoken": "^8.5.1",
"nodemon": "^2.0.4",
"sequelize": "^6.3.5",
"sqlite3": "^5.0.0"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-0": "^6.24.1"
}
}
Now that our development environment is set up, let’s turn to the database where we’ll be storing things.
Database setup
We will be using MySQL as our database and Sequelize ORM for our relationships. Run sequelize init (assuming you installed it globally earlier). The command should create three folders: /config
/models
and /migrations
. At this point, our project directory structure is shaping up.
Let’s configure our database. First, create a .env
file in the project root directory and paste this:
NODE_ENV=development
DB_HOST=localhost
DB_USERNAME=
DB_PASSWORD=
DB_NAME=
Then go to the /config
folder we just created and rename the config.json
file in there to config.js
. Then, drop this code in there:
require('dotenv').config()
const dbDetails = {
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOST,
dialect: 'mysql'
}
module.exports = {
development: dbDetails,
production: dbDetails
}
Here we are reading the database details we set in our .env
file. process.env is a global variable injected by Node and it’s used to represent the current state of the system environment.
Let’s update our database details with the appropriate data. Open the SQL database and create a table called graphql_auth
. I use Laragon as my local server and phpmyadmin
to manage database tables.
What ever you use, we’ll want to update the .env
file with the latest information:
NODE_ENV=development
DB_HOST=localhost
DB_USERNAME=graphql_auth
DB_PASSWORD=
DB_NAME=<your_db_username_here>
Let’s configure Sequelize. Create a .sequelizerc
file in the project’s root and paste this:
const path = require('path')
module.exports = {
config: path.resolve('config', 'config.js')
}
Now let’s integrate our config into the models. Go to the index.js
in the /models
folder and edit the config
variable.
const config = require(__dirname + '/../../config/config.js')[env]
Finally, let’s write our model. For this project, we need a User
model. Let’s use Sequelize to auto-generate the model. Here’s what we need to run in the terminal to set that up:
sequelize model:generate --name User --attributes username:string,email:string,password:string
Let’s edit the model that creates for us. Go to user.js
in the /models
folder and paste this:
'use strict';
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
username: {
type: DataTypes.STRING,
},
email: {
type: DataTypes.STRING,
},
password: {
type: DataTypes.STRING,
}
}, {});
return User;
};
Here, we created attributes and fields for username, email and password. Let’s run a migration to keep track of changes in our schema:
yarn migrate
Let’s now write the schema and resolvers.
Integrate schema and resolvers with the GraphQL server
In this section, we’ll define our schema, write resolver functions and expose them on our server.
The schema
In the src folder, create a new folder called /schema
and create a file called schema.js
. Paste in the following code:
const { gql } = require('apollo-server')
const typeDefs = gql`
type User {
id: Int!
username: String
email: String!
}
type AuthPayload {
token: String!
user: User!
}
type Query {
user(id: Int!): User
allUsers: [User!]!
me: User
}
type Mutation {
registerUser(username: String, email: String!, password: String!): AuthPayload!
login (email: String!, password: String!): AuthPayload!
}
`
module.exports = typeDefs
Here we’ve imported graphql-tag from apollo-server. Apollo Server requires wrapping our schema with gql
.
The resolvers
In the src
folder, create a new folder called /resolvers
and create a file in it called resolver.js
. Paste in the following code:
const bcrypt = require('bcryptjs')
const jsonwebtoken = require('jsonwebtoken')
const models = require('../models')
require('dotenv').config()
const resolvers = {
Query: {
async me(_, args, { user }) {
if(!user) throw new Error('You are not authenticated')
return await models.User.findByPk(user.id)
},
async user(root, { id }, { user }) {
try {
if(!user) throw new Error('You are not authenticated!')
return models.User.findByPk(id)
} catch (error) {
throw new Error(error.message)
}
},
async allUsers(root, args, { user }) {
try {
if (!user) throw new Error('You are not authenticated!')
return models.User.findAll()
} catch (error) {
throw new Error(error.message)
}
}
},
Mutation: {
async registerUser(root, { username, email, password }) {
try {
const user = await models.User.create({
username,
email,
password: await bcrypt.hash(password, 10)
})
const token = jsonwebtoken.sign(
{ id: user.id, email: user.email},
process.env.JWT_SECRET,
{ expiresIn: '1y' }
)
return {
token, id: user.id, username: user.username, email: user.email, message: "Authentication 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 bcrypt.compare(password, user.password)
if (!isValid) {
throw new Error('Incorrect password')
}
// return jwt
const token = jsonwebtoken.sign(
{ id: user.id, email: user.email},
process.env.JWT_SECRET,
{ expiresIn: '1d'}
)
return {
token, user
}
} catch (error) {
throw new Error(error.message)
}
}
},
}
module.exports = resolvers
That’s a lot of code, so let’s see what’s happening in there.
First we imported our models, bcrypt
and jsonwebtoken
, and then initialized our environmental variables.
Next are the resolver functions. In the query resolver, we have three functions (me
, user
and allUsers
):
me
query fetches the details of the currentlyloggedIn
user. It accepts auser
object as the context argument. The context is used to provide access to our database which is used to load the data for a user by the ID provided as an argument in the query.user
query fetches the details of a user based on their ID. It acceptsid
as the context argument and auser
object.alluser
query returns the details of all the users.
user
would be an object if the user state is loggedIn
and it would be null
, if the user is not. We would create this user in our mutations.
In the mutation resolver, we have two functions (registerUser
and loginUser
):
registerUser
accepts theusername
,email
andpassword
of theuser
and creates a new row with these fields in our database. It’s important to note that we used the bcryptjs package to hash the users password withbcrypt.hash(password, 10)
.jsonwebtoken.sign
synchronously signs the given payload into a JSON Web Token string (in this case the userid
andemail
). Finally,registerUser
returns the JWT string and user profile if successful and returns an error message if something goes wrong.login
acceptsemail
andpassword
, and checks if these details match with the one that was supplied. First, we check if theemail
value already exists somewhere in the user database.
models.User.findOne({ where: { email }})
if (!user) {
throw new Error('No user with that email')
}
Then, we use bcrypt’s bcrypt.compare
method to check if the password matches.
const isValid = await bcrypt.compare(password, user.password)
if (!isValid) {
throw new Error('Incorrect password')
}
Then, just like we did previously in registerUser
, we use jsonwebtoken.sign
to generate a JWT string. The login
mutation returns the token and user
object.
Now let’s add the JWT_SECRET
to our .env
file.
JWT_SECRET=somereallylongsecret
The server
Finally, the server! Create a server.js
in the project’s root folder and paste this:
const { ApolloServer } = require('apollo-server')
const jwt = require('jsonwebtoken')
const typeDefs = require('./schema/schema')
const resolvers = require('./resolvers/resolvers')
require('dotenv').config()
const { JWT_SECRET, PORT } = process.env
const getUser = token => {
try {
if (token) {
return jwt.verify(token, JWT_SECRET)
}
return null
} catch (error) {
return null
}
}
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: process.env.PORT || 4000 }).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
Here, we import the schema, resolvers and jwt, and initialize our environment variables. First, we verify the JWT token with verify
. jwt.verify
accepts the token and the JWT secret as parameters.
Next, we create our server with an ApolloServer
instance that accepts typeDefs
and resolvers.
We have a server! Let’s start it up by running yarn dev
in the terminal.
Testing the API
Let’s now test the GraphQL API with GraphQL Playground. We should be able to register, login and view all users — including a single user — by ID.
We’ll start by opening up the GraphQL Playground app or just open localhost://4000
in the browser to access it.
Mutation for register user
mutation {
registerUser(username: "Wizzy", email: "ekpot@gmail.com", password: "wizzyekpot" ){
token
}
}
We should get something like this:
{
"data": {
"registerUser": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsImVtYWlsIjoiZWtwb3RAZ21haWwuY29tIiwiaWF0IjoxNTk5MjQwMzAwLCJleHAiOjE2MzA3OTc5MDB9.gmeynGR9Zwng8cIJR75Qrob9bovnRQT242n6vfBt5PY"
}
}
}
Mutation for login
Let’s now log in with the user details we just created:
mutation {
login(email:"ekpot@gmail.com" password:"wizzyekpot"){
token
}
}
We should get something like this:
{
"data": {
"login": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsImVtYWlsIjoiZWtwb3RAZ21haWwuY29tIiwiaWF0IjoxNTk5MjQwMzcwLCJleHAiOjE1OTkzMjY3NzB9.PDiBKyq58nWxlgTOQYzbtKJ-HkzxemVppLA5nBdm4nc"
}
}
}
Awesome!
Query for a single user
For us to query a single user, we need to pass the user token as authorization header. Go to the HTTP Headers tab.
…and paste this:
{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsImVtYWlsIjoiZWtwb3RAZ21haWwuY29tIiwiaWF0IjoxNTk5MjQwMzcwLCJleHAiOjE1OTkzMjY3NzB9.PDiBKyq58nWxlgTOQYzbtKJ-HkzxemVppLA5nBdm4nc"
}
Here’s the query:
query myself{
me {
id
email
username
}
}
And we should get something like this:
{
"data": {
"me": {
"id": 15,
"email": "ekpot@gmail.com",
"username": "Wizzy"
}
}
}
Great! Let’s now get a user by ID:
query singleUser{
user(id:15){
id
email
username
}
}
And here’s the query to get all users:
{
allUsers{
id
username
email
}
}
Summary
Authentication is one of the toughest tasks when it comes to building websites that require it. GraphQL enabled us to build an entire Authentication API with just one endpoint. Sequelize ORM makes creating relationships with our SQL database so easy, we barely had to worry about our models. It’s also remarkable that we didn’t require a HTTP server library (like Express) and use Apollo GraphQL as middleware. Apollo Server 2, now enables us to create our own library-independent GraphQL servers!
Check out the source code for this tutorial on GitHub.
The post Let’s Create Our Own Authentication API with Nodejs and GraphQL appeared first on CSS-Tricks.
You can support CSS-Tricks by being an MVP Supporter.