DEV Community

Cover image for Login and Sign up Auth using JWT
Vishal Shinde
Vishal Shinde

Posted on

Login and Sign up Auth using JWT

This is a part of my whole Journaling Web Project which I am building to gain experience in NodeJS and MongoDB. I have used NodeJS, Express, Mongoose and EJS(Embedded JavaScript)

We install required packages using npm by the following command.

npm i express bcrypt dotenv mongoose cookie-parser ejs jsonwebtoken

Then we install nodemon as a Dev Dependency for keeping our server live after every save

npm i --save-dev nodemon
In your package.json file,you change your scripts to this
"dev": "nodemon app.js"
On the command line you just type npm run dev and your server will run live.

We write Routes for Sign up, Login, Logout and Home Page.

1. Creating a User Schema using Mongoose

  • Make a directory called "models" and keep user.js in it.
// user.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
    email : {
        type : String,
        required : true,
        unique : true,
        validate : {
            validator : async function (value) {
                const user = await this.constructor.findOne({email : value})
                return !user;
            },
            message : 'Email already exists',
        },
    },
    password : {
        type : String,
        required : true,
        }
    })
    module.exports = mongoose.model("User", userSchema)
Enter fullscreen mode Exit fullscreen mode
  • I have taken a Email Validation which will check during Sign up if the Email is already taken. All the required options for Fields is written.
  • We create a model of our User Schema and export user.js to use it in routes. We can import user.js using this line const User = require('../models/user.js');

2. Writing the Signup Route.

Make directory by name routes and create login.js and signup.js files.
First we import all the required packages.

const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const User = require('../models/user.js');
Enter fullscreen mode Exit fullscreen mode

We write two requests for our sign up route.
GET and POST.

router.get('/', (req, res) => {
  res.render('signup', { title: 'Sign Up', emailError: null, passError : null });
});
Enter fullscreen mode Exit fullscreen mode

router.post('/', async (req, res) => {
  const { email, password } = req.body;
  try {
    const existingUser = await User.findOne({ email });
    if (existingUser) {
      return res.render('signup', {
        emailError: 'Email already exists',
        title: 'Sign Up',
        passError : null
      }) }
    if(password.length < 8) {
      return res.render('signup', {
        title: 'Sign Up',
        passError: 'Password must be at least 8 characters long',
        emailError : null
      })
    }
    const salt = await bcrypt.genSalt(10);
    const hashedPassword = await bcrypt.hash(password, salt);
    const newUser = new User({ email, password: hashedPassword });
    await newUser.save();
    res.redirect('/login')
    // res.send('Welcome to Journalling');
  } catch (error) {
    console.error(error);
    res.redirect('/signup');
  }
});
Enter fullscreen mode Exit fullscreen mode
  • As we can see in router.get method, we are rendering our view using ejs which is created in views/signup.ejs. here we write our views and render it. We can tell our Server to use view Engines using this two lines which I have written in app.js

app.set('view engine', 'ejs');

  • A view engine is responsible for rendering dynamic templates and generating HTML to be sent as a response to the client. EJS is one of the popular view engines available for Express.js.

app.use(express.static('public'));

  • The line app.use(express.static('public')); is a middleware function in an Express.js application that serves static files from the "public" directory.

Here is my signup.ejs

<!DOCTYPE html>
<html>
<head>
  <title>Journalling | <%= title %></title>
  <link rel="stylesheet" type="text/css" href="/styles.css">
