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.
241 lines
8.4 KiB
241 lines
8.4 KiB
/*
|
|
Implements everything needed for a [Passport.js](https://www.passportjs.org) middleware.
|
|
It supports either a Redis store or a MemoryStore. The MemoryStore can be used in development
|
|
if you can't or won't run Redis while you work. Ultimately you should be using the Redis
|
|
store even in development as it keeps you logged in while you work which is easier.
|
|
|
|
The type of store is configured in the `secrets/config.json` file (which is loaded into `lib/config.js`) using the `session_store` option. See `configure_sessions` for more information on this
|
|
configuration option.
|
|
|
|
The most important part of this module is the `socket` function, which adds `Passport.js`
|
|
authentication to `socket.io` which is something missing from `Passport.js`. Look at that
|
|
if you wondered how to do that.
|
|
*/
|
|
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);
|
|
}
|
|
});
|
|
|
|
/*
|
|
Configures `Passport.js` to use either `RedisStore` or `MemoryStore`. If you want to
|
|
add a different store then modify this function. If the `config.json:session_store` is
|
|
`"redis"` you'll get the `RedisStore`. If it's set to `"memory"` then it will use the
|
|
`MemoryStore`.
|
|
|
|
___WARNING___: You don't really call this and instead use the `sessionStore` variable in
|
|
this module.
|
|
|
|
+ ___return___ (RedisStore|MemoryStore|undefined) -- The store to use or `undefined` on error.
|
|
*/
|
|
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
|
|
}
|
|
}
|
|
|
|
/*
|
|
* The configured session store being used. Just use this instead of
|
|
* calling `configure_sessions` yourself.
|
|
*/
|
|
export const sessionStore = configure_sessions();
|
|
|
|
/*
|
|
The cookie secret configured in `secrets/config.json:auth`.
|
|
|
|
___FOOTGUN___: Don't expose this to the internet!
|
|
*/
|
|
export const cookie_secret = auth.cookie_secret;
|
|
|
|
/*
|
|
The cookie _domain_ configured in `secrets/config.json:auth`. If this isn't
|
|
exactly the same as your domain then you'll not be able to log in. You can
|
|
run:
|
|
|
|
```shell
|
|
node bando.js api --cookies-suck
|
|
```
|
|
|
|
This will output cookie debugging information so you can figure out what's going on.
|
|
Incidentally, debugging the stupid cookies ends up a huge chunk of the code in `commands/api.js`
|
|
because, you guessed it, `--cookies-suck`.
|
|
*/
|
|
export const cookie_domain = auth.cookie_domain;
|
|
assert(cookie_domain !== undefined, "secrets/config.json:auth.cookie_domain isn't set.");
|
|
|
|
/*
|
|
This does the actual `Passport.js` login, but you wouldn't necessarily use this
|
|
directly unless you are making something custom. Instead, you "tag" handlers
|
|
in `api/` handlers with `.login = true` and then the `commands/api.js` will
|
|
automatically run this before running your handler. Look in `api/login.js` to see
|
|
how to use `post.login = true` to actually use this.
|
|
|
|
If you want to see how this is used, look for `auth.login` in `commands/api.js`.
|
|
|
|
+ ___return__ Function -- Returns a Express.js handler that does the `passport.authenticate`.
|
|
*/
|
|
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);
|
|
}
|
|
}
|
|
|
|
/*
|
|
An Express.js handler that make authentication required. Just like with `login` it
|
|
is normally used by adding the `.authenticated = true` tag to your handlers. If you
|
|
do `get.authenticated = true` then that `get` will require a login before it runs.
|
|
The `commands/api.js` command is the one that uses this `require` function to add that
|
|
feature to your handlers in `api/`.
|
|
*/
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This is added to any `socket/` handlers tagged with `.authenticated`. It does
|
|
* the same thing as `required` above but instead does all the fairly stupid things
|
|
* you need to authenticate a `socket.io` connection.
|
|
*
|
|
* It's main purpose is to authenticate, but _also_ to connect the `req.user` to the
|
|
* socket so you can access the user in your `socket/` handlers.
|
|
*
|
|
* + `socket Object` -- A socket.io socket to authenticate.
|
|
* + ___return___ `boolean` -- A `Promise` that performs the authentication then adds the `req.user` to the socket.
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Initializes your Express.js application with the `Passport.js` required handlers.
|
|
*/
|
|
export const init = (app) => {
|
|
app.use(passport.initialize());
|
|
app.use(passport.session());
|
|
}
|
|
|