import cookie from 'cookie'; import { Strategy } from 'passport-local'; import passport from 'passport'; import signature from 'cookie-signature'; import redis from 'redis'; import session from 'express-session'; import memorystore from 'memorystore'; import connectRedis from 'connect-redis'; import * as models from '../lib/models.js'; import logging from '../lib/logging.js'; import { session_store, auth } from "../lib/config.js"; import assert from "assert"; const log = logging.create("lib/auth.js"); const redisClient = redis.createClient(); const MemoryStore = memorystore(session); const RedisStore = connectRedis(session); passport.use(new Strategy( async (username, password, cb) => { let user = await models.User.auth(username, password); return user ? cb(null, user) : cb(null, false); }) ); passport.serializeUser((user, cb) => { cb(null, user.id); }); passport.deserializeUser(async (id, cb) => { try { const user = await models.User.first({id}); if(user) { return cb(null, user); } else { return cb(null, false); } } catch(error) { log.error(error); return cb(null, false); } }); export const configure_sessions = () => { if(session_store === "redis") { log.info("Using redis to store sessions."); return new RedisStore({client: redisClient}); } else if(session_store === "memory") { log.warn("Using memory to store sessions. Restarts will force logins."); return new MemoryStore({ checkPeriod: 86400000, }); } else { assert(false, `Error, unknown session store ${session_store} in secrets/config.json. Only "redis" and "memory" supported.`); return undefined; // shouldn't reach this but shut up eslint } } export const sessionStore = configure_sessions(); export const cookie_secret = auth.cookie_secret; export const cookie_domain = auth.cookie_domain; assert(cookie_domain !== undefined, "secrets/config.json:auth.cookie_domain isn't set."); export const login = () => { return (req, res, next) => { passport.authenticate('local', (err, user, info) => { if(err) { log.error(err, info); return res.status(500).json({error: info}); } else if(!user) { // log.debug(`Auth attempt user is ${user.id}.`); return res.status(401).json({error: "Authentication required"}); } else { // log.debug(`User exists, attempting login ${user.id}`); return req.login(user, (err) => { if(err) { log.error(err); res.status(403).json({error: "Invalid login"}); // BUG: is this right or is next("route") return next(err); } else { return next(); } }); } })(req, res, next); } } export const required = () => { return (req, res, next) => { const user = req.user; if(!user) { return res.status(401).json({error: "Authentication required"}); // WARNING: next('route') is NOT needed to abort the chain of calls despite express docs } else { return next(); } } } export const socket = async (socket) => { try { if(!socket.handshake.headers.cookie) { throw new Error("No cookies in socket handshake headers, disconnecting socket."); } let cookies = cookie.parse(socket.handshake.headers.cookie); if(!cookies) { throw new Error("No cookies were parsed even though the header exists, disconnecting socket."); } const connect_sid = cookies['connect.sid']; if(!connect_sid) { throw new Error("No connect.sid cookie in the list of cookies, disconnecting socket."); } if(connect_sid.slice(0, 2) !== 's:') { throw new Error(`is_authenticate only supports signed cookies. Your cookies don't start with s: they start with ${connect_sid.slice(0, 2)}`); } let session_id = signature.unsign(connect_sid.slice(2), cookie_secret); log.debug(`Session id is ${session_id}`); if(!session_id) { throw new Error(`Unable to get session_id ${session_id} with session signature.`); } return new Promise((resolve, reject) => { sessionStore.get(session_id, (err, session) => { socket.express_session = session; if(err) { log.error(err); reject(err); } else if(session && session.passport && session.passport.user !== undefined) { socket.user_id = session.passport.user; resolve(true); } else { socket.disconnect(); delete socket.user; delete socket.express_session; resolve(false); } }); }); } catch(error) { // not logging the error stack trace for now since i'm using the Error as a jump/clean log.debug(error.message); socket.disconnect(); delete socket.user; delete socket.express_session; return false; } } export const init = (app) => { app.use(passport.initialize()); app.use(passport.session()); }