supertokens-node
Version:
NodeJS driver for SuperTokens core
281 lines (280 loc) • 12.4 kB
JavaScript
;
/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateAndGetMFARelatedInfoInSession = void 0;
exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput;
const error_1 = __importDefault(require("../session/error"));
const types_1 = require("./types");
const utils_1 = require("../multitenancy/utils");
function validateAndNormaliseUserInput(config) {
if (
(config === null || config === void 0 ? void 0 : config.firstFactors) !== undefined &&
(config === null || config === void 0 ? void 0 : config.firstFactors.length) === 0
) {
throw new Error("'firstFactors' can be either undefined or a non-empty array");
}
let override = Object.assign(
{
functions: (originalImplementation) => originalImplementation,
apis: (originalImplementation) => originalImplementation,
},
config === null || config === void 0 ? void 0 : config.override
);
return {
firstFactors: config === null || config === void 0 ? void 0 : config.firstFactors,
override,
};
}
// This function is to reuse a piece of code that is needed in multiple places
const updateAndGetMFARelatedInfoInSession = async function (input) {
let sessionRecipeUserId;
let tenantId;
let accessTokenPayload;
let sessionHandle;
if ("session" in input) {
sessionRecipeUserId = input.session.getRecipeUserId(input.userContext);
tenantId = input.session.getTenantId(input.userContext);
accessTokenPayload = input.session.getAccessTokenPayload(input.userContext);
sessionHandle = input.session.getHandle(input.userContext);
} else {
sessionRecipeUserId = input.sessionRecipeUserId;
tenantId = input.tenantId;
accessTokenPayload = input.accessTokenPayload;
sessionHandle = accessTokenPayload.sessionHandle;
}
let updatedClaimVal = false;
let mfaClaimValue = input.stInstance
.getRecipeInstanceOrThrow("multifactorauth")
.multiFactorAuthClaim.getValueFromPayload(accessTokenPayload);
if (input.updatedFactorId) {
if (mfaClaimValue === undefined) {
updatedClaimVal = true;
mfaClaimValue = {
c: {
[input.updatedFactorId]: Math.floor(Date.now() / 1000),
},
v: true, // updated later in the function
};
} else {
updatedClaimVal = true;
mfaClaimValue.c[input.updatedFactorId] = Math.floor(Date.now() / 1000);
}
}
if (mfaClaimValue === undefined) {
// it should be fine to get the user multiple times since the caching will de-duplicate these requests
const sessionUser = await input.stInstance
.getRecipeInstanceOrThrow("accountlinking")
.recipeInterfaceImpl.getUser({ userId: sessionRecipeUserId.getAsString(), userContext: input.userContext });
if (sessionUser === undefined) {
throw new error_1.default({
type: error_1.default.UNAUTHORISED,
message: "Session user not found",
});
}
// This can happen with older session, because we did not add MFA claims previously.
// We try to determine best possible factorId based on the session's recipe user id.
const sessionInfo = await input.stInstance
.getRecipeInstanceOrThrow("session")
.recipeInterfaceImpl.getSessionInformation({ sessionHandle, userContext: input.userContext });
if (sessionInfo === undefined) {
throw new error_1.default({
type: error_1.default.UNAUTHORISED,
message: "Session not found",
});
}
const firstFactorTime = sessionInfo.timeCreated;
let computedFirstFactorIdForSession = undefined;
for (const lM of sessionUser.loginMethods) {
if (lM.recipeUserId.getAsString() === sessionRecipeUserId.getAsString()) {
if (lM.recipeId === "emailpassword") {
let validRes = await (0, utils_1.isValidFirstFactor)(
input.stInstance,
tenantId,
types_1.FactorIds.EMAILPASSWORD,
input.userContext
);
if (validRes.status === "TENANT_NOT_FOUND_ERROR") {
throw new error_1.default({
type: error_1.default.UNAUTHORISED,
message: "Tenant not found",
});
} else if (validRes.status === "OK") {
computedFirstFactorIdForSession = types_1.FactorIds.EMAILPASSWORD;
break;
}
} else if (lM.recipeId === "thirdparty") {
let validRes = await (0, utils_1.isValidFirstFactor)(
input.stInstance,
tenantId,
types_1.FactorIds.THIRDPARTY,
input.userContext
);
if (validRes.status === "TENANT_NOT_FOUND_ERROR") {
throw new error_1.default({
type: error_1.default.UNAUTHORISED,
message: "Tenant not found",
});
} else if (validRes.status === "OK") {
computedFirstFactorIdForSession = types_1.FactorIds.THIRDPARTY;
break;
}
} else {
let factorsToCheck = [];
if (lM.email !== undefined) {
factorsToCheck.push(types_1.FactorIds.LINK_EMAIL);
factorsToCheck.push(types_1.FactorIds.OTP_EMAIL);
}
if (lM.phoneNumber !== undefined) {
factorsToCheck.push(types_1.FactorIds.LINK_PHONE);
factorsToCheck.push(types_1.FactorIds.OTP_PHONE);
}
for (const factorId of factorsToCheck) {
let validRes = await (0, utils_1.isValidFirstFactor)(
input.stInstance,
tenantId,
factorId,
input.userContext
);
if (validRes.status === "TENANT_NOT_FOUND_ERROR") {
throw new error_1.default({
type: error_1.default.UNAUTHORISED,
message: "Tenant not found",
});
} else if (validRes.status === "OK") {
computedFirstFactorIdForSession = factorId;
break;
}
}
if (computedFirstFactorIdForSession !== undefined) {
break;
}
}
}
}
if (computedFirstFactorIdForSession === undefined) {
throw new error_1.default({
type: error_1.default.UNAUTHORISED,
message: "Incorrect login method used",
});
}
updatedClaimVal = true;
mfaClaimValue = {
c: {
[computedFirstFactorIdForSession]: firstFactorTime,
},
v: true, // updated later in this function
};
}
const completedFactors = mfaClaimValue.c;
let userProm;
function userGetter() {
if (userProm) {
return userProm;
}
userProm = input.stInstance
.getRecipeInstanceOrThrow("accountlinking")
.recipeInterfaceImpl.getUser({ userId: sessionRecipeUserId.getAsString(), userContext: input.userContext })
.then((sessionUser) => {
if (sessionUser === undefined) {
throw new error_1.default({
type: error_1.default.UNAUTHORISED,
message: "Session user not found",
});
}
return sessionUser;
});
return userProm;
}
const mfaRequirementsForAuth = await input.stInstance
.getRecipeInstanceOrThrow("multifactorauth")
.recipeInterfaceImpl.getMFARequirementsForAuth({
accessTokenPayload,
tenantId,
get user() {
return userGetter();
},
get factorsSetUpForUser() {
return userGetter().then((user) =>
input.stInstance
.getRecipeInstanceOrThrow("multifactorauth")
.recipeInterfaceImpl.getFactorsSetupForUser({
user,
userContext: input.userContext,
})
);
},
get requiredSecondaryFactorsForUser() {
return userGetter().then((sessionUser) => {
if (sessionUser === undefined) {
throw new error_1.default({
type: error_1.default.UNAUTHORISED,
message: "Session user not found",
});
}
return input.stInstance
.getRecipeInstanceOrThrow("multifactorauth")
.recipeInterfaceImpl.getRequiredSecondaryFactorsForUser({
userId: sessionUser.id,
userContext: input.userContext,
});
});
},
get requiredSecondaryFactorsForTenant() {
return input.stInstance
.getRecipeInstanceOrThrow("multitenancy")
.recipeInterfaceImpl.getTenant({ tenantId, userContext: input.userContext })
.then((tenantInfo) => {
var _a;
if (tenantInfo === undefined) {
throw new error_1.default({
type: error_1.default.UNAUTHORISED,
message: "Tenant not found",
});
}
return (_a = tenantInfo.requiredSecondaryFactors) !== null && _a !== void 0 ? _a : [];
});
},
completedFactors,
userContext: input.userContext,
});
const areAuthReqsComplete =
input.stInstance
.getRecipeInstanceOrThrow("multifactorauth")
.multiFactorAuthClaim.getNextSetOfUnsatisfiedFactors(completedFactors, mfaRequirementsForAuth).factorIds
.length === 0;
if (mfaClaimValue.v !== areAuthReqsComplete) {
updatedClaimVal = true;
mfaClaimValue.v = areAuthReqsComplete;
}
if ("session" in input && updatedClaimVal) {
await input.session.setClaimValue(
input.stInstance.getRecipeInstanceOrThrow("multifactorauth").multiFactorAuthClaim,
mfaClaimValue,
input.userContext
);
}
return {
completedFactors,
mfaRequirementsForAuth,
isMFARequirementsForAuthSatisfied: mfaClaimValue.v,
};
};
exports.updateAndGetMFARelatedInfoInSession = updateAndGetMFARelatedInfoInSession;