Express Sessions (JSON)

Dr. Greg M. Bernstein

Update October 19th, 2021

Session Implementation

Why another library?

  • Sessions are a prime attack point for websites and web apps
  • A compromised session ID is as bad as a compromised password
  • Always good to use well proven libraries and techniques. “express-sessions” has over 600K weekly downloads.

References

express-session 1

Express Middleware to help us:

  • Manage session IDs: creation, deletion, validation
  • Associate session IDs with session storage
  • Sign and validate cookies used to transport session IDs

express-session 2

Uses other packages for:

  • Cryptographically strong session ID generation using uid-safe
  • Cookie signing/verification cookie-signature
  • Session Storage for production environments. See long list of adapters

Sessions

  • We’ll establish a session as soon as a user visits our site. Before login
  • We’ll alway keep track of the users login status or role, we can also keep track of some user information such as preferences or shopping carts prior to login if we like
  • After login we’ll make sure basic user information is available to the web server application.

Express-Session Functionality

  • If the session middleware sees an HTTP request without a cookie for a session ID it will generate one and allocate associated session storage.
  • If the middleware sees an HTTP request with a cookie for a valid session ID it will retrieve the stored session information and attach it to the express request parameter for easy use by our software.
  • The session middleware works with a session store of your choosing. It comes with a simple session store only suitable for development purposes.

Tour Site Example (JSON API)

Tour Company

Example files in: SessionJSONExample.zip. Start of a tour company JSON API

  • Three user roles: guest, customer, admin
  • Only admins can add tours
  • All users can see tours, or login

APIs

  • /tours, GET: returns list of tours in JSON
  • /login, POST: takes email and password, returns user info
  • /logout, GET: ends the session
  • /addTour, POST: takes tour data, only admin users

Server Initialization

From tourServer.mjs

import express from 'express';
const app = express(); 
import session from 'express-session';
import bcrypt from 'bcryptjs';

const cookieName = "TourSid"; // Session ID cookie name, use this to delete cookies too.
app.use(session({
    secret: 'website development CSUEB',
    resave: false,
    saveUninitialized: false,
    name: cookieName // Sets the name of the cookie used by the session middleware
}));

// Fake user and tour data
import { readFile } from 'fs/promises';
const users = JSON.parse(await readFile(new URL('./secUsers.json',
    import.meta.url)));
const tours = JSON.parse(await readFile(new URL('./tours.json',
    import.meta.url)));

Session Initialization

From tourServer.mjs, uses middleware

// This initializes session state
function setUpSessionMiddleware(req, res, next) {
    console.log(`\nsession object: ${JSON.stringify(req.session)}`);
    console.log(`session id: ${req.session.id}`);
    if (!req.session.user) {
        req.session.user = { role: "guest" };
    };
    next();
};

app.use(setUpSessionMiddleware);

Path Protection Middleware

From tourServer.mjs

// Use this middleware to restrict paths to only logged in users
function checkCustomerMiddleware(req, res, next) {
    if (req.session.user.role === "guest") {
        res.status(401).json({ error: "Not permitted" });;
    } else {
        //      console.log(`\nSession info: ${JSON.stringify(req.session)} \n`);
        next();
    }
};

// Use this middleware to restrict paths only to admins
function checkAdminMiddleware(req, res, next) {
    if (req.session.user.role !== "admin") {
        res.status(401).json({ error: "Not permitted" });;
    } else {
        next();
    }
};

A Protected Path

From tourServer.mjs

// Only available to admin, returns updated tour list.
app.post('/addTour', checkAdminMiddleware, express.json(), function(req, res) {
    let temp = req.body;
    //  console.log(temp);
    // Note need to check input here to prevent injection attacks
    let event = {
        name: temp.name,
        date: temp.date,
    };
    tours.virtTours.push(event);
    res.json(tours.virtTours);
});

Login Processing

From tourServer.mjs

