smc-hub
Version:
CoCalc: Backend webserver component
935 lines (934 loc) • 74.5 kB
JavaScript
"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]);