@intuitionrobotics/user-account
Version:
395 lines • 19 kB
JavaScript
"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