diff --git a/src/server/middleware/Authenticator.js b/src/server/middleware/Authenticator.js new file mode 100644 index 0000000..1b902d0 --- /dev/null +++ b/src/server/middleware/Authenticator.js @@ -0,0 +1,123 @@ +const { AbstractUserDatabase } = require('../interfaces'); +const { Util } = require('../../util'); + +const session = require('express-session'); +const MongoStore = require('connect-mongo'); +const Strategy = require('@navy.gif/passport-discord'); +const Passport = require('passport'); + +/** + * Takes care of sessions and authentication + * + * @class Authenticator + */ +class Authenticator { + + /** + * Creates an instance of Authenticator. + * @param {Object} express + * @param {AbstractUserDatabase} users + * @param {Object} sessionOptions { + * mongo, + * cookie: { + * maxAge = 0.5 * 24 * 60 * 60 * 1000, + * secure = false + * }, + * secret + * } + * @memberof Authenticator + */ + constructor (server, express, users, { + mongo, secret, discordID, discordSecret, callbackURL, discordScope, discordVersion, + cookie = { } + }) { + + if (!(users instanceof AbstractUserDatabase)) Util.fatal(new Error(`Expecting user database to be an instance inheriting AbstractUserDatabase`)); + this.userdb = users; + + if (!mongo) Util.fatal(new Error('Missing mongo client for ')); + + this.logger = server.createLogger(this); + + cookie = { maxAge: 0.5 * 24 * 60 * 60 * 1000, secure: false, ...cookie }; + cookie.secure = cookie.secure && process.env.NODE_ENV !== 'development'; + express.use(session({ + cookie, + store: MongoStore.create({ client: mongo, dbName: 'sessions' }), + secret, + resave: false, + saveUninitialized: false + })); + + express.use(Passport.initialize()); + express.use(Passport.session()); + + Passport.serializeUser((user, callback) => { + callback(null, user.id); + }); + + Passport.deserializeUser(async (id, callback) => { + const user = await this.userdb.fetchUser(id); + callback(null, user); + }); + + Passport.use(new Strategy({ + clientID: discordID, clientSecret: discordSecret, callbackURL, scope: discordScope, version: discordVersion + }, async (accessToken, refreshToken, profile, callback) => { + this.logger.info(`${profile.username} (${profile.id}) is logging in.`); + const user = await this.userdb.userFromDiscord(profile); + callback(null, user); + })); + + } + + async authenticate (req, res, next) { + + if (this._authenticate(req, res)) return next(); + + } + + async _authenticate (req, res) { + + if (req.isAuthenticated()) return true; + + // Sometimes the authorisation key automatically has a token type prepended to the header value, + // we don't care about that, we just want the key, which will always be the last element + const authHeader = req.get('Authorization') || req.get('Authorisation'); + const segments = authHeader.split(' '); + const key = segments[segments.length - 1]; + + const user = await this.userdb.matchToken(key); + if (user) req.user = user; + else { + res.status(401).send('Unknown identity'); + return false; + } + + return true; + + } + + /** + * Authorisation implicitly checks for authentication + * + * @param {*} permission + * @return {*} + * @memberof Authenticator + */ + createAuthoriser (permission) { + + const func = (req, res, next) => { + const { user } = req; + if (!this._authenticate(req, res)) return; + if (user.hasPermission(permission)) return next(); + return res.status(403).send('Access denied'); + }; + + return func; + + } + +} + +module.exports = Authenticator; \ No newline at end of file diff --git a/src/server/middleware/index.js b/src/server/middleware/index.js new file mode 100644 index 0000000..3044f3e --- /dev/null +++ b/src/server/middleware/index.js @@ -0,0 +1,3 @@ +module.exports = { + Authenticator: require('./Authenticator') +}; \ No newline at end of file