const config = require('../setup/config'); const crypto = require('crypto'); const debug = require('debug')('server-connect:auth'); class AuthProvider { constructor(app, opts, name) { this.app = app; this.name = name; this.identity = options.jwt ? false : this.app.getSession(this.name + 'Id') || false; this.jwt = options.jwt || false; this.jwtExpires = options.jwtExpires || '1h'; this.jwtIssuer = options.jwtIssuer || 'serverconnect'; this.secret = opts.secret || config.secret; this.basicAuth = opts.basicAuth; this.basicRealm = opts.basicRealm; this.passwordVerify = opts.passwordVerify || false; this.cookieOpts = { domain: opts.domain || undefined, httpOnly: true, maxAge: (opts.expires || 30) * 24 * 60 * 60 * 1000, // from days to ms path: opts.path || '/', secure: !!opts.secure, sameSite: opts.sameSite || 'Strict', signed: true }; } async autoLogin() { if (this.basicAuth) { const auth = require('../core/basicauth')(this.app.req); debug(`Basic auth credentials received: %o`, auth); if (auth) await this.login(auth.username, auth.password, false, true); } if (this.jwt && this.app.req.headers['authorization']) { const matches = /(\S+)\s+(\S+)/; if (matches && matches[1].toLowerCase == 'bearer') { const jwt = require('jsonwebtoken'); const { identity } = jwt.verify(matches[2], this.secret, { algorithm: 'HS256', issuer: this.jwtIssuer, }); this.app.set('identity', identity); this.identity = identity; } } const cookie = this.app.getCookie(this.name + '.auth', true); if (cookie) { const auth = this.decrypt(cookie); debug(`Login with cookie: %o`, auth); if (auth) await this.login(auth.username, auth.password, true, true); } else { debug(`No login cookie found`); } } async login(username, password, remember, autoLogin) { const identity = await this.validate(username, password, this.passwordVerify); if (!identity) { await this.logout(); if (!autoLogin) { this.unauthorized(); return false; } } else { this.app.set('identity', identity); if (!this.jwt) { this.app.setSession(this.name + 'Id', identity); if (remember) { debug('setCookie', identity, username, password); this.app.setCookie(this.name + '.auth', this.encrypt({ username, password }), this.cookieOpts); } } this.identity = identity; } if (this.jwt) { const jwt = require('jsonwebtoken'); return jwt.sign({ identity }, this.secret, { algorithm: 'HS256', expiresIn: this.jwtExpires, issuer: this.jwtIssuer, }); } return identity; } async logout() { if (!this.jwt) { this.app.removeSession(this.name + 'Id'); this.app.removeCookie(this.name + '.auth', this.cookieOpts); } this.app.remove('identity'); this.identity = false; } async restrict(opts) { if (this.identity === false) { if (opts.loginUrl) { if (this.app.req.fragment) { this.app.res.status(222).send(opts.loginUrl); } else { this.app.res.redirect(opts.loginUrl); } } else { this.unauthorized(); } return; } if (opts.permissions) { const allowed = await this.permissions(this.identity, opts.permissions); if (!allowed) { if (opts.forbiddenUrl) { if (this.app.req.fragment) { this.app.res.status(222).send(opts.forbiddenUrl); } else { this.app.res.redirect(opts.forbiddenUrl); } } else { this.forbidden(); } } } } encrypt(data) { const iv = crypto.randomBytes(16); const key = crypto.scryptSync(this.secret, iv, 32); const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); const encrypted = cipher.update(JSON.stringify(data), 'utf8', 'base64'); return iv.toString('base64') + '.' + encrypted + cipher.final('base64'); } decrypt(data) { // try/catch to prevent errors on currupt cookies try { const iv = Buffer.from(data.split('.')[0], 'base64'); const key = crypto.scryptSync(this.secret, iv, 32); const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); const decrypted = decipher.update(data.split('.')[1], 'base64', 'utf8'); return JSON.parse(decrypted + decipher.final('utf8')); } catch (err) { return undefined; } } unauthorized() { if (this.basicAuth) { this.app.res.set('WWW-Authenticate', `Basic Realm="${this.basicRealm}"`); } this.app.res.sendStatus(401); } forbidden() { this.app.res.sendStatus(403); } async validate(username, password) { throw new Error('auth.validate needs to be extended.'); } async permissions(identity, permissions) { throw new Error('auth.permissions needs to be extended.'); } } module.exports = AuthProvider;