Bootcamp Notes – Day 10 (Tues) – NodeJS: Week 3: Cookies and Express Sessions

Cookies and Express Sessions

 

Overview: Cookies and Express Sessions

  • In Basic Authentication, client must explicitly send username & password with every request to server for protected resource.
  • HTTP cookies enable the server to temporarily store information on client-side that the client can use for authentication after authenticating once with user/pass
  • Can also track more information about a user’s session – cookies are small and cannot be used for this, but we will use Express Sessions middleware library

COOKIES

  • HTTP cookies are small pieces of data sent from a web server and stored on the client side computer
  • Browsers include the cookie in the request header for requests to the server that originated the cookie
  • Think of it like showing your real ID to get an event pass at an event – then every time you need to show credentials after, you can show the event pass instead of your real ID.

HOW DO COOKIES WORK?

  • First time client authenticates with server, server sends a SET-COOKIE response header back to client that contains a cookie as its value, with optional expiration
  • Client stores cookie and includes it in the headers of subsequent requests to that server
  • When server receives request and sees valid cookie in header, then will grant access without asking for username/password

HOW DO WE USE COOKIES IN EXPRESS?

  • Express has built-in support for cookies in its Response object’s APi
  • res.cookie() – use to set up Set-Cookie response header
  • res.clearCookie() – delete a cookie
  • cookie-parser – middleware library to parse and sign cookies with a secrete key for encryption, increases security

SESSIONS

  • Cookies are of a small, fixed size, cannot hold information
  • Cookies are just used to remind server when an authenticated client sends a request
  • To track more information about a client, implement a server-side session tracking mechanism
  • The concept of sessions is generic, any server can implement a variation
  • Express uses a library called express-sessions
  • A session itself is a combination of a cookie + a session ID
  • Server tracks information associated with the session ID
  • Instead of passing that information back and forth between client and server, passes the cookie that hold the session ID
  • When a client sends a request, the session ID is retrieved from the cookie then used by the server to access/update tracking data on the server side & make decisions on how to respond.
  • By default, session information is stored in application memory – lost if server application needs to restart
  • Instead, many servers store session data in more permanent ways – local file storage or database
  • File storage can be done with the session-file-store middleware library
  • Keep in mind that a distributed server setup would need a distributed session storage setup.

Using Cookies

  • Set up your Express application to send signed cookies to the client upon successful authorization.
  • Set up your Express application to parse cookies in the header of incoming request messages.
  • The cookie-parser Express middleware NPM package is already included in the Express REST API application. You do not need to install it.
  • Update app.js as follows:
var createError = require(‘http-errors’);
var express = require(‘express’);
var path = require(‘path’);
var cookieParser = require(‘cookie-parser’);
var logger = require(‘morgan’);
var indexRouter = require(‘./routes/index’);
var usersRouter = require(‘./routes/users’);
const campsiteRouter = require(‘./routes/campsiteRouter’);
const promotionRouter = require(‘./routes/promotionRouter’);
const partnerRouter = require(‘./routes/partnerRouter’);
const mongoose = require(‘mongoose’);
const url = ‘mongodb://localhost:27017/nucampsite’;
const connect = mongoose.connect(url, {
    useCreateIndex: true,
    useFindAndModify: false,
    useNewUrlParser: true, 
    useUnifiedTopology: true
});
connect.then(() => console.log(‘Connected correctly to server’), 
    err => console.log(err)
);
var app = express();
// view engine setup
app.set(‘views’, path.join(__dirname, ‘views’));
app.set(‘view engine’, ‘jade’);
app.use(logger(‘dev’));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(‘12345-67890-09876-54321’));
function auth(req, res, next) {
    if (!req.signedCookies.user) {
        const authHeader = req.headers.authorization;
        if (!authHeader) {
            const err = new Error(‘You are not authenticated!’);
            res.setHeader(‘WWW-Authenticate’, ‘Basic’);
            err.status = 401;
            return next(err);
        }
  
        const auth = Buffer.from(authHeader.split(‘ ‘)[1], ‘base64’).toString().split(‘:’);
        const user = auth[0];
        const pass = auth[1];
        if (user === ‘admin’ && pass === ‘password’) {
            res.cookie(‘user’, ‘admin’, {signed: true});
            return next(); // authorized
        } else {
            const err = new Error(‘You are not authenticated!’);
            res.setHeader(‘WWW-Authenticate’, ‘Basic’);
            err.status = 401;
            return next(err);
        }
    } else {
        if (req.signedCookies.user === ‘admin’) {
            return next();
        } else {
            const err = new Error(‘You are not authenticated!’);
            err.status = 401;
            return next(err);
        }
    }
}
app.use(auth);
app.use(express.static(path.join(__dirname, ‘public’)));
app.use(‘/’, indexRouter);
app.use(‘/users’, usersRouter);
app.use(‘/campsites’, campsiteRouter);
app.use(‘/promotions’, promotionRouter);
app.use(‘/partners’, partnerRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get(‘env’) === ‘development’ ? err : {};
  // render the error page
  res.status(err.status || 500);
  res.render(‘error’);
});
module.exports = app;
  • Save the changes, run the server and test the behavior. Test with Postman.
  • Optional: Do a Git commit with the message “Cookies”.