// Available to all visitors, returns user info if successful
app.post('/login', express.json(), function(req, res) {
    let email = req.body.email;
    let password = req.body.password;
    // Find user
    let auser = users.find(function(user) {
        return user.email === email
    });
    if (!auser) { // Not found
        res.status(401).json({ error: true, message: "User/Password error" });
        return;
    }
    let verified = bcrypt.compareSync(password, auser.passHash);
    if (verified) {
        // Upgrade in priveledge, should generate new session id
        // Save old session information if any, create a new session
        let oldInfo = req.session.user;
        req.session.regenerate(function(err) {
            if (err) {
                console.log(err);
            }
            let newUserInfo = Object.assign(oldInfo, auser);
            delete newUserInfo.passHash;
            req.session.user = newUserInfo;
            res.json(newUserInfo);
        });
    } else {
        res.status(401).json({ error: true, message: "User/Password error" });
    }
});

Testing Protected Interface

Using a Node.js program, addTourTest.mjs

/* Testing the POST /tours/add API */
import fetch from "node-fetch";
import urlBase from './testURL.mjs';

function extractCookies(rawStrings) {
    let cookies = [];
    rawStrings.forEach(function(ck) {
        cookies.push(ck.split(";")[0]); // Just grabs cookie name=value part
    });
    return cookies.join(";"); // If more than one cookie join with ;
}

let addTour = {
    url: urlBase + "addTour",
    options: {
        method: "POST",
        body: JSON.stringify({
            name: "Windsurf K2-18b, 110 Light Years",
            date: "Sometime in 2025",
        }),
        headers: { "Content-Type": "application/json" },
    },
};

let loginAdmin = {
    url: urlBase + "login",
    options: {
        method: "POST",
        body: JSON.stringify({
            // admin user, see users.json file
            email: "antisun1921@outlook.com",
            password: "R.r<E&xt",
        }),
        headers: { "Content-Type": "application/json" },
    },
};

let loginCust = {
    url: urlBase + "login",
    options: {
        method: "POST",
        body: JSON.stringify({
            // admin user, see users.json file
            email: "stedhorses1903@yahoo.com",
            password: "nMQs)5Vi",
        }),
        headers: { "Content-Type": "application/json" },
    },
};

async function someTests() {
    console.log("Try adding tour without logging in");
    try {
        let res = await fetch(addTour.url, addTour.options);
        console.log(`Add Tour result: ${res.statusText}`);
    } catch (e) {
        console.log(`Error: ${e}\n`);
    }

    console.log("Login as admin, then adding tour");
    try {
        let res = await fetch(loginAdmin.url, loginAdmin.options);
        console.log(`login results: ${res.statusText}`);
        // Look at the cookie
        let savedCookie = extractCookies(res.headers.raw()["set-cookie"]);
        console.log(`Saved cookie: ${savedCookie}`);
        addTour.options.headers.cookie = savedCookie;
        // User info from login
        let userInfo = await res.json();
        console.log(userInfo);
        res = await fetch(addTour.url, addTour.options);
        console.log(`Add Tour result: ${res.statusText}\n`);
        let data = await res.json();
        console.log(data);
    } catch (e) {
        console.log(`Error: ${e}\n`);
    }

    console.log("Login as customer, then try adding tour");
    try {
        let res = await fetch(loginCust.url, loginCust.options);
        console.log(`login results: ${res.statusText}`);
        // Look at the cookie
        let savedCookie = extractCookies(res.headers.raw()["set-cookie"]);
        console.log(`Saved cookie: ${savedCookie}`);
        addTour.options.headers.cookie = savedCookie;
        // User info from login
        let userInfo = await res.json();
        console.log(userInfo);
        res = await fetch(addTour.url, addTour.options);
        console.log(`Add Tour result: ${res.statusText}\n`);
        let data = await res.json();
        console.log(data);
    } catch (e) {
        console.log(`Error: ${e}\n`);
    }
}

someTests();

Logout Processing

From tourServer.mjs

app.get('/logout', function (req, res) {
    let options = req.session.cookie;
    req.session.destroy(function (err) {
        if (err) {
            console.log(err);
        }
        res.clearCookie(cookieName, options); // the cookie name and options
        res.json({message: "Goodbye"});
    })
});
// reveal.js plugins