DockerERTFF/lib/auth/provider.js

159 lines
5.2 KiB
JavaScript

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 = this.app.getSession(this.name + 'Id') || false;
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);
}
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 impersonate(identity) {
this.app.setSession(this.name + 'Id', identity);
this.app.set('identity', identity);
this.identity = identity;
await this.app.regenerateSessionId();
}
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.setSession(this.name + 'Id', identity);
this.app.set('identity', identity);
if (remember) {
debug('setCookie', identity, username, password);
this.app.setCookie(this.name + '.auth', this.encrypt({ username, password }), this.cookieOpts);
}
this.identity = identity;
}
await this.app.regenerateSessionId();
return identity;
}
async logout() {
this.app.removeSession(this.name + 'Id');
this.app.removeCookie(this.name + '.auth', this.cookieOpts);
this.app.remove('identity');
this.identity = false;
await this.app.regenerateSessionId();
}
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;