LAB: how to do auth in your app?
Let's learn about authentication.
Authentication is the one of the most common and widely use in almost all web applications.
Authentication will be 2 methods mainly
- Register (Sign up)
- Login
Let's start:
Download this zip file and extract it in to a folder
https://drive.google.com/file/d/1IRCFkS_xvYXZ90SgXQeYt7a4QRHZP-5I/view?usp=sharing
We are working in the backend directory
In db/connect.js edit look like this :
create a directory name auth in /src/controller/
Let's create a required services first
create jwt.js
file in /controller/auth/
:
import jwt from 'jsonwebtoken';
const JWTSecretKey = 'wuuuuuuuuuuuw';
//move this value to .env and import it here instead
// also Change it to your student ID
export const generateToken = (id) => {
const token = jwt.sign(
{
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24,
userId: id,
},
JWTSecretKey
);
return token;
};
export const validateToken = (token) => {
const id = jwt.verify(token, JWTSecretKey);
return id.userId;
};
export const checkauth = (req, res, next) => {
try {
const token = req.cookies.token;
if (token) {
if (!jwt.verify(token, JWTSecretKey)) {
throw err;
}
} else {
throw err;
}
next();
} catch (err) {
console.error(err);
return res
.status(401)
.json({ success: false, message: 'Invalid token' });
}
};
We create these function to handle the authentication methods
Now we can deny access to user only routes by using checkauth():
import and use checkauth() from /src/controller/auth/jwt.js and use it above all of the paths we want to restrict
Next let's start the real authentication functions start with register method
create new register.js file in /src/controller/auth/
import db from '../../db/connect.js';
import { v4 as uuidv4 } from 'uuid';
import bcrypt from 'bcrypt';
export const register = async (req, res, next) => {
try {
const username = req.body.username;
const salt = 10; // Move this to .env and import it here instead
const password = bcrypt.hashSync(req.body.password, salt);
const firstname = req.body.firstname;
const lastname = req.body.lastname;
const email = req.body.email;
const existingUser = await db
.promise()
.query('SELECT * FROM `users` WHERE username = ?', [username]);
// If the username exists, throw an error
if (existingUser[0].length > 0) {
return res
.status(400)
.json({ success: false, payload: 'Username already exists' });
}
const credit_ID = uuidv4();
const data = [
[username, password, firstname, lastname, email, credit_ID],
];
await db
.promise()
.query(
'INSERT INTO `users` (`username`, `password`, `firstname`, `lastname`, `email`,`credit_ID`) VALUES ?',
[data]
);
return res
.status(200)
.json({ success: true, payload: 'register successful' });
} catch (error) {
return res.status(400).json({ success: false, payload: error.message });
}
};
Here as you can see the password has been hashed with bcrypt (node dependency for encoding data) bcrypt.hash() is a function to encode a data with salt. We are hashing the password because the possibility that the database can be leaked. This also ensure user that even the developer will not know users password.
Now we register next let's go to login
create another file in the same directory as register.js name login.js
this file will handle the login function
import db from '../../db/connect.js';
import bcrypt from 'bcrypt';
import { generateToken } from './jwt.js';
export const login = (req, res, next) => {
const username = req.body.username;
const password = req.body.password;
console.log(username);
try {
db.query(
'SELECT id ,password FROM `users` WHERE username = ?',
[username],
async (err, user) => {
if (err) {
throw err;
} else {
if (user.length == 0) {
return res.status(400).send('username not found');
} else if (
bcrypt.compare(password, user[0].password) == false
) {
return res.status(400).send('password not correct');
} else {
const id = user[0].id;
const token = generateToken(id);
res.cookie('token', token);
return res.status(200).json({ token: token });
}
}
}
);
} catch (error) {
next();
return res.status(400).json({
payload: error,
});
}
};
as you see here we also use bcrypt here but now we use the compare() function. this function is use to compare normal string and hashed string that is it matched without using salt value.
now all of the function are all created let's create the router for these functions
create a file name auth.js in /router/ directory.
import express from 'express';
import { login } from '../controller/auth/login.js';
import { register } from '../controller/auth/register.js';
const authRouter = express();
authRouter.post('/login', login);
authRouter.post('/register', register);
export default authRouter;
then add the router in index.js file above the checkauth() call to bypass the checkauth() function
app.use('/auth', authRouter);
if you call this under checkauth it will use checkauth middleware before using the endpoint. and will not be able to access some of the endpoint because we don't have the token yet.
now let's move to other functionality
1. deposit function (/controller/bank/deposits.js)
before this class we used to define a userId as a static integer but if we have multiple users this will not work as expected anymore or if we send it by body there would be possibility of the request to leaked so we need to change it to get the userId from user token
import db from '../../db/connect.js';
import { validateToken } from '../auth/jwt.js';
import timeStamp from '../../utill/timeStamp.js';
export const getDeposit = (req, res) => {
const token = req.cookies.token;
const userId = validateToken(token);
// const userId = req.body.user_id;
db.query(
'SELECT * FROM `banks` WHERE (owner = ? && type = ?) ORDER BY date DESC',
[userId, 'disposit'],
(err, re) => {
if (err) {
res.status(400).json({
success: false,
data: null,
error: err.message,
});
} else {
return res.json({
success: true,
data: re,
error: null,
});
}
}
);
};
export const deposit = async (req, res, next) => {
try {
// const user_id = req.body.user_id;
const token = req.cookies.token;
const userId = validateToken(token);
const receiver = null;
const note = req.body?.note;
const amount = req.body.amount;
const bank = req.body.bank;
// eslint-disable-next-line no-undef
const user = await new Promise((resolve, reject) => {
db.query(
'SELECT balance, firstname FROM `users` WHERE id = ?',
[userId],
(err, result) => {
if (err) {
reject(err);
} else {
resolve(result[0]);
}
}
);
});
const data = [
[
userId,
user.firstname,
receiver,
note,
amount,
bank,
'deposit',
timeStamp(),
],
];
db.query(
'INSERT INTO banks (`owner`, `sender`, `receiver`,`note`, `amount`, `bank`, `type`, `date`) VALUES ?',
[data],
(err, result) => {
if (err) {
console.log(err);
throw err;
} else {
db.query('UPDATE users SET balance = ? WHERE id = ?', [
user.balance + amount,
userId,
(err) => {
if (err) {
console.log(err);
throw err;
}
},
]);
return res.json({
success: true,
data: 'deposit success',
});
}
}
);
} catch (error) {
res.status(400).json({ error });
return next();
}
};
Also change this userId variables in all of the files (/controller/bank/deposits.js
, transfer.js
, withdraw.js
and /controller/user/getUser.js
, getBalance.js
)
After you are done. Please write in the docs.md
to explain each of these function functionality in about 20 - 150 words each
/controller/auth/jwt.js
->generateToken()
,validateToken()
andcheckauth()
/controller/auth/login.js
->login()
/controller/auth/register.js
->register()
Submit the Lab and Excercise in gitlab by create new 2 repository under your user.
- The lab project name <student id>_backend_week3 (The lab we do today)
- The exercise project <student id>_bank_project_auth (The bank project we do today)
Reminder: there will be extra point to student who move the sensitive data to .env