Express Sessions Part 1

  • Set up your Express server to use Express sessions to track authenticated users.
  • Enable clients to access secure resources on the server after authentication.

Install express-session and session-file-store

  • Still in the nucampsiteServer folder, install express-session and session-file-store Node modules as follows:   npm install express-session@1.17.1 session-file-store@1.5.0

Use express-session

  • Then, update app.js as follows to use express-session:
var createError = require(‘http-errors’);
var express = require(‘express’);
var path = require(‘path’);
var cookieParser = require(‘cookie-parser’);
var logger = require(‘morgan’);
const session = require(‘express-session’);
const FileStore = require(‘session-file-store’)(session);
var indexRouter = require(‘./routes/index’);
var usersRouter = require(‘./routes/users’);
const campsiteRouter = require(‘./routes/campsiteRouter’);
const promotionRouter = require(‘./routes/promotionRouter’);
const partnerRouter = require(‘./routes/partnerRouter’);
const mongoose = require(‘mongoose’);
const url = ‘mongodb://localhost:27017/nucampsite’;
const connect = mongoose.connect(url, {
    useCreateIndex: true,
    useFindAndModify: false,
    useNewUrlParser: true, 
    useUnifiedTopology: true
});
connect.then(() => console.log(‘Connected correctly to server’), 
    err => console.log(err)
);
var app = express();
// view engine setup
app.set(‘views’, path.join(__dirname, ‘views’));
app.set(‘view engine’, ‘jade’);
app.use(logger(‘dev’));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
//app.use(cookieParser(‘12345-67890-09876-54321’));
app.use(session({
  name: ‘session-id’,
  secret: ‘12345-67890-09876-54321’,
  saveUninitialized: false,
  resave: false,
  store: new FileStore()
}));
function auth(req, res, next) {
  console.log(req.session);
  if (!req.session.user) {
      const authHeader = req.headers.authorization;
      if (!authHeader) {
          const err = new Error(‘You are not authenticated!’);
          res.setHeader(‘WWW-Authenticate’, ‘Basic’);
          err.status = 401;
          return next(err);
      }
      const auth = Buffer.from(authHeader.split(‘ ‘)[1], ‘base64’).toString().split(‘:’);
      const user = auth[0];
      const pass = auth[1];
      if (user === ‘admin’ && pass === ‘password’) {
          req.session.user = ‘admin’;
          return next(); // authorized
      } else {
          const err = new Error(‘You are not authenticated!’);
          res.setHeader(‘WWW-Authenticate’, ‘Basic’);
          err.status = 401;
          return next(err);
      }
  } else {
      if (req.session.user === ‘admin’) {
          return next();
      } else {
          const err = new Error(‘You are not authenticated!’);
          err.status = 401;
          return next(err);
      }
  }
}
app.use(auth);
app.use(express.static(path.join(__dirname, ‘public’)));
app.use(‘/’, indexRouter);
app.use(‘/users’, usersRouter);
app.use(‘/campsites’, campsiteRouter);
app.use(‘/promotions’, promotionRouter);
app.use(‘/partners’, partnerRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get(‘env’) === ‘development’ ? err : {};
  // render the error page
  res.status(err.status || 500);
  res.render(‘error’);
});
module.exports = app;
  • Save the changes, run the server and examine the behavior.  Test with Postman.
  • Optional: Do a Git commit with the message “Express Session Part 1”.

In Part 1, we used Express Sessions and  session-file-store library to track user information/activity and permit requests from authenticated users.


Express Sessions Part 2

In Part 2, we will add a Mongoose Schema and Model for a users collection and set up users router to register & authenticate users with user documents. We will continue to use Express Sessions to track users when logged in and we will update our handling of user sessions to remove a session when a user log out.

  • Set up a new Mongoose Schema and Model for a new ‘users’ MongoDB collection.
  • Use the new User model, as well as your knowledge of Express Sessions, to set up the users router to handle registering, logging into, and logging out from user accounts.
  • Enable users to access secure resources on the server after authentication.

Add a Mongoose model for users

  • Add a new Mongoose model for the users collection in the file named user.js in the models folder, and add the following to it:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const userSchema = new Schema({
    username: {
        type: String,
        required: true,
        unique: true
    },
    password: {
        type: String,
        required: true
    },
    admin: {
        type: Boolean,
        default: false
    }
});

