Web Authentication (Example in Node JS)

Parsa Hejabi

I'm Parsa Hejabi 👋🏻

  • I was Software Engineer at Digikala for one year
  • Studying Computer Engineering
  • Full-Stack developer

Special Thanks to Randall Degges

@rdegges

Path
and
journey...

Terminologies

Auth

Auth

0x01

Authentication

Authorization

VS

Authentication

Authentication is the act of validating that users are who they claim to be. Passwords are the most common authentication factor. if a user enters the correct password, the system assumes the identity is valid and grants access. Biometrics, pins, ...

Authorization

Authorization in system security is the process of giving the user permission to access a specific resource or function. Giving someone permission to download a particular file on a server or providing individual users with administrative access to an application are good examples.

HTTP

0x02

Hypertext Transfer Protocol

HTTP is stateless

HTTP is a "stateless" protocol which means each time a client retrieves a Webpage, the client opens a separate connection to the Web server and the server automatically does not keep any record of previous client request.

REGISTER

LOGIN

DASHBOARD

0x00 -
Setup Project

Prerequisites

$ npm init -y
$ npm install express
$ npm install pug
  • Make a new npm package
  • Install dependencies
$ mkdir views
$ touch server.js
$ touch views/base.pug
$ touch views/index.pug
$ touch views/register.pug
$ touch views/login.pug
$ touch views/dashboard.pug

Prepare the application

block vars
doctype html
html
  head
    title SS-Auth | #{title}
  body
    block body
-> views/base.pug
<!DOCTYPE html>
<html>
  <head>
    <title>SS-Auth | </title>
  </head>
  <body>
  </body>
</html>
extends base

block vars
  - var title = "Home"

block body
  h1 SS-Auth!
  p.
    Welcome to the SS-Auth! Please #[a(href="/register") register] or #[a(href="/login") login] to continue.
-> views/index.pug
extends base

block vars
  - var title = "Register"

block body
  h1 Create an Account
  form(method="post")
    span First Name:
    input(type="text", name="firstName", required=true)
    br

    span Last Name:
    input(type="text", name="lastName", required=true)
    br

    span Email:
    input(type="email", name="email", required=true)
    br

    span Password:
    input(type="password", name="password", required=true)
    br

    input(type="submit")
-> views/register.pug
extends base

block vars
    - var title = "Login"

block body
    h1 Log Into Your Account

    if error
        p ERROR: #{error}
    
    form(method="post")
        span Email:
        input(type="email", name="email", required=true)
        br

        span Password:
        input(type="password", name="password", required=true)
        br

        input(type="submit")
-> views/login.pug
extends base

block vars
    - var title = "Dashboard"

block body
    h1 Dashboard
    p. 
        Welcome to your dashboard! You are now logged in.
-> views/dashboard.pug

Create Basic Web App with Express

  • Define routes
  • Render templates
  • Start server
const express = require("express");

let app = express();

app.set("view engine", "pug");

app.get("/", (req, res) => {
  res.render("index");
});

app.get("/register", (req, res) => {
  res.render("register");
});

app.get("/login", (req, res) => {
  res.render("login");
});

app.get("/dashboard", (req, res) => {
  res.render("dashboard");
});

app.listen(3000);
-> server.js

DEMO

0x01 -
Forms

HTML!

<form method="post">
  First Name: <input type="text" name="firstName" required/>
  Last Name: <input type="text" name="lastName" required/>
  Email: <input type="email" name="email" required/>
  Password: <input type="password" name="password" required/>
  <input type="submit"/>
</form>

HTML!

<form method="post">
  First Name: <input type="text" name="firstName" required/>
  Last Name: <input type="text" name="lastName" required/>
  Email: <input type="email" name="email" required/>
  Password: <input type="password" name="password" required/>
  <input type="submit"/>
</form>

Grab form data

$ npm install body-parser
// server.js
const bodyParser = require("body-parser");

app.use(bodyParser.urlencoded({
  extended: false
}));

app.post("/register", (req, res) => {
  res.json(req.body);
});
{
  "firstName": "Parsa",
  "lastName": "Hejabi",
  "email": "parsahejabi@parsahejabi.com",
  "password": "longlongstring"
}

0x02-
Database

Mongo

$ mongo
MongoDB shell version v4.2.0

Mongo vs SQL terms

Mongo Basics

