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:
- TutorialsPoint – ExpressJS – Cookies
- Github – express-session
- ExpressJS – res.cookie()
- TutorialsPoint – ExpressJS – Cookies
- NPM – cookie-parser
- WhatAreCookies.com
- MDN – Cookies
- The best chocolate chip cookies ever 🙂
- Github – express-session
- NPM – express-session
- NPM – session-file-store
- FlavioCopes – Express Sessions
- Mongoose Guide
- Express.js – clearCookie()
- NPM – express-session