You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
165 lines
4.8 KiB
165 lines
4.8 KiB
2 years ago
|
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());
|
||
|
}
|