> use test;
switched to db test
> show collections;
> db.users.insert({ email: "parsahejabi@parsahejabi.com" });
WriteResult({ "nInserted" : 1 })
> db.users.find()
{ "_id" : ObjectId("5f21e7536902e4210694c7bf"), "email" : "parsahejabi@parsahejabi.com" }

Mongo Basics

> use test;
switched to db test
> show collections;
> db.users.insert({ email: "parsahejabi@parsahejabi.com" });
WriteResult({ "nInserted" : 1 })
> db.users.find()
{ "_id" : ObjectId("5f21e7536902e4210694c7bf"), "email" : "parsahejabi@parsahejabi.com" }

Mongoose ORM

$ npm install mongoose
// server.js
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/test', {useNewUrlParser: true, useUnifiedTopology: true});

Mongoose model

// name, schema, collection
let User = mongoose.model(
  "User",
  new mongoose.Schema({
    firstName: { type: String, required: true },
    lastName: { type: String, required: true },
    email: { type: String, required: true, unique: true },
    password: { type: String, required: true },
  }),
  "User"
);

Creating users

app.post("/register", (req, res) => {
  let user = new User(req.body);
  
  user.save((err) => {
    if (err) {
      let error = "Something bad happened! Please try again.";
      
      if (err.code === 11000) {
        error = "That email is already taken, please try another.";
      }
      
      return res.render("register", { error: error });
    }
    
    res.redirect("/dashboard");
  });
});

Creating users

app.post("/register", (req, res) => {
  let user = new User(req.body);
  
  user.save((err) => {
    if (err) {
      let error = "Something bad happened! Please try again.";
      
      if (err.code === 11000) {
        error = "That email is already taken, please try another.";
      }
      
      return res.render("register", { error: error });
    }
    
    res.redirect("/dashboard");
  });
});
{
  "firstName": "Parsa",
  "lastName": "Hejabi",
  "email": "parsahejabi@parsahejabi.com",
  "password": "longlongstring"
}

Check the Database

> db.users.find()
{
  "_id" : ObjectId("5f21f14131eba444d3f7fb60"),
  "firstName" : "Parsa",
  "LastName" : "Hejabi",
  "email" : "parsahejabi@parsahejabi.com",
  "password" : "longlongstring",
  "__v" : 0
}

Authenticating Users!

app.post("/login", (req, res) => {
  User.findOne({ email: req.body.email }, (err, user) => {
    if (err || !user || req.body.password !== user.password) {
      return res.render("login", {
        error: "Incorrect email / password."
      });
    }
    
    res.redirect("/dashboard");
  });
});

0x03 -
Sessions

DASHBOARD

ACCOUNT

HOME

ACCOUNT

HOME

DASHBOARD

LOGIN

What is a Session?

Browser

Website

Login

Unique ID

Hey, browser! Remember this Unique ID and for the rest of the times send me back this so I know who you are!

Cookies?

Browser

Server

Authenticate

Cookies

Cookies

HTTP Requests & Responses

{
  "User-Agent": "Mozilla/5.0 ...",
  "Accept": "text/html"
  "Connection": "...",
  "Keep-Alive": ""
}
<html>
...
</html>

Headers

Body

Creating Cookies

{
  "Set-Cookie": "session=12345"
}

Body

Response

Creating multiple Cookies

{
  "Set-Cookie": "alef=b; b=jim;"
}

Body

Reading Cookies

{
  "Accept": text/html,
  "Accept-Language": en-US,
  "Cookie": "session=12345"
}

Body

client-sessions package

$ npm install client-sessions
const sessions = require("client-sessions");

app.use(sessions({
  cookieName: "myOwnSession",
  secret: "PrivateLongString",
  duration: 30 * 60 * 1000, // 30 minutes
}));
const sessions = require("client-sessions");

app.use(sessions({
  cookieName: "anotherSessionForMe",
  secret: "PrivateLongString",
  duration: 30 * 60 * 1000, // 30 minutes
}));

client-sessions package

Using Sessions

app.post("/login", (req, res) => {
  User.findOne({ email: req.body.email }, (err, user) => {
    if (err || !user || req.body.password !== user.password) {
      return res.render("login", {
        error: "Incorrect email / password."
      });
    }
    
    req.session.userId = user._id;
    res.redirect("/dashboard");
  });
});

Demo

Dashboard page!

app.get("/dashboard", (req, res, next) => {
  if (!(req.session && req.session.userId)) {
    return res.redirect("/login");
  }
  
  User.findById(req.session.userId, (err, user) => {
    if (err) {
      return next(err);
    }
    
    if (!user) {
      return res.redirect("/login");
    }
    
    res.render("dashboard");
  });
});

0x04 -
Storing
Passwords

> db.users.find()
{
  "_id" : ObjectId("5f21f14131eba444d3f7fb60"),
  "firstName" : "Parsa",
  "LastName" : "Hejabi",
  "email" : "parsahejabi@parsahejabi.com",
  "password" : "longlongstring",
  "__v" : 0
}

Password hashing!

Hash

function

Password

LongRandomStringlkjalsdkflhaf

Password Hashing

  • The same password always generates the same hash.
    (Forget salting for now...)
  • Given a hash, you can never retrieve the original password that created it. Hashes are one-way functions!

Password hashing algorithms

  • md5
  • sha256
  • bcrypt
  • scrypt
  • argon2
  • ...

Password hashing algorithms

  • md5
  • sha256
  • bcrypt
  • scrypt
  • argon2
  • ...
  • SUCKS!
  • NO!
  • OK
  • AWESOME
  • 5 YEARS
  • NOPE!

Test bcrypt

Improving Registration

$ npm install bcryptjs
app.post("/register", (req, res) => {
  let hash = bcrypt.hashSync(req.body.password, 14);
  req.body.pashword = hash;
  let user = new User(req.body);
  
  user.save((err) => {
    // ...
  });
});

Improving Registration

app.post("/register", (req, res) => {
  let hash = bcrypt.hashSync(req.body.password, 14);
  req.body.pashword = hash;
  let user = new User(req.body);
  
  user.save((err) => {
    // ...
  });
});

Improving Login

app.post("/login", (req, res) => {
  User.findOne({ email: req.body.email }, (err, user) => {
    if (!user || !bcrypt.compareSync(req.body.password, user.password)) {
      return res.render("login", {
        error: "Incorrect email / password."
      });
    }
    
    req.session.userId = user._id;
    res.redirect("/dashboard");
  });
});

Improving Login

app.post("/login", (req, res) => {
  User.findOne({ email: req.body.email }, (err, user) => {
    if (!user || !bcrypt.compareSync(req.body.password, user.password)) {
      return res.render("login", {
        error: "Incorrect email / password."
      });
    }
    
    req.session.userId = user._id;
    res.redirect("/dashboard");
  });
});

Check the Database

> db.users.find()
{
  "_id" : ObjectId("5f21f14131eba444d3f7fb60"),
  "firstName" : "Parsa",
  "LastName" : "Hejabi",
  "email" : "parsahejabi@parsahejabi.com",
  "password" : "$2a$14$t5v8pnwlYu2ncSbQWXJ1WORV0n.HQfZ.HccYXFEzmSf.spaRWlv0y",
  "__v" : 0
}

Check the Database

> db.users.find()
{
  "_id" : ObjectId("5f21f14131eba444d3f7fb60"),
  "firstName" : "Parsa",
  "LastName" : "Hejabi",
  "email" : "parsahejabi@parsahejabi.com",
  "password" : "$2a$14$t5v8pnwlYu2ncSbQWXJ1WORV0n.HQfZ.HccYXFEzmSf.spaRWlv0y",
  "__v" : 0
}

Having fun!!! 🤦🏻‍♂️

0x05 -
Refactor

Smart User Middleware

app.use((req, res, next) => {
  if (!(req.session && req.session.userId)) {
    return next();
  }
  
  User.findById(req.session.userId, (err, user) => {
    if (err) {
      return next(err);
    }
    
    if (!user) {
      return next();
    }
    
    user.password = undefined;
    
    req.uesr = user;
    res.locals.user = user;
    
    next();
  })
})

Smart User Middleware

app.use((req, res, next) => {
  if (!(req.session && req.session.userId)) {
    return next();
  }
  
  User.findById(req.session.userId, (err, user) => {
    if (err) {
      return next(err);
    }
    
    if (!user) {
      return next();
    }
    
    user.password = undefined;
    
    req.uesr = user;
    res.locals.user = user;
    
    next();
  })
})

Smart User Middleware

app.use((req, res, next) => {
  if (!(req.session && req.session.userId)) {
    return next();
  }
  
  User.findById(req.session.userId, (err, user) => {
    if (err) {
      return next(err);
    }
    
    if (!user) {
      return next();
    }
    
    user.password = undefined;
    
    req.uesr = user;
    res.locals.user = user;
    
    next();
  })
})

