UNPKG

smc-hub

Version:

CoCalc: Backend webserver component

935 lines (934 loc) 74.5 kB
"use strict"; /* * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. * License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var __spreadArray = (this && this.__spreadArray) || function (to, from) { for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) to[j] = from[i]; return to; }; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.verify_email_send_token = exports.is_password_correct = exports.PassportManager = exports.init_passport = exports.get_passport_manager = exports.password_hash = exports.generate_hash = exports.remember_me_cookie_name = void 0; var ms_1 = __importDefault(require("ms")); var async_utils_1 = require("smc-util/async-utils"); var debug_1 = __importDefault(require("debug")); var LOG = debug_1.default("hub:auth"); var path_1 = require("path"); var node_uuid_1 = require("node-uuid"); var passport_1 = __importDefault(require("passport")); var dot = __importStar(require("dot-object")); var _ = __importStar(require("lodash")); var misc = __importStar(require("smc-util/misc")); var message = __importStar(require("smc-util/message")); // message protocol between front-end and back-end var sign_in = require("./sign-in"); var cookies_1 = __importDefault(require("cookies")); var express_session_1 = __importDefault(require("express-session")); var theme_1 = require("smc-util/theme"); var email_1 = require("./email"); var passport_types_1 = require("smc-webapp/account/passport-types"); var safeJsonStringify = require("safe-json-stringify"); var base_path_1 = __importDefault(require("smc-util-node/base-path")); // primary strategies -- all other ones are "extra" var PRIMARY_STRATEGIES = __spreadArray(["email", "site_conf"], __read(passport_types_1.PRIMARY_SSO)); // root for authentication related endpoints -- will be prefixed with the base_path var AUTH_BASE = "/auth"; var defaults = misc.defaults, required = misc.required; var API_KEY_COOKIE_NAME = base_path_1.default + "get_api_key"; // Nov'19: actually two cookies due to same-site changes. // See https://web.dev/samesite-cookie-recipes/#handling-incompatible-clients function remember_me_cookie_name() { return (base_path_1.default.length <= 1 ? "" : encodeURIComponent(base_path_1.default)) + "remember_me"; } exports.remember_me_cookie_name = remember_me_cookie_name; //####################################### // Password hashing //####################################### var password_hash_library = require("password-hash"); var crypto = require("crypto"); // You can change the parameters at any time and no existing passwords // or cookies should break. This will only impact newly created // passwords and cookies. Old ones can be read just fine (with the old // parameters). var HASH_ALGORITHM = "sha512"; var HASH_ITERATIONS = 1000; var HASH_SALT_LENGTH = 32; // This function is private and burried inside the password-hash // library. To avoid having to fork/modify that library, we've just // copied it here. We need it for remember_me cookies. function generate_hash(algorithm, salt, iterations, password) { // there are cases where createHmac throws an error, because "salt" is undefined if (algorithm == null || salt == null) { throw new Error("undefined arguments: algorithm='" + algorithm + "' salt='" + salt + "'"); } iterations = iterations || 1; var hash = password; for (var i = 1, end = iterations, asc = 1 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) { hash = crypto.createHmac(algorithm, salt).update(hash).digest("hex"); } return algorithm + "$" + salt + "$" + iterations + "$" + hash; } exports.generate_hash = generate_hash; function password_hash(password) { // This blocks the server for about 5-9ms. return password_hash_library.generate(password, { algorithm: HASH_ALGORITHM, saltLength: HASH_SALT_LENGTH, iterations: HASH_ITERATIONS, }); } exports.password_hash = password_hash; // docs for getting these for your app // https://developers.google.com/identity/protocols/oauth2/openid-connect#appsetup // and https://console.developers.google.com/apis/credentials // // You must then put them in the database, via // // require 'c'; db() // db.set_passport_settings(strategy:'google', conf:{clientID:'...',clientSecret:'...'}, cb:console.log) // Scope: // Enabling "profile" below I think required that I explicitly go to Google Developer Console for the project, // then select API&Auth, then API's, then Google+, then explicitly enable it. Otherwise, stuff just mysteriously // didn't work. To figure out that this was the problem, I had to grep the source code of the passport-google-oauth // library and put in print statements to see what the *REAL* errors were, since that // library hid the errors (**WHY**!!?). var GoogleStrategyConf = { strategy: "google", PassportStrategyConstructor: require("@passport-next/passport-google-oauth2") .Strategy, auth_opts: { scope: "openid email profile" }, login_info: { id: function (profile) { return profile.id; }, first_name: function (profile) { return profile.name.givenName; }, last_name: function (profile) { return profile.name.familyName; }, emails: function (profile) { return profile.emails.map(function (x) { return x.value; }); }, }, }; // Get these here: // https://github.com/settings/applications/new // You must then put them in the database, via // db.set_passport_settings(strategy:'github', conf:{clientID:'...',clientSecret:'...'}, cb:console.log) var GithubStrategyConf = { strategy: "github", PassportStrategyConstructor: require("passport-github2").Strategy, auth_opts: { scope: ["user:email"], }, login_info: { id: function (profile) { return profile.id; }, full_name: function (profile) { return profile.name || profile.displayName || profile.username; }, emails: function (profile) { var _a; return ((_a = profile.emails) !== null && _a !== void 0 ? _a : []).map(function (x) { return x.value; }); }, }, }; // Get these by going to https://developers.facebook.com/ and creating a new application. // For that application, set the url to the site CoCalc will be served from. // The Facebook "App ID" and is clientID and the Facebook "App Secret" is the clientSecret // for oauth2, as I discovered by a lucky guess... (sigh). // // You must then put them in the database, via // db.set_passport_settings(strategy:'facebook', conf:{clientID:'...',clientSecret:'...'}, cb:console.log) var FacebookStrategyConf = { strategy: "facebook", PassportStrategyConstructor: require("passport-facebook").Strategy, extra_opts: { enableProof: false, profileFields: ["id", "email", "name", "displayName"], }, auth_opts: { scope: "email" }, login_info: { id: function (profile) { return profile.id; }, full_name: function (profile) { return profile.displayName; }, emails: function (profile) { var _a; return ((_a = profile.emails) !== null && _a !== void 0 ? _a : []).map(function (x) { return x.value; }); }, }, }; // Get these by: // (1) Go to https://apps.twitter.com/ and create a new application. // (2) Click on Keys and Access Tokens // // You must then put them in the database, via // db.set_passport_settings(strategy:'twitter', conf:{clientID:'...',clientSecret:'...'}, cb:console.log) var TwitterWrapper = function (_a, verify) { var consumerKey = _a.clientID, consumerSecret = _a.clientSecret, callbackURL = _a.callbackURL; // cast to any, because otherwies TypeScript complains: // Only a void function can be called with the 'new' keyword. var TwitterStrat = require("passport-twitter").Strategy; return new TwitterStrat({ consumerKey: consumerKey, consumerSecret: consumerSecret, callbackURL: callbackURL }, verify); }; var TwitterStrategyConf = { strategy: "twitter", PassportStrategyConstructor: TwitterWrapper, login_info: { id: function (profile) { return profile.id; }, full_name: function (profile) { return profile.displayName; }, emails: function (profile) { var _a; return ((_a = profile.emails) !== null && _a !== void 0 ? _a : []).map(function (x) { return x.value; }); }, }, extra_opts: { includeEmail: true, }, }; // generalized OpenID (OAuth2) profile parser for the "userinfo" endpoint // the returned structure matches passport.js's conventions function parse_openid_profile(json) { var profile = {}; profile.id = json.sub || json.id; profile.displayName = json.name; if (json.family_name || json.given_name) { profile.name = { familyName: json.family_name, givenName: json.given_name, }; // no name? we use the email address } else if (json.email) { // don't include dots, because our "spam protection" rejects domain-like patterns var emailacc = json.email.split("@")[0].split("."); var _a = __read(emailacc), first = _a[0], last = _a.slice(1); // last is always at least [] profile.name = { givenName: first, familyName: last.join(" "), }; } if (json.email) { profile.emails = [ { value: json.email, verified: json.email_verified || json.verified_email, }, ]; } if (json.picture) { profile.photos = [{ value: json.picture }]; } return profile; } // singleton var pp_manager = null; function get_passport_manager() { return pp_manager; } exports.get_passport_manager = get_passport_manager; function init_passport(opts) { return __awaiter(this, void 0, void 0, function () { var err_1; return __generator(this, function (_a) { switch (_a.label) { case 0: opts = defaults(opts, { router: required, database: required, host: required, cb: required, }); _a.label = 1; case 1: _a.trys.push([1, 4, , 5]); if (!(pp_manager == null)) return [3 /*break*/, 3]; pp_manager = new PassportManager(opts); return [4 /*yield*/, pp_manager.init()]; case 2: _a.sent(); _a.label = 3; case 3: opts.cb(); return [3 /*break*/, 5]; case 4: err_1 = _a.sent(); opts.cb(err_1); return [3 /*break*/, 5]; case 5: return [2 /*return*/]; } }); }); } exports.init_passport = init_passport; var PassportManager = /** @class */ (function () { function PassportManager(opts) { // configured strategies this.strategies = undefined; // prefix for those endpoints, where SSO services return back this.auth_url = undefined; var router = opts.router, database = opts.database, host = opts.host; this.handle_get_api_key.bind(this); this.router = router; this.database = database; this.host = host; } PassportManager.prototype.init_passport_settings = function () { var _a; return __awaiter(this, void 0, void 0, function () { var dbg, settings, settings_1, settings_1_1, setting, name_1, conf, err_2; var e_1, _b; return __generator(this, function (_c) { switch (_c.label) { case 0: dbg = LOG.extend("init_passport_settings"); if (this.strategies != null) { dbg("already initialized -- just returning what we have"); return [2 /*return*/, this.strategies]; } _c.label = 1; case 1: _c.trys.push([1, 3, , 4]); // we always offer email! this.strategies = { email: { name: "email" } }; return [4 /*yield*/, async_utils_1.callback2(this.database.get_all_passport_settings)]; case 2: settings = _c.sent(); try { for (settings_1 = __values(settings), settings_1_1 = settings_1.next(); !settings_1_1.done; settings_1_1 = settings_1.next()) { setting = settings_1_1.value; name_1 = setting.strategy; conf = setting.conf; if (conf.disabled === true) { continue; } conf.name = name_1; conf.public = (_a = setting.conf.public) !== null && _a !== void 0 ? _a : true; // set the default this.strategies[name_1] = conf; } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (settings_1_1 && !settings_1_1.done && (_b = settings_1.return)) _b.call(settings_1); } finally { if (e_1) throw e_1.error; } } return [2 /*return*/, this.strategies]; case 3: err_2 = _c.sent(); dbg("error getting passport settings -- " + err_2); throw err_2; case 4: return [2 /*return*/, {}]; } }); }); }; // Define handler for api key cookie setting. PassportManager.prototype.handle_get_api_key = function (req, res, next) { var dbg = LOG.extend("handle_get_api_key"); dbg(""); if (req.query.get_api_key) { var cookies = new cookies_1.default(req, res); // maxAge: User gets up to 60 minutes to go through the SSO process... cookies.set(API_KEY_COOKIE_NAME, req.query.get_api_key, { maxAge: 30 * 60 * 1000, }); } next(); }; // this is for pure backwards compatibility. at some point remove this! // it only returns a string[] array of the legacy authentication strategies PassportManager.prototype.strategies_v1 = function (res) { var data = []; var known = __spreadArray(["email"], __read(passport_types_1.PRIMARY_SSO)); for (var name_2 in this.strategies) { if (name_2 === "site_conf") continue; if (known.indexOf(name_2) >= 0) { data.push(name_2); } } res.json(data); }; PassportManager.prototype.get_strategies_v2 = function () { var data = []; for (var name_3 in this.strategies) { if (name_3 === "site_conf") continue; // this is sent to the web client → do not include any secret info! var info = _.pick(this.strategies[name_3], [ "name", "display", "type", "icon", "public", "exclusive_domains", ]); data.push(info); } return data; }; // version 2 tells the web client a little bit more. // the additional info is used to render customizeable SSO icons. PassportManager.prototype.strategies_v2 = function (res) { res.json(this.get_strategies_v2()); }; PassportManager.prototype.init = function () { return __awaiter(this, void 0, void 0, function () { var dbg, settings, dns; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: dbg = LOG.extend("init"); dbg(""); // initialize use of middleware this.router.use(express_session_1.default({ secret: node_uuid_1.v4() })); // secret is totally random and per-hub session this.router.use(passport_1.default.initialize()); this.router.use(passport_1.default.session()); // Define user serialization passport_1.default.serializeUser(function (user, done) { return done(null, user); }); passport_1.default.deserializeUser(function (user, done) { return done(null, user); }); // Return the configured and supported authentication strategies. this.router.get(AUTH_BASE + "/strategies", function (req, res) { if (req.query.v === "2") { _this.strategies_v2(res); } else { _this.strategies_v1(res); } }); // email verification this.router.get(AUTH_BASE + "/verify", function (req, res) { return __awaiter(_this, void 0, void 0, function () { var DOMAIN_URL, path, url, email, token, err_3; return __generator(this, function (_a) { switch (_a.label) { case 0: DOMAIN_URL = require("smc-util/theme").DOMAIN_URL; path = require("path").join(base_path_1.default, "app"); url = "" + DOMAIN_URL + path; res.header("Content-Type", "text/html"); res.header("Cache-Control", "private, no-cache, must-revalidate"); if (!(req.query.token && req.query.email) || typeof req.query.email !== "string" || typeof req.query.token !== "string") { res.send("ERROR: I need the email address and the corresponding token data"); return [2 /*return*/]; } email = decodeURIComponent(req.query.email); token = req.query.token.toLowerCase(); _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, async_utils_1.callback2(this.database.verify_email_check_token, { email_address: email, token: token, })]; case 2: _a.sent(); res.send(email_1.email_verified_successfully(url)); return [3 /*break*/, 4]; case 3: err_3 = _a.sent(); res.send(email_1.email_verification_problem(url, err_3)); return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }); }); // reset password: user email link contains a token, which we store in a session cookie. // this prevents leaking that token to 3rd parties as a referrer // endpoint has to match with smc-hub/password this.router.get(AUTH_BASE + "/password_reset", function (req, res) { if (typeof req.query.token !== "string") { res.send("ERROR: reset token must be set"); } else { var token = req.query.token.toLowerCase(); var cookies = new cookies_1.default(req, res); // to match smc-webapp/client/password-reset var name_4 = encodeURIComponent(base_path_1.default + "PWRESET"); cookies.set(name_4, token, { maxAge: ms_1.default("5 minutes"), secure: true, overwrite: true, httpOnly: false, }); res.redirect("../app"); } }); // prerequisite for setting up any SSO endpoints return [4 /*yield*/, this.init_passport_settings()]; case 1: // prerequisite for setting up any SSO endpoints _a.sent(); return [4 /*yield*/, async_utils_1.callback2(this.database.get_server_settings_cached)]; case 2: settings = _a.sent(); dns = settings.dns || theme_1.DNS; this.auth_url = "https://" + dns + path_1.join(base_path_1.default, AUTH_BASE); dbg("auth_url='" + this.auth_url + "'"); return [4 /*yield*/, Promise.all([ this.init_strategy(GoogleStrategyConf), this.init_strategy(GithubStrategyConf), this.init_strategy(FacebookStrategyConf), this.init_strategy(TwitterStrategyConf), this.init_extra_strategies(), ])]; case 3: _a.sent(); return [2 /*return*/]; } }); }); }; PassportManager.prototype.extra_strategy_constructor = function (name) { // LDAP via passport-ldapauth: https://github.com/vesse/passport-ldapauth#readme // OAuth2 via @passport-next/passport-oauth2: https://github.com/passport-next/passport-oauth2#readme // ORCID via passport-orcid: https://github.com/hubgit/passport-orcid#readme switch (name) { case "ldap": return require("passport-ldapauth").Strategy; case "oauth1": return require("passport-oauth").OAuthStrategy; case "oauth2": return require("passport-oauth").OAuth2Strategy; case "oauth2next": return require("@passport-next/passport-oauth2").Strategy; case "orcid": return require("passport-orcid").Strategy; case "saml": return require("passport-saml").Strategy; default: throw Error("hub/auth: unknown extra strategy \"" + name + "\""); } }; // this maps additional strategy configurations to a list of StrategyConf objects // the overall goal is to support custom OAuth2 and LDAP endpoints, where additional // info is sent to the webapp client to properly present them. Google&co are "primary" configurations. // // here is one example what can be saved in the DB to make this work for a general OAuth2 // if this SSO is not public (e.g. uni campus, company specific, ...) mark it as {"public":false}! // // insert into passport_settings (strategy, conf ) VALUES ( '[unique, e.g. "wowtech"]', '{"type": "oauth2next", "clientID": "CoCalc_Client", "scope": ["email", "cocalc", "profile", ... depends on the config], "clientSecret": "[a password]", "authorizationURL": "https://domain.edu/.../oauth2/authorize", "userinfoURL" :"https://domain.edu/.../oauth2/userinfo", "tokenURL":"https://domain.edu/.../oauth2/...extras.../access_token", "login_info" : {"emails" :"emails[0].value"}, "display": "[user visible, e.g. "WOW Tech"]", "icon": "https://storage.googleapis.com/square.svg", "public": false}'::JSONB ); // // note, the login_info.emails string extracts from the profile object constructed by parse_openid_profile, // which is only triggered if there is such a "userinfoURL", which is OAuth2 specific. // other auth mechanisms might already provide the profile in passport.js's structure! PassportManager.prototype.init_extra_strategies = function () { return __awaiter(this, void 0, void 0, function () { var inits, _a, _b, _c, name_5, strategy, cons, dflt_login_info, config; var e_2, _d; return __generator(this, function (_e) { switch (_e.label) { case 0: if (this.strategies == null) throw Error("strategies not initalized!"); inits = []; try { for (_a = __values(Object.entries(this.strategies)), _b = _a.next(); !_b.done; _b = _a.next()) { _c = __read(_b.value, 2), name_5 = _c[0], strategy = _c[1]; if (PRIMARY_STRATEGIES.indexOf(name_5) >= 0) { continue; } if (strategy.type == null) { throw new Error("all \"extra\" strategies must define their type, in particular also \"" + name_5 + "\""); } cons = this.extra_strategy_constructor(strategy.type); dflt_login_info = { id: "id", first_name: "name.givenName", last_name: "name.familyName", emails: "emails[0].value", }; config = { strategy: name_5, PassportStrategyConstructor: cons, login_info: Object.assign(dflt_login_info, strategy.login_info), userinfoURL: strategy.userinfoURL, // e.g. tokenURL will be extracted here, and then passed to the constructor extra_opts: _.omit(strategy, [ "name", "display", "type", "icon", "clientID", "clientSecret", "userinfoURL", "public", // we don't need that info for initializing them ]), }; inits.push(this.init_strategy(config)); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_b && !_b.done && (_d = _a.return)) _d.call(_a); } finally { if (e_2) throw e_2.error; } } return [4 /*yield*/, Promise.all(inits)]; case 1: _e.sent(); return [2 /*return*/]; } }); }); }; // a generalized strategy initizalier PassportManager.prototype.init_strategy = function (strategy_config) { return __awaiter(this, void 0, void 0, function () { var strategy, PassportStrategyConstructor, extra_opts, auth_opts, login_info, userinfoURL, dbg, conf, opts, verify, strategy_instance; var _this = this; return __generator(this, function (_a) { strategy = strategy_config.strategy, PassportStrategyConstructor = strategy_config.PassportStrategyConstructor, extra_opts = strategy_config.extra_opts, auth_opts = strategy_config.auth_opts, login_info = strategy_config.login_info, userinfoURL = strategy_config.userinfoURL; dbg = LOG.extend("init_strategy " + strategy); dbg("start"); if (this.strategies == null) throw Error("strategies not initalized!"); if (strategy == null) { dbg("strategy is null -- aborting initialization"); return [2 /*return*/]; } conf = this.strategies[strategy]; if (conf == null) { dbg("conf is null -- aborting initialization"); return [2 /*return*/]; } opts = Object.assign({ clientID: conf.clientID, clientSecret: conf.clientSecret, callbackURL: this.auth_url + "/" + strategy + "/return", }, extra_opts); verify = function (_accessToken, _refreshToken, params, profile, done) { done(undefined, { params: params, profile: profile }); }; strategy_instance = new PassportStrategyConstructor(opts, verify); // OAuth2 userinfoURL: next to /authorize // https://github.com/passport-next/passport-oauth2/blob/master/lib/strategy.js#L276 if (userinfoURL != null) { // closure captures "strategy" strategy_instance.userProfile = function userProfile(accessToken, done) { var dbg = LOG.extend("PassportStrategy").extend("userProfile"); dbg("userinfoURL=" + userinfoURL + ", accessToken=" + accessToken); this._oauth2.useAuthorizationHeaderforGET(true); this._oauth2.get(userinfoURL, accessToken, function (err, body) { dbg("get->body = " + body); var json; if (err) { dbg("InternalOAuthError: Failed to fetch user profile -- " + safeJsonStringify(err)); if (err.data) { try { json = safeJsonStringify(err.data); } catch (_) { json = {}; } } if (json && json.error && json.error_description) { return done(new Error("UserInfoError: " + json.error_description + ", " + json.error)); } return done(new Error("InternalOAuthError: Failed to fetch user profile -- " + safeJsonStringify(err))); } try { json = JSON.parse(body); } catch (ex) { return done(new Error("Failed to parse user profile -- " + body)); } var profile = parse_openid_profile(json); profile.provider = strategy; profile._raw = body; dbg("PassportStrategyConstructor.userProfile: profile = " + safeJsonStringify(profile)); return done(null, profile); }); }; } passport_1.default.use(strategy, strategy_instance); this.router.get(AUTH_BASE + "/" + strategy, this.handle_get_api_key, passport_1.default.authenticate(strategy, auth_opts || {})); this.router.get(AUTH_BASE + "/" + strategy + "/return", passport_1.default.authenticate(strategy), function (req, res) { return __awaiter(_this, void 0, void 0, function () { var dbg2, profile, login_opts, k, v, param; var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: dbg2 = dbg.extend("router.get"); if (req.user == null) { throw Error("req.user == null -- that shouldn't happen"); } dbg2(strategy + "/return user = " + safeJsonStringify(req.user)); profile = req.user["profile"]; dbg2(strategy + "/return profile = " + safeJsonStringify(profile)); login_opts = { strategy: strategy, profile: profile, req: req, res: res, host: this.host, }; for (k in login_info) { v = login_info[k]; param = typeof v == "function" ? // v is a LoginInfoDerivator<T> v(profile) : // v is a string for dot-object dot.pick(v, profile); Object.assign(login_opts, (_a = {}, _a[k] = param, _a)); } // this log line below suddenly produces a lot of output [rub, 2020-05-06] //dbg2( // `login_opts = ${safeJsonStringify(_.omit(login_opts, ["req, res"]))}` //); return [4 /*yield*/, this.passport_login(login_opts)]; case 1: // this log line below suddenly produces a lot of output [rub, 2020-05-06] //dbg2( // `login_opts = ${safeJsonStringify(_.omit(login_opts, ["req, res"]))}` //); _b.sent(); return [2 /*return*/]; } }); }); }); dbg("initialization successful"); return [2 /*return*/]; }); }); }; PassportManager.prototype.passport_login = function (opts) { var _a, _b; return __awaiter(this, void 0, void 0, function () { var dbg, cookies, locals, name_6, i, err_4, err_msg; return __generator(this, function (_c) { switch (_c.label) { case 0: opts = defaults(opts, { strategy: required, profile: required, id: required, first_name: undefined, last_name: undefined, full_name: undefined, emails: undefined, req: required, res: required, host: required, }); dbg = LOG.extend("passport_login"); cookies = new cookies_1.default(opts.req, opts.res); locals = { dbg: dbg, cookies: cookies, new_account_created: false, has_valid_remember_me: false, account_id: undefined, email_address: undefined, target: path_1.join(base_path_1.default + "app#login"), remember_me_cookie: cookies.get(remember_me_cookie_name()), get_api_key: cookies.get(API_KEY_COOKIE_NAME), action: undefined, api_key: undefined, }; //# dbg("cookies = '#{opts.req.headers['cookie']}'") # DANGER -- do not uncomment except for debugging due to SECURITY dbg("strategy=" + opts.strategy + " id=" + opts.id + " emails=" + opts.emails + " remember_me_cookie = '" + locals.remember_me_cookie + "' user=" + safeJsonStringify(opts.req.user)); // check if user is just trying to get an api key. if (locals.get_api_key) { dbg("user is just trying to get api_key"); // Set with no value **deletes** the cookie when the response is set. It's very important // to delete this cookie ASAP, since otherwise the user can't sign in normally. locals.cookies.set(API_KEY_COOKIE_NAME); } if (opts.full_name != null && opts.first_name == null && opts.last_name == null) { name_6 = opts.full_name; i = name_6.lastIndexOf(" "); if (i === -1) { opts.first_name = ""; opts.last_name = name_6; } else { opts.first_name = name_6.slice(0, i).trim(); opts.last_name = name_6.slice(i).trim(); } } opts.first_name = (_a = opts.first_name) !== null && _a !== void 0 ? _a : ""; opts.last_name = (_b = opts.last_name) !== null && _b !== void 0 ? _b : ""; if (opts.emails != null) { opts.emails = (function () { var e_3, _a; var emails = typeof opts.emails == "string" ? [opts.emails] : opts.emails; var result = []; try { for (var emails_1 = __values(emails), emails_1_1 = emails_1.next(); !emails_1_1.done; emails_1_1 = emails_1.next()) { var x = emails_1_1.value; if (typeof x === "string" && misc.is_valid_email_address(x)) { result.push(x.toLowerCase()); } } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (emails_1_1 && !emails_1_1.done && (_a = emails_1.return)) _a.call(emails_1); } finally { if (e_3) throw e_3.error; } } return result; })(); } opts.id = "" + opts.id; // convert to string (id is often a number) _c.label = 1; case 1: _c.trys.push([1, 10, , 11]); // do we have a valid remember me cookie for a given account_id already? return [4 /*yield*/, this.check_remember_me_cookie(locals)]; case 2: // do we have a valid remember me cookie for a given account_id already? _c.sent(); // do we already have a passport? return [4 /*yield*/, this.check_passport_exists(opts, locals)]; case 3: // do we already have a passport? _c.sent(); // there might be accounts already with that email address return [4 /*yield*/, this.check_existing_emails(opts, locals)]; case 4: // there might be accounts already with that email address _c.sent(); // if no account yet → create one return [4 /*yield*/, this.maybe_create_account(opts, locals)]; case 5: // if no account yet → create one _c.sent(); // record a sign-in activity, if we deal with an existing account return [4 /*yield*/, this.maybe_record_sign_in(opts, locals)]; case 6: // record a sign-in activity, if we deal with an existing account _c.sent(); // deal with the case where user wants an API key return [4 /*yield*/, this.maybe_provision_api_key(locals)]; case 7: // deal with the case where user wants an API key _c.sent(); // check if user is banned? return [4 /*yield*/, this.is_user_banned(locals.account_id, locals.email_address)]; case 8: // check if user is banned? _c.sent(); // last step: set remember me cookie (for a new sign in) return [4 /*yield*/, this.handle_new_sign_in(opts, locals)]; case 9: // last step: set remember me cookie (for a new sign in) _c.sent(); // no exceptions → we're all good dbg("redirect the client to '" + locals.target + "'"); opts.res.redirect(locals.target); return [3 /*break*/, 11]; case 10: err_4 = _c.sent(); err_msg = "Error trying to login using " + opts.strategy + " -- " + err_4; dbg("sending error \"" + err_msg + "\""); opts.res.send(err_msg); return [3 /*break*/, 11]; case 11: return [2 /*return*/]; } }); }); }; // end passport_login // Check for a valid remember me cookie. If there is one, set // the account_id and has_valid_remember_me fields of locals. // If not, do NOTHING except log some debugging messages. Does // not raise an exception. See // https://github.com/sagemathinc/cocalc/issues/4767 // where this was failing the sign in if the remmeber me was // invalid in any way, which is overkill... since rememember_me // not being valid should just not entitle the user to having a // a specific account_id. PassportManager.prototype.check_remember_me_cookie = function (locals) { return __awaiter(this, void 0, void 0, function () { var dbg, value, x, hash, err, signed_in_mesg; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!locals.remember_me_cookie) return [2 /*return*/]; dbg = locals.dbg.extend("check_remember_me_cookie"); dbg("check if user has a valid remember_me cookie"); value = locals.remember_me_cookie; x = value.split("$"); if (x.length !== 4) { dbg("badly formatted remember_me cookie"); return [2 /*return*/]; } try { hash = generate_hash(x[0], x[1], x[2], x[3]);