@meese-os/server
Version:
meeseOS Server
336 lines (289 loc) • 9.03 kB
JavaScript
/**
* OS.js - JavaScript Cloud/Web Desktop Platform
*
* Copyright (c) 2011-Present, Anders Evenrud <andersevenrud@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @author Anders Evenrud <andersevenrud@gmail.com>
* @licence Simplified BSD License
*/
/* eslint-disable no-unused-vars */
const fs = require("fs-extra");
const consola = require("consola");
const pathLib = require("path");
const logger = consola.withTag("Auth");
const nullAdapter = require("./adapters/auth/null");
const TokenStorage = require("./utils/token-storage");
const TokenFactory = require("./utils/token-factory");
/* eslint-enable no-unused-vars */
/**
* TODO: typedef
* @typedef {Object} AuthAdapter
*/
/**
* Authentication User Profile
* @typedef {Object} AuthUserProfile
* @property {String} username
* @property {String} name
* @property {String[]} groups
*/
/**
* Authentication Service Options
* @typedef {Object} AuthOptions
* @property {AuthAdapter} [adapter]
* @property {String[]} [requiredGroups]
* @property {String[]} [denyUsers]
*/
/**
* Authentication Handler
*/
class Auth {
/**
* Creates a new instance.
* @param {Core} core MeeseOS Core instance reference
* @param {AuthOptions} [options={}] Service Provider arguments
*/
constructor(core, options = {}) {
const { requiredGroups, denyUsers } = core.configuration.auth;
/**
* @type {Core}
*/
this.core = core;
/**
* @type {AuthOptions}
*/
this.options = {
adapter: nullAdapter,
requiredGroups,
denyUsers,
...options,
};
/**
* @type {AuthAdapter}
*/
this.adapter = nullAdapter(core, this.options.config);
try {
this.adapter = this.options.adapter(core, this.options.config);
} catch (e) {
logger.warn(e);
}
}
/**
* Initializes adapter.
* @returns {Promise<Boolean>}
*/
async init() {
/**
* @type {TokenFactory}
*/
this.tokenFactory = this.core.make("meeseOS/token-factory");
await this.tokenFactory.init();
/**
* @type {TokenStorage}
*/
this.storage = this.core.make("meeseOS/token-storage");
await this.storage.init();
if (this.adapter.init) {
return this.adapter.init();
}
return true;
}
/**
* Destroys instance.
*/
destroy() {
if (this.adapter.destroy) {
this.adapter.destroy();
}
}
/**
* Performs a login request.
* @param {Request} req HTTP request
* @param {Response} res HTTP response
* @returns {Promise<undefined>}
*/
async login(req, res) {
let result = {};
const refreshToken = req.body.refreshToken;
if (refreshToken) {
// Decodes the JWT refresh token and returns the user information
const refreshTokenUser =
await this.tokenFactory.refreshToAccessToken(refreshToken);
if (refreshTokenUser?.accessToken) {
result.username = refreshTokenUser.username;
result.groups = refreshTokenUser.groups;
result.accessToken = refreshTokenUser.accessToken;
}
} else {
// Attempts to log the user in using the adapter
const standardAuthResult = await this.adapter.login(req, res);
if (standardAuthResult) {
result = standardAuthResult;
result.refreshToken = this.tokenFactory.createRefreshToken(
standardAuthResult.username,
standardAuthResult.groups
);
result.accessToken = this.tokenFactory.createAccessToken(
standardAuthResult.username,
standardAuthResult.groups
);
this.storage.create(result.refreshToken);
}
}
if (Object.keys(result).length !== 0) {
const profile = this.createUserProfile(req.body, result);
if (profile && this.checkLoginPermissions(profile)) {
await this.createHomeDirectory(profile);
req.session.user = profile;
req.session.save(() => {
this.core.emit(
"meeseOS/core:logged-in",
Object.freeze({ ...req.session, })
);
res.status(200).json(profile);
});
return;
}
}
res.status(403).json({ error: "Invalid login or permission denied" });
}
/**
* Performs a logout request.
* @param {Request} req HTTP request
* @param {Response} res HTTP response
* @returns {Promise<undefined>}
*/
async logout(req, res) {
this.core.emit(
"meeseOS/core:logging-out",
Object.freeze({ ...req.session, })
);
await this.adapter.logout(req, res);
try {
req.session.destroy();
} catch (e) {
logger.warn(e);
}
res.json({});
}
/**
* Performs a register request.
* @param {Request} req HTTP request
* @param {Response} res HTTP response
* @returns {Promise<undefined>}
*/
async register(req, res) {
if (this.adapter.register) {
const result = await this.adapter.register(req, res);
return res.json(result);
}
return res.status(403).json({ error: "Registration unavailable" });
}
/**
* Checks if login is allowed for this user.
* @param {AuthUserProfile} profile User profile
* @returns {Boolean}
*/
checkLoginPermissions(profile) {
const { requiredGroups, denyUsers } = this.options;
if (denyUsers.indexOf(profile.username) !== -1) {
return false;
}
if (requiredGroups.length > 0) {
const passes = requiredGroups.every((name) =>
profile.groups.indexOf(name) !== -1
);
return passes;
}
return true;
}
/**
* Creates user profile object.
* @param {Object} fields Input fields
* @param {Object} result Login result
* @returns {AuthUserProfile|Boolean}
*/
createUserProfile(fields, result) {
const ignores = ["password"];
const required = ["username"];
const template = {
username: fields.username,
id: fields.username,
name: fields.username,
groups: this.core.config("auth.defaultGroups", []),
refreshToken: fields.refreshToken,
};
const mergedArrays = { ...fields, ...result };
const missing = required.filter((k) => typeof mergedArrays[k] === "undefined");
if (missing.length) {
logger.warn("Missing user attributes:", missing);
return false;
}
const values = Object.keys(mergedArrays)
.filter((key) => ignores.indexOf(key) === -1)
.reduce((obj, key) => ({ ...obj, [key]: mergedArrays[key] }), {});
return { ...template, ...values };
}
/**
* Tries to create home directory for a user.
* @param {AuthUserProfile} profile User profile
* @returns {Promise<undefined>}
*/
async createHomeDirectory(profile) {
const vfs = this.core.make("meeseOS/vfs");
const template = this.core.config("vfs.home.template", []);
if (typeof template === "string") {
// If the template is a string, it is a path to a directory
// that should be copied to the user's home directory
const root = await vfs.realpath("home:/", profile);
await fs.copy(template, root, { overwrite: false });
} else if (Array.isArray(template)) {
await this.createHomeDirectoryFromArray(template, vfs, profile);
}
}
/**
* If the template is an array, it is a list of files that should be copied
* to the user's home directory.
* @param {Object[]} template Array of objects with a specified path,
* optionally with specified content but defaulting to an empty string
* @param {VFSServiceProvider} vfs An instance of the virtual file system
* @param {AuthUserProfile} profile User profile
*/
async createHomeDirectoryFromArray(template, vfs, profile) {
for (const file of template) {
try {
const { path, contents = "" } = file;
const shortcutsFile = await vfs.realpath(`home:/${path}`, profile);
const dir = pathLib.dirname(shortcutsFile);
if (!await fs.pathExists(shortcutsFile)) {
await fs.ensureDir(dir);
await fs.writeFile(shortcutsFile, contents);
}
} catch (e) {
logger.warn(`There was a problem writing '${file.path}' to the home directory template`);
logger.error("ERROR:", e);
}
}
}
}
module.exports = Auth;