Force Authentication (Authorization :))

function loginRequired(req, res, next) {
  if (!req.user) {
    return res.redirect("/login");
  }
  
  next();
}

app.get("/dashboard", loginRequired, (req, res, next) => {
  //...
});

Before

After

function loginRequired(req, res, next) {
  if (!req.user) {
    return res.redirect("/login");
  }
  
  next();
}

app.get("/dashboard", loginRequired, (req, res, next) => {
  //...
});
app.get("/dashboard", (req, res, next) => {
  if (!(req.session && req.session.userId)) {
    return res.redirect("/login");
  }
  
  User.findById(req.session.userId, (err, user) => {
    if (err) {
      return next(err);
    }
    
    if (!user) {
      return res.redirect("/login");
    }
    
    res.render("dashboard");
  });
});

Before

After

function loginRequired(req, res, next) {
  if (!req.user) {
    return res.redirect("/login");
  }
  
  next();
}

app.get("/dashboard", loginRequired, (req, res, next) => {
  //...
});
app.get("/dashboard", (req, res, next) => {
  if (!(req.session && req.session.userId)) {
    return res.redirect("/login");
  }
  
  User.findById(req.session.userId, (err, user) => {
    if (err) {
      return next(err);
    }
    
    if (!user) {
      return res.redirect("/login");
    }
    
    res.render("dashboard");
  });
});

Before

After

0x06 -
CSRF

CSRF?! 🤔

Cross-Site Request Forgery

???

<!-- bank.com/bardasht -->
<form>
  <input type="text" name="account"/>
  <input type="text" name="amount"/>
  <input type="text" name="for"/>
</form>

CSRF

😈

New Email!

Yo Parsa,

Check out my new طوطی.

He can do the روپایی thing.

<a href="http://bank.com/bardasht?account=Parsa&amp;amount=100000000&amp;for=Dozd">
  <img src=tooti.png>
</a>

CSRF Tokens

Browser

Server

CSRF Cookie

CSRF Tokens

Browser

Server

CSRF Cookie

GET /login

CSRF Tokens

Browser

Server

CSRF Cookie

GET /login

CSRF Tokens

Browser

Server

CSRF Cookie

GET /login
Login Page with CSRF Token

CSRF Tokens

Browser

Server

CSRF Cookie

GET /login
Login Page with CSRF Token
POST /login with
CSRF Token

CSRF Tokens

Browser

Server

CSRF Cookie

GET /login
Login Page with CSRF Token
POST /login with
CSRF Token
they are NOT the same
NO!

HTML(pug) solution!

form(method="post")
  input(type="hidden", name="_csrf", value=csrfToken)

CSRF Protection

$ npm install csurf
const csurf = require("csurf");

app.use(csurf());

app.get("/register", (req, res) => {
  res.render("register", { csrfToken: req.csrfToken() });
});

app.get("/login", (req, res) => {
  res.render("login", { csrfToken: req.csrfToken() });
})

// Every place you have a form and call render!

CSRF Protection

const csurf = require("csurf");

app.use(csurf());

app.get("/register", (req, res) => {
  res.render("register", { csrfToken: req.csrfToken() });
});

app.get("/login", (req, res) => {
  res.render("login", { csrfToken: req.csrfToken() });
})

// Every place you have a form and call render!

0x07 -
Best
Practices

SSL! 🔐

Browser

Server

Secret

Cookie Flags

const sessions = require("client-sessions");

app.use(sessions({
  cookieName: "myOwnSession",
  secret: "PrivateLongString",
  duration: 30 * 60 * 1000, // 30 minutes
}));

Cookie Flags

app.use(sessions({
  cookieName: "myOwnSession",
  secret: "PrivateLongString",
  duration: 30 * 60 * 1000, // 30 minutes
  activeDuration: 5 * 60 * 1000,
  httpOnly: true, // JS code does not access cookies
  secure: true, // only over https
  ephermal: true // destroy cookies when the browser closes
}));

Use Helmet

$ npm install helmet
// Manages http header security for your application
const helmet = require("helmet");
app.use(helmet());

Other useful packages
Use them! 👍

  • Passport
  • Aqua
  • Okta
  • Node-login

Thank you! 👋

Feel free to contact me on Twitter or my website!

Authentication

By Parsa Hejabi

Authentication

These slides were used for a 1-hour meeting about authentication on 30th July 2020.

  • 705