I don't think it's necessary to explain why we need to have an authentication system in an application at all. You've probably heard of the terms authentication
and authorization
and I have to point out that these words have different meanings.
"Authentication is the act of validating that users are whom they claim to be. This is the first step in any security process. " Okta.com
"Authorization in system security is the process of giving the user permission to access a specific resource or function. This term is often used interchangeably with access control or client privilege." Okta.com
In this tutorial we will learn how to make an authentication system using JWT.
Database models
We will first have to deal with the database because we need to store user data somewhere. We need to store email and hashed password which will be used later for the sign in process. For this tutorial we will use NoSQL MongoDB database and we will also use mongoose. Mongoose is a MongoDB object modeling tool which is designed to work in an asynchronous environment and supports both promises and callbacks.
We will install the necessary packages:
npm install --save mongoose
npm install --save-dev @types/mongoose
After the packages are installed, we can start making the model. We will create a model for the user who will have the fields _id, email, name and password. We will also create a unique email index so that there are no two users with the same email in our database.
import { model, Model, Schema } from 'mongoose';
export interface IUser {
_id: string;
email: string;
password: string;
name: string;
}
const IUserSchema = new Schema<IUser>(
{
_id: { type: String, required: true },
email: {
type: String,
required: true,
lowercase: true,
index: true,
unique: true,
},
name: { type: String, required: true },
password: { type: String, required: true },
},
{ collection: 'user', timestamps: true }
);
export const UserModel: Model<IUser> = model('user', IUserSchema);
Now lets create a connection to the MongoDB database via mongoose.
Note: We need to have a MongoDB database running in order to connect to it. If you use docker you can find the
docker-compose.yml
file on github which link is provided in this tutorial and just rundocker-compose up -d
.
import mongoose, { Connection } from 'mongoose';
let mongooseConnection: Connection = null;
export async function connect(): Promise<void> {
try {
mongoose.connection.on('connecting', () => {
console.log(`MongoDB: connecting.`);
});
mongoose.connection.on('connected', () => {
console.log('MongoDB: connected.');
});
mongoose.connection.on('disconnecting', () => {
console.log('MongoDB: disconnecting.');
});
mongoose.connection.on('disconnected', () => {
console.log('MongoDB: disconnected.');
});
if (mongoose.connection.readyState !== 1 && mongoose.connection.readyState !== 2) {
const conn = await mongoose.connect('mongodb://localhost:27017/ts-tutorial', { // <- replace connection string if necessary
autoIndex: true,
serverSelectionTimeoutMS: 5000,
});
mongooseConnection = conn.connection;
}
} catch (error) {
console.log(`Error connecting to DB`, error);
}
}
Now in the server.ts
file we can call the method for connecting to the database:
connect();
If the application is successfully connected to the database then we should get the messages from log:
MongoDB: connecting.
Application started on port 3000!
MongoDB: connected
Sign up process
We will first create an endpoint to which we will send data to create a new user. We will add the new route in the server.ts
file. Email, name and password fields are required (we will not do the validation of parameters). After that, we must first check if there is an existing user with the same email and only after we determine that the user does not exist, can we proceed further.
The next step is to make a hash of the plain password because the plain password is never stored in the database. So when we create a new user we take his plain password, make a hash and keep the hash in the database. We will need the hashed password later for the sign in process.
Required npm packages:
npm install --save ulid
npm install --save bcrypt
npm install --save-dev @types/bcrypt
app.post('/sign-up', async (req: Request, res: Response, next: NextFunction) => {
const { email, name, password } = req.body;
// check if user exists
const userExists = await UserModel.findOne({ email: email });
if (!!userExists) {
next(new ErrorException(ErrorCode.DuplicateEntityError, { email }));
}
// generate password hash
const hash = passwordHash(password);
const newUser: IUser = {
_id: ulid(),
email,
name,
password: hash,
};
const created = await UserModel.create(newUser);
res.send({ done: true });
});
Note: Add the code in
server.ts
file before the routes for encoding the data that is being sent to our application from the client side.
const app = express();
app.use(
express.urlencoded({
extended: true,
})
);
app.use(express.json());
We used the bcrypt library to create a hash from a plain password. The code for hashing and comparing plain and hashed passwords:
import bcrypt from 'bcrypt';
export const passwordHash = (plainPassword: string): string => {
const hash = bcrypt.hashSync(plainPassword, 10);
return hash;
};
export const comparePassword = (plainPassword: string, passwordHash: string): boolean => {
const compared = bcrypt.compareSync(plainPassword, passwordHash);
return compared;
};
In the code above, you can see that we have two functions. The passwordHash
function will hash a plain password.
The comparePassword
function will check that the plain password entered is the same as the hash from the database. We will need this method later for the login form.
If we have successfully created a user in the database, the next step is to create a JWT when the user tries to sign in.
Sign in process
As we said in the introduction, we will use the jsonwebtoken package and for that we need to install the packages:
npm install --save jsonwebtoken
npm install --save-dev @types/jsonwebtoken
Actually how does it work? It is necessary to create a route for sign in where it will be necessary to enter email and password.
We will first check if there is a user with the provided email and if there is one, then we will take the password hash that is saved in the database. It is necessary to check whether the plain password from the login form agrees with the hash password from the database using the comparePassword
method. If the method returns true then the user has entered a good password, otherwise the method will return false.
After that, it is necessary to generate jsonwebtoken through the mentioned library. We will generate the JWT with the help of a secret key which we keep in our application and the client should not be aware of the secret key. We will generate that jsonwebtoken string and return that token to the client application.
app.post('/sign-in', async (req: Request, res: Response, next: NextFunction) => {
const { email, password } = req.body;
// check if user exists
const userExists = await UserModel.findOne({ email: email });
if (!userExists) {
next(new ErrorException(ErrorCode.Unauthenticated));
}
// validate the password
const validPassword = comparePassword(password, userExists.password);
if (!validPassword) {
next(new ErrorException(ErrorCode.Unauthenticated));
}
// generate the token
const token = generateAuthToken(userExists);
res.send({ token });
});
Code for JWT helper:
import { IUser } from '../models/db/user.db';
import jwt from 'jsonwebtoken';
import { ErrorException } from '../error-handler/error-exception';
import { ErrorCode } from '../error-handler/error-code';
const jwtKey = 'keyyyy';
export const generateAuthToken = (user: IUser): string => {
const token = jwt.sign({ _id: user._id, email: user.email }, jwtKey, {
expiresIn: '2h',
});
return token;
};
export const verifyToken = (token: string): { _id: string; email: string } => {
try {
const tokenData = jwt.verify(token, jwtKey);
return tokenData as { _id: string; email: string };
} catch (error) {
throw new ErrorException(ErrorCode.Unauthenticated);
}
};
Authentication middleware
We will create one middleware called authMiddleware
which we will put on the routes where we need to have protection and whose job will be to check if the JWT that was generated is valid. authMiddleware
function is just a middleware function which will get a token from the header and check its validation. We can check the validation of the token with the function verifyToken
which is placed inside our middleware.
The client side is required to send the JWT token string in the header for each API call that requires authentication. Header with authorization token looks like:
Authorization: Bearer eyJhbGciOiJIUzI1NiIXVCJ9TJV...r7E20RMHrHDcEfxjoYZgeFONFh7HgQ
Protected route with middleware:
app.get('/protected-route', authMiddleware, (req: Request, res: Response, next: NextFunction) => {
// data from the token that is verified
const tokenData = req.body.tokenData;
console.log('tokenData', tokenData);
res.send('this is a protected route');
});
The middleware itself:
import { Request, Response, NextFunction } from 'express';
import { ErrorCode } from '../error-handler/error-code';
import { ErrorException } from '../error-handler/error-exception';
import { verifyToken } from './jwt';
export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
const auth = req.headers.authorization;
if (auth && auth.startsWith('Bearer')) {
const token = auth.slice(7);
try {
const tokenData = verifyToken(token);
req.body.tokenData = tokenData;
next();
} catch (error) {
throw new ErrorException(ErrorCode.Unauthenticated);
}
} else {
throw new ErrorException(ErrorCode.Unauthenticated);
}
};
Wrapping up
In this tutorial we covered how to create basic models with mongoose
and MongoDB
and how to connect to MongoDB instances. We also learned how to create a new user and save the user in the database and what is important, how to create a hash password using the bcrypt
library. After saving the user, we showed how to create a sign in process and generate a token using the jsonwebtoken
library. Finally, we demonstrated how to create one middleware to be placed on a route to protect certain routes.
Top comments (0)