module.exports = mongoose.model('User', userSchema);

Support user registration, login, and logout in the usersRouter

  • Replace the contents of users.js in the routes folder with the below, to support user registration, login, and logout:
const express = require('express');
const User = require('../models/user');

const router = express.Router();

/* GET users listing. */
router.get('/', function(req, res, next) {
    res.send('respond with a resource');
});

router.post('/signup', (req, res, next) => {
    User.findOne({username: req.body.username})
    .then(user => {
        if (user) {
            const err = new Error(`User ${req.body.username} already exists!`);
            err.status = 403;
            return next(err);
        } else {
            User.create({
                username: req.body.username,
                password: req.body.password})
            .then(user => {
                res.statusCode = 200;
                res.setHeader('Content-Type', 'application/json');
                res.json({status: 'Registration Successful!', user: user});
            })
            .catch(err => next(err));
        }
    })
    .catch(err => next(err));
});

router.post('/login', (req, res, next) => {
    if(!req.session.user) {
        const authHeader = req.headers.authorization;

        if (!authHeader) {
            const err = new Error('You are not authenticated!');
            res.setHeader('WWW-Authenticate', 'Basic');
            err.status = 401;
            return next(err);
        }
      
        const auth = Buffer.from(authHeader.split(' ')[1], 'base64').toString().split(':');
        const username = auth[0];
        const password = auth[1];
      
        User.findOne({username: username})
        .then(user => {
            if (!user) {
                const err = new Error(`User ${username} does not exist!`);
                err.status = 401;
                return next(err);
            } else if (user.password !== password) {
                const err = new Error('Your password is incorrect!');
                err.status = 401;
                return next(err);
            } else if (user.username === username && user.password === password) {
                req.session.user = 'authenticated';
                res.statusCode = 200;
                res.setHeader('Content-Type', 'text/plain');
                res.end('You are authenticated!')
            }
        })
        .catch(err => next(err));
    } else {
        res.statusCode = 200;
        res.setHeader('Content-Type', 'text/plain');
        res.end('You are already authenticated!');
    }
});

router.get('/logout', (req, res, next) => {
    if (req.session) {
        req.session.destroy();
        res.clearCookie('session-id');
        res.redirect('/');
    } else {
        const err = new Error('You are not logged in!');
        err.status = 401;
        return next(err);
    }
});

module.exports = router;

Update app.js to use the user authentication support

  • Next, update app.js as follows to use the user authentication support:
var createError = require(‘http-errors’);
var express = require(‘express’);
var path = require(‘path’);
var cookieParser = require(‘cookie-parser’);
var logger = require(‘morgan’);
const session = require(‘express-session’);
const FileStore = require(‘session-file-store’)(session);
var indexRouter = require(‘./routes/index’);
var usersRouter = require(‘./routes/users’);
const campsiteRouter = require(‘./routes/campsiteRouter’);
const promotionRouter = require(‘./routes/promotionRouter’);
const partnerRouter = require(‘./routes/partnerRouter’);
const mongoose = require(‘mongoose’);
const url = ‘mongodb://localhost:27017/nucampsite’;
const connect = mongoose.connect(url, {
    useCreateIndex: true,
    useFindAndModify: false,
    useNewUrlParser: true, 
    useUnifiedTopology: true
});
connect.then(() => console.log(‘Connected correctly to server’), 
    err => console.log(err)
);
var app = express();
// view engine setup
app.set(‘views’, path.join(__dirname, ‘views’));
app.set(‘view engine’, ‘jade’);
app.use(logger(‘dev’));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
//app.use(cookieParser(‘12345-67890-09876-54321’));
app.use(session({
  name: ‘session-id’,
  secret: ‘12345-67890-09876-54321’,
  saveUninitialized: false,
  resave: false,
  store: new FileStore()
}));
app.use(‘/’, indexRouter);
app.use(‘/users’, usersRouter);
function auth(req, res, next) {
  console.log(req.session);
  if (!req.session.user) {
      const err = new Error(‘You are not authenticated!’);
      err.status = 401;
      return next(err);
  } else {
      if (req.session.user === ‘authenticated’) {
          return next();
      } else {
          const err = new Error(‘You are not authenticated!’);
          err.status = 401;
          return next(err);
      }
  }
}
app.use(auth);
app.use(express.static(path.join(__dirname, ‘public’)));
app.use(‘/campsites’, campsiteRouter);
app.use(‘/promotions’, promotionRouter);
app.use(‘/partners’, partnerRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get(‘env’) === ‘development’ ? err : {};
  // render the error page
  res.status(err.status || 500);
  res.render(‘error’);
});
module.exports = app;
  • Save the changes and test the server.
  • Optional: Do a Git commit with the message “Express Session Part 2”.

Additional Resources: