Stateful session management: Store session which associate with user, and store in the menory on server.
Sign Up:
app.route('/api/signup')
.post(createUser);
import {Request, Response} from 'express'; import {db} from './database'; import * as argon2 from 'argon2'; import {validatePassword} from './password-validation'; import {randomBytes} from './security.utils'; import {sessionStore} from './session-store'; export function createUser (req: Request, res: Response) { const credentials = req.body; const errors = validatePassword(credentials.password); if (errors.length > 0) { res.status(400).json({ errors }); } else { createUserAndSession(res, credentials); } } async function createUserAndSession(res, credentials) { // Create a password digest const passwordDigest = await argon2.hash(credentials.password); // Save into db const user = db.createUser(credentials.email, passwordDigest); // create random session id const sessionId = await randomBytes(32).then(bytes => bytes.toString('hex')); // link sessionId with user sessionStore.createSession(sessionId, user); // set sessionid into cookie res.cookie('SESSIONID', sessionId, { httpOnly: true, // js cannot access cookie secure: true // enable https only }); // send back to UI res.status(200).json({id: user.id, email: user.email}); }
Password validation:
import * as passwordValidator from 'password-validator'; // Create a schema const schema = new passwordValidator(); // Add properties to it schema .is().min(7) // Minimum length 7 .has().uppercase() // Must have uppercase letters .has().lowercase() // Must have lowercase letters .has().digits() // Must have digits .has().not().spaces() // Should not have spaces .is().not().oneOf(['Passw0rd', 'Password123']); // Blacklist these values export function validatePassword(password: string) { return schema.validate(password, {list: true}); }
Random bytes generator:
const util = require('util'); const crypto = require('crypto'); // convert a callback based code to promise based export const randomBytes = util.promisify( crypto.randomBytes );
Session storage:
import {Session} from './session'; import {User} from '../src/app/model/user'; class SessionStore { private sessions: {[key: string]: Session} = {}; createSession(sessionId: string, user: User) { this.sessions[sessionId] = new Session(sessionId, user); } findUserBySessionId(sessionId: string): User | undefined { const session = this.sessions[sessionId]; return this.isSessionValid(sessionId) ? session.user : undefined; } isSessionValid(sessionId: string): boolean { const session = this.sessions[sessionId]; return session && session.isValid(); } destroySession(sessionId: string): void { delete this.sessions[sessionId]; } } // We want only global singleton export const sessionStore = new SessionStore();
In menory database:
import * as _ from 'lodash'; import {LESSONS, USERS} from './database-data'; import {DbUser} from './db-user'; class InMemoryDatabase { userCounter = 0; createUser(email, passwordDigest) { const id = ++this.userCounter; const user: DbUser = { id, email, passwordDigest }; USERS[id] = user; return user; } findUserByEmail(email: string): DbUser { const users = _.values(USERS); return _.find(users, user => user.email === email); } } export const db = new InMemoryDatabase();
Login:
app.route('/api/login')
.post(login);
import {Request, Response} from 'express'; import {db} from './database'; import {DbUser} from './db-user'; import * as argon2 from 'argon2'; import {randomBytes} from './security.utils'; import {sessionStore} from './session-store'; export function login(req: Request, res: Response) { const info = req.body; const user = db.findUserByEmail(info.email); if (!user) { res.sendStatus(403); } else { loginAndBuildResponse(info, user, res); } } async function loginAndBuildResponse(credentials: any, user: DbUser, res: Response) { try { const sessionId = await attemptLogin(credentials, user); res.cookie('SESSIONID', sessionId, {httpOnly: true, secure: true}); res.status(200).json({id: user.id, email: user.email}); } catch (err) { res.sendStatus(403); } } async function attemptLogin(info: any, user: DbUser) { const isPasswordValid = await argon2.verify(user.passwordDigest, info.password); if (!isPasswordValid) { throw new Error('Password Invalid'); } const sessionId = await randomBytes(32).then(bytes => bytes.toString('hex')); sessionStore.createSession(sessionId, user); return sessionId; }
Logout:
app.route('/api/logout')
.post(logout);
import {Response, Request} from 'express'; import {sessionStore} from './session-store'; export const logout = (req: Request, res: Response) => { console.log(req.cookies['SESSIONID']); const sessionId = req.cookies['SESSIONID']; sessionStore.destroySession(sessionId); res.clearCookie('SESSIONID'); res.sendStatus(200); };