</head>
<body>
  <div class="container">
    <h1>Sign up</h1>
    <form action="/signup" method="POST">
      <label for="email">Email</label>
      <input type="email" id="email" name="email" required placeholder="Email">
      <% if (emailError) { %>
        <p class="error-message"><%= emailError %></p>
        <% } %>

        <% if (passError) { %>
          <p class="error-message"><%= passError %></p>
          <% } %>
      <label for="password">Password</label>
      <input type="password" id="password" name="password" required>
      <button type="submit">Sign up</button>
    </form>
  </div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode
  • We have designed a form with POST request it means whatever we put in fields will send as req.body.
  • I have used emailError and passError variables for Email and Password validation. emailError to check if Email already exists and passError to take atleast 8 characters as a password.

  • <% %> is for any control flow, and it does not give any output display to the page.

  • <%= %> gives output to the page in this case I have written passErrror inside a paragraph tag so whenever a user will put less than 8 characters of password, they will get a error message. same applies for the Email.

  • Let's move on to the post request function body.
    We take email and password as our req.body to save it in our database.
    I wrote the logic for Email and Password Validation
    We will check for Existing user by User.findOne({}). We have annotated await keyword to handle asynchronous behavior.
    then put a if-condition to check whether existingUser is true then we will render the same signup with a error message. Similar logic applies for password, if the password length is less than 8, it will render the same page with an error message.

  • We installed bcrypt in the start, it is used for password hashing, it is a good practice to store password in a hashed format to protect users from attackers.
    then we add salt to our password.
    A salt is a random value that is added to the password before hashing. The purpose of the salt is to introduce additional randomness and uniqueness to each hashed password.
    We store the credentials to our Database and redirect the user to login to access the protected routes.


3. Writing the Login Route.

  • The Basic idea is, we will be comparing the passwords of the user and password inputted during login and put a validation to check if email exists in our database or not, if yes, display an error message in the same way I did in the signup route.

  • Now enters JWT, which is used for Auth(Authenticataion and Authorization).
    You can learn about JWT at this links 1 2 3

  • The Decision to generate tokens during login or signup(or register) has its pros and cons. If we generate token During Signup, there is seamless login experience. and if we do it durign login, we could have enchanced security.
    Here is my login.js

const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const User = require('../models/user.js');
const jwt = require('jsonwebtoken')
require('dotenv').config();

router.get('/', (req, res) => {
  res.render('login', { title: 'Login', error : null });
});

router.post('/', async (req,res) => {
    const { email, password } = req.body;
    try {
        // Find the user by email
        const user = await User.findOne({email});

        // If user is not found, display an error message
        if(!user) {
            return res.render('login',{title : 'Login', error : 'Invalid Email or Password'})
        }
        // Compare the entered password with the stored hashed password
        const isPasswordValid = await bcrypt.compare(password, user.password);

        // if password is invalid, display an error message
        if (!isPasswordValid) {
            return res.render('login', { title: 'Login', error: 'Invalid email or password' });
        }
        token = jwt.sign({id: user._id,email:user.email},process.env.ACCESS_TOKEN, {expiresIn: '30m'})
        res.cookie('jwt',token, {httpOnly : true, secure : true});
        res.redirect('/dashboard');
    } catch (error) {
        console.error(error);
        res.redirect('/login');
    }
})
module.exports = router;

Enter fullscreen mode Exit fullscreen mode
  • You need a secret token which will used to generate your JWT Token. you can use crypto library on node console using this command require('crypto').randomBytes(64).toString('hex')
  • This will generate a random token which you will then put it in your .env file. and access it. Note: It is a secret key which can be used to generate your JWT token so it's crucial to keep it safe. If you will be uploading your repository to Github then don't forget to add .env file to your .gitignore file.
  • We generate JWT token by jwt.sign(), which will contain three parameters, the data we want to store, a secret key we talked earlier and a expiry(optional). We store this token in Client-side Browser and redirect to a protected route if and only if token is valid and exists.

4. Writing a auth middleware

To access the routes after logging, we have to setup a middleware function which will run everytime we request a protected resource.
This is the middleware function which check if token is valid and exists

const jwt = require('jsonwebtoken');
require('dotenv').config();

