UNPKG

@intuitionrobotics/user-account

Version:
395 lines 19 kB
"use strict"; 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()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AccountModule = exports.AccountsModule_Class = exports.Collection_Accounts = exports.Collection_Sessions = exports.Header_SessionId = void 0; const ts_common_1 = require("@intuitionrobotics/ts-common"); const backend_1 = require("@intuitionrobotics/firebase/backend"); const _imports_1 = require("./_imports"); const backend_2 = require("@intuitionrobotics/thunderstorm/backend"); const SecretsModule_1 = require("./SecretsModule"); const SamlModule_1 = require("./SamlModule"); const thunderstorm_1 = require("@intuitionrobotics/thunderstorm"); exports.Header_SessionId = new backend_2.HeaderKey(_imports_1.HeaderKey_SessionId, 404); exports.Collection_Sessions = "user-account--sessions"; exports.Collection_Accounts = "user-account--accounts"; const dispatch_onUserLogin = new ts_common_1.Dispatcher("__onUserLogin"); const dispatch_onNewUserRegistered = new ts_common_1.Dispatcher("__onNewUserRegistered"); function getUIAccount(account) { const { email, _id, createdTimestamp } = account; return { email, _id, createdTimestamp }; } class AccountsModule_Class extends ts_common_1.Module { constructor() { super("AccountsModule"); this.isAuthRequest = (request) => request.header(SecretsModule_1.SecretsModule.AUTHENTICATION_KEY) !== undefined; this.validateSession = (request, scopes, response) => __awaiter(this, void 0, void 0, function* () { if (this.isAuthRequest(request)) return this.validateAuthenticationHeader(request, scopes, response); return yield this.validateSessionId(exports.Header_SessionId.get(request)); }); this.TTLExpired = (session) => { const delta = (0, ts_common_1.currentTimeMillies)() - session.timestamp; let sessionTTLms = this.config.sessionTTLms.web; if (session.frontType === _imports_1.FrontType.App) return false; return delta > sessionTTLms || delta < 0; }; this.setDefaultConfig({ sessionTTLms: { web: ts_common_1.Day, app: ts_common_1.Day, jwt: 30 * ts_common_1.Minute }, jwtSecretKey: "TS_AUTH_SECRET" }); } __queryRequestInfo(request) { return __awaiter(this, void 0, void 0, function* () { let data; try { data = yield this.validateSession(request, []); } catch (e) { } return { key: this.getName(), data: data }; }); } init() { const firestore = backend_1.FirebaseModule.createAdminSession(this.config.projectId).getFirestore(); this.sessions = firestore.getCollection(exports.Collection_Sessions, ["userId"]); this.accounts = firestore.getCollection(exports.Collection_Accounts, ["email"]); } getUser(_email) { return __awaiter(this, void 0, void 0, function* () { const email = _email.toLowerCase(); return this.accounts.queryUnique({ where: { email }, select: ["email", "_id"] }); }); } getUsers(_emails) { return __awaiter(this, void 0, void 0, function* () { return (0, ts_common_1.batchActionParallel)(_emails, 10, (batchedEmails) => __awaiter(this, void 0, void 0, function* () { return this.accounts.query({ where: { email: { $in: batchedEmails.map(e => e.toLowerCase()) } }, select: ["email", "_id"] }); })); }); } listUsers() { return __awaiter(this, void 0, void 0, function* () { return this.accounts.getAll(["_id", "email"]); }); } listSessions() { return __awaiter(this, void 0, void 0, function* () { return this.sessions.getAll(["userId", "timestamp"]); }); } getSession(_email) { return __awaiter(this, void 0, void 0, function* () { const email = _email.toLowerCase(); return this.accounts.queryUnique({ where: { email } }); }); } querySessions(_email) { return __awaiter(this, void 0, void 0, function* () { const account = yield this.getSession(_email); if (!account) return; const sessions = yield this.sessions.query({ select: ["userId", "timestamp", "version", "frontType"], where: { userId: account._id } }); return sessions.map((session) => { return Object.assign(Object.assign({}, session), { isExpired: this.TTLExpired(session) }); }); }); } create(request, response) { return __awaiter(this, void 0, void 0, function* () { const account = yield this.createAccount(request); const session = yield this.login(request, response); yield dispatch_onNewUserRegistered.dispatchModuleAsync(getUIAccount(account)); return session; }); } upsert(request) { return __awaiter(this, void 0, void 0, function* () { let callback = () => Promise.resolve([]); const account = yield this.accounts.runInTransaction((transaction) => __awaiter(this, void 0, void 0, function* () { const existAccount = yield transaction.queryUnique(this.accounts, { where: { email: request.email } }); if (existAccount) return this.changePassword(request.email, request.password, transaction); callback = () => __awaiter(this, void 0, void 0, function* () { return dispatch_onNewUserRegistered.dispatchModuleAsync(getUIAccount(account)); }); return this.createImpl(request, transaction); })); yield this.loginValidate(request); yield callback(); return getUIAccount(account); }); } addNewAccount(email, password, password_check) { return __awaiter(this, void 0, void 0, function* () { let account; if (password && password_check) { account = yield this.createAccount({ password, password_check, email }); yield dispatch_onNewUserRegistered.dispatchModuleAsync(getUIAccount(account)); } else account = yield this.createSAML(email); return getUIAccount(account); }); } changePassword(userEmail, newPassword, _transaction) { return __awaiter(this, void 0, void 0, function* () { const email = userEmail.toLowerCase(); const processor = (transaction) => __awaiter(this, void 0, void 0, function* () { const account = yield transaction.queryUnique(this.accounts, { where: { email } }); if (!account) throw new backend_2.ApiException(422, "User with email does not exist"); if (!account.saltedPassword || !account.salt) throw new backend_2.ApiException(401, "Account login using SAML"); account.saltedPassword = (0, ts_common_1.hashPasswordWithSalt)(account.salt, newPassword); account._audit = (0, ts_common_1.auditBy)(email, 'Changed password'); return transaction.upsert(this.accounts, account); }); if (_transaction) return processor(_transaction); return this.accounts.runInTransaction(processor); }); } createAccount(request) { return __awaiter(this, void 0, void 0, function* () { request.email = request.email.toLowerCase(); (0, ts_common_1.validate)(request.email, ts_common_1.validateEmail); return this.accounts.runInTransaction((transaction) => __awaiter(this, void 0, void 0, function* () { const account = yield transaction.queryUnique(this.accounts, { where: { email: request.email } }); if (account) throw new backend_2.ApiException(422, "User with email already exists"); return this.createImpl(request, transaction); })); }); } createImpl(request, transaction) { const salt = (0, ts_common_1.generateHex)(32); const account = { _id: (0, ts_common_1.generateHex)(32), _audit: (0, ts_common_1.auditBy)(request.email), createdTimestamp: (0, ts_common_1.currentTimeMillies)(), email: request.email, salt, saltedPassword: (0, ts_common_1.hashPasswordWithSalt)(salt, request.password) }; return transaction.insert(this.accounts, account); } logout(sessionId) { return __awaiter(this, void 0, void 0, function* () { const query = { where: { sessionId } }; yield this.sessions.deleteUnique(query); }); } logoutAccount(accountId) { return __awaiter(this, void 0, void 0, function* () { yield this.sessions.delete({ where: { userId: accountId } }); }); } login(request, response) { return __awaiter(this, void 0, void 0, function* () { return this.loginValidate(request, response); }); } loginValidate(request, response) { return __awaiter(this, void 0, void 0, function* () { request.email = request.email.toLowerCase(); const query = { where: { email: request.email } }; const account = yield this.accounts.queryUnique(query); if (!account) throw new backend_2.ApiException(401, "account does not exists"); if (!account.saltedPassword || !account.salt) throw new backend_2.ApiException(401, "Account login using SAML"); if (account.saltedPassword !== (0, ts_common_1.hashPasswordWithSalt)(account.salt, request.password)) throw new backend_2.ApiException(401, "wrong username or password"); if (!account._id) { account._id = (0, ts_common_1.generateHex)(32); yield this.accounts.upsert(account); } let sessionWithAccountId; if (response) { sessionWithAccountId = yield this.upsertSession(account, request.frontType); this.setJWTinResp(response, sessionWithAccountId.jwt); } yield dispatch_onUserLogin.dispatchModuleAsync(getUIAccount(account)); return sessionWithAccountId; }); } loginSAML(__email) { return __awaiter(this, void 0, void 0, function* () { const _email = __email.toLowerCase(); const account = yield this.createSAML(_email); const sessionWithAccountId = yield this.upsertSession(account); yield dispatch_onUserLogin.dispatchModuleAsync(getUIAccount(account)); return sessionWithAccountId; }); } createSAML(__email) { return __awaiter(this, void 0, void 0, function* () { const _email = __email.toLowerCase(); const query = { where: { email: _email } }; let dispatchEvent = false; const toRet = yield this.accounts.runInTransaction((transaction) => __awaiter(this, void 0, void 0, function* () { const account = yield transaction.queryUnique(this.accounts, query); if (account === null || account === void 0 ? void 0 : account._id) return account; const _account = Object.assign({ _id: (0, ts_common_1.generateHex)(32), _audit: (0, ts_common_1.auditBy)(_email), createdTimestamp: (0, ts_common_1.currentTimeMillies)(), email: _email }, account); dispatchEvent = true; return transaction.upsert(this.accounts, _account); })); if (dispatchEvent) yield dispatch_onNewUserRegistered.dispatchModuleAsync(getUIAccount(toRet)); return toRet; }); } verifyAccount(account) { if (!account) throw new backend_2.ApiException(401, 'Missing account in token payload'); const email = account['email']; if (!email || typeof email !== 'string') throw new backend_2.ApiException(401, 'Missing email in token payload'); const _id = account['_id']; if (!_id || typeof _id !== 'string') throw new backend_2.ApiException(401, 'Missing _id in token payload'); return { _id, email }; } validateAuthenticationHeader(request, scopes, response) { return __awaiter(this, void 0, void 0, function* () { const token = SecretsModule_1.SecretsModule.validateRequest(request, scopes); const payload = token.payload; const isExpired = SecretsModule_1.SecretsModule.isExpired(token); const sessionId = payload.sessionId; if (!sessionId) throw new backend_2.ApiException(401, `Missing session id in token ${JSON.stringify(payload)}`); if (!isExpired) { const account = payload.account; this.verifyAccount(account); return account; } const dbAccount = yield this.validateSessionId(sessionId); if (response) { const jwt = this.generateJWT(dbAccount, sessionId); this.setJWTinResp(response, jwt); } return dbAccount; }); } setJWTinResp(response, jwt) { // Set in header response response.setHeaders({ [thunderstorm_1.HeaderKey_JWT]: jwt }); } generateJWT(account, sessionId) { return SecretsModule_1.SecretsModule.generateJwt({ account, sessionId, exp: (0, ts_common_1.currentTimeMillies)() + this.config.sessionTTLms.jwt }, this.config.jwtSecretKey); } validateSessionId(sessionId) { return __awaiter(this, void 0, void 0, function* () { const query = { where: { sessionId } }; const session = yield this.sessions.queryUnique(query); if (!session) throw new backend_2.ApiException(401, `Invalid session id: ${sessionId}`); if (this.TTLExpired(session)) throw new backend_2.ApiException(401, "Session timed out"); return yield this.getUserEmailFromSession(session); }); } getUserEmailFromSession(session) { return __awaiter(this, void 0, void 0, function* () { const account = yield this.accounts.queryUnique({ where: { _id: session.userId } }); if (!account) { yield this.sessions.deleteItem(session); throw new backend_2.ApiException(403, `No user found for session: ${(0, ts_common_1.__stringify)(session)}`); } return getUIAccount(account); }); } getUserEmailFromUserId(userId) { return __awaiter(this, void 0, void 0, function* () { const account = yield this.accounts.queryUnique({ where: { _id: userId } }); if (!account) throw new backend_2.ApiException(403, `No user found for session: ${userId}`); return getUIAccount(account); }); } getAccountFromParams(p) { return __awaiter(this, void 0, void 0, function* () { if (typeof p === "string") return this.getUserEmailFromUserId(p); return getUIAccount(p); }); } upsertSession(p, frontType) { return __awaiter(this, void 0, void 0, function* () { const account = yield this.getAccountFromParams(p); const session = yield this.getSessionFromAccount(account, frontType); const sessionId = session.sessionId; return { sessionId, jwt: this.generateJWT(account, sessionId), email: account.email, _id: account._id, createdTimestamp: account.createdTimestamp }; }); } ; getSessionFromAccount(account, frontType) { return __awaiter(this, void 0, void 0, function* () { const session = yield this.sessions.queryUnique({ where: { userId: account._id } }); if (session && !this.TTLExpired(session)) return session; const _session = { sessionId: (0, ts_common_1.generateHex)(64), timestamp: (0, ts_common_1.currentTimeMillies)(), userId: account._id, }; if (frontType) _session.frontType = frontType; return this.sessions.upsert(_session); }); } assertApi(body, response) { return __awaiter(this, void 0, void 0, function* () { const options = { request_body: body }; try { const data = yield SamlModule_1.SamlModule.assert(options); this.logDebug(`Got data from assertion ${(0, ts_common_1.__stringify)(data)}`); const email = data.userId; const loginData = yield exports.AccountModule.loginSAML(email); let redirectUrl = data.loginContext[_imports_1.QueryParam_RedirectUrl]; redirectUrl = redirectUrl.replace(new RegExp(_imports_1.QueryParam_SessionId.toUpperCase(), "g"), loginData.sessionId); redirectUrl = redirectUrl.replace(new RegExp(_imports_1.QueryParam_Email.toUpperCase(), "g"), email); redirectUrl = redirectUrl.replace(new RegExp(_imports_1.QueryParam_JWT.toUpperCase(), "g"), loginData.jwt); return yield response.redirect(302, redirectUrl); } catch (error) { throw new backend_2.ApiException(401, 'Error authenticating user', error); } }); } } exports.AccountsModule_Class = AccountsModule_Class; exports.AccountModule = new AccountsModule_Class(); //# sourceMappingURL=AccountModule.js.map