const authenticateToken= (req,res,next) => {
  const token = req.cookies.jwt;

  // check json exists and is verified
  if(token) {
      jwt.verify(token, process.env.ACCESS_TOKEN, (err, decodedToken) => {
          if(err) {
              console.log(err.message);
              res.status(403).redirect('/login')    
      // If the token is provided but invalid (e.g., expired or tampered with): Status code 403 (Forbidden) can be used. This indicates that the user is authenticated but does not have permission to access the requested resource. In this case, you can redirect the user to the login page or display an error message.
          } else {
              next();
            }
      })
  } else {
  // token is missing or not provided
  // If the token is missing or not provided: Status code 401 (Unauthorized) can be used. This indicates that the user is not authenticated and should be redirected to the login page.
      res.status(401).redirect('/login')
  }
}
module.exports = authenticateToken
Enter fullscreen mode Exit fullscreen mode
  • The token is extracted from the request cookies using req.cookies.jwt.
  • The code checks if the token exists. If it does, it proceeds to verify the token using jwt.verify. The jwt.verify function verifies the token's authenticity and decodes its contents.
  • If the token is successfully verified, the next() function is called to proceed to the next middleware or route handler. This indicates that the user is authenticated and has permission to access the requested resource.

5. Writing the Logout Route.

  • We cannot delete cookies from server-side directly, instead we created a blank cookie and set its expiry to 1ms or just 0(immediately).
  • From client-side(also Browser), we can remove cookies from Developer tools but that's not a good UX, so implemented a button which will delete cookies for us.
  • Deleting the cookies eventually revoking the token will log out the user out of their session. Here is the Code Snippet for logout.js
const router = require('express').Router();
router.get('/logout',(req,res) =>{
    res.cookie('jwt','',{maxAge : 1})
    res.redirect('/');
})
module.exports = router;
Enter fullscreen mode Exit fullscreen mode

6. Final Part.

  • We have all required Routes. Now we import it all in our main app.js which will run our entire application. Here is the app.js
  • Imported all the Required Packages for app.js
const express = require('express');
const mongoose = require('mongoose');
const cookieParser = require('cookie-parser')
Enter fullscreen mode Exit fullscreen mode

const app = express(); // Creates an instance of our Express Application
Enter fullscreen mode Exit fullscreen mode

  • The following contains all the Routes we wrote above
  • protectedRoute will execute if and only if auth middleware runs, so it ensure the user is logged in.
const loginRoute = require('./routes/login.js');
const signupRoute = require('./routes/signup.js');
const protectedRoute = require('./routes/dashboard.js')
const logoutRoute = require('./routes/logout.js');
Enter fullscreen mode Exit fullscreen mode

mongoose.connect('mongodb://localhost:27017/database_name'); // connect to our mongoDB Database using Mongoose
Enter fullscreen mode Exit fullscreen mode

  • Here, we use our middleware functions and required parsers like json, cookie.
  • urlencoded is used to parse URL-encoded data in the request body. This allows you to access the submitted form data in your route handlers using req.body. For example, if you have a form with an input field named email,you can access its value in a route handler as req.body.email.
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cookieParser())
// Register view engine
app.set('view engine', 'ejs');

// Static files
app.use(express.static('public'));

app.get('/', (req, res) => {
  res.render('index.ejs'); 
}); // this is our main/index route where our app starts. and we are rendering index.ejs(home page)

// Mount the login routes
app.use('/login', loginRoute);

// Mount the signup routes
app.use('/signup', signupRoute);

app.use('/',logoutRoute)
app.use('/dashboard',protectedRoute);
app.listen(3000); //  is used to start the Express application and make it listen for incoming HTTP requests on port 3000.
Enter fullscreen mode Exit fullscreen mode

PS : This is my First ever Tech Blog, there could by too many mistakes so Open for Suggestions. I am using this blog thing to take note of whatever I learnt during projects. I would be glad if someone could benefit from it.
Moreover, I haven't done refresh tokens here which is more better way to handle Tokens incase if a token ever gets stolen, I might edit it later on.

THANK YOU FOR READING!

Top comments (0)