UNPKG

supertokens-node

Version:
501 lines (500 loc) 27.8 kB
"use strict"; /* Copyright (c) 2021, 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 __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (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 () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("./utils"); const plugins_1 = require("./plugins"); const querier_1 = require("./querier"); const constants_1 = require("./constants"); const normalisedURLDomain_1 = __importDefault(require("./normalisedURLDomain")); const normalisedURLPath_1 = __importDefault(require("./normalisedURLPath")); const error_1 = __importStar(require("./error")); const logger_1 = require("./logger"); const postSuperTokensInitCallbacks_1 = require("./postSuperTokensInitCallbacks"); const constants_2 = require("./recipe/multitenancy/constants"); class SuperTokens { constructor(config) { var _a, _b, _c, _d, _e, _f; this.handleAPI = async (matchedRecipe, id, tenantId, request, response, path, method, userContext) => { return await matchedRecipe.handleAPIRequest(id, tenantId, request, response, path, method, userContext); }; this.getAllCORSHeaders = () => { let headerSet = new Set(); headerSet.add(constants_1.HEADER_RID); headerSet.add(constants_1.HEADER_FDI); this.recipeModules.forEach((recipe) => { let headers = recipe.getAllCORSHeaders(); headers.forEach((h) => { headerSet.add(h); }); }); return Array.from(headerSet); }; this.getUserCount = async (includeRecipeIds, tenantId, userContext) => { let querier = querier_1.Querier.getNewInstanceOrThrowError(this); let apiVersion = await querier.getAPIVersion(userContext); if ((0, utils_1.maxVersion)(apiVersion, "2.7") === "2.7") { throw new Error("Please use core version >= 3.5 to call this function. Otherwise, you can call <YourRecipe>.getUserCount() instead (for example, EmailPassword.getUserCount())"); } let includeRecipeIdsStr = undefined; if (includeRecipeIds !== undefined) { includeRecipeIdsStr = includeRecipeIds.join(","); } let response = await querier.sendGetRequest({ path: "/<tenantId>/users/count", params: { tenantId: tenantId === undefined ? constants_2.DEFAULT_TENANT_ID : tenantId, }, }, { includeRecipeIds: includeRecipeIdsStr, includeAllTenants: tenantId === undefined, }, userContext); return Number(response.count); }; this.createUserIdMapping = async function (input) { let querier = querier_1.Querier.getNewInstanceOrThrowError(this); let cdiVersion = await querier.getAPIVersion(input.userContext); if ((0, utils_1.maxVersion)("2.15", cdiVersion) === cdiVersion) { // create userId mapping is only available >= CDI 2.15 return await querier.sendPostRequest("/recipe/userid/map", { superTokensUserId: input.superTokensUserId, externalUserId: input.externalUserId, externalUserIdInfo: input.externalUserIdInfo, force: input.force, }, input.userContext); } else { throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); } }; this.getUserIdMapping = async function (input) { let querier = querier_1.Querier.getNewInstanceOrThrowError(this); let cdiVersion = await querier.getAPIVersion(input.userContext); if ((0, utils_1.maxVersion)("2.15", cdiVersion) === cdiVersion) { // create userId mapping is only available >= CDI 2.15 let response = await querier.sendGetRequest("/recipe/userid/map", { userId: input.userId, userIdType: input.userIdType, }, input.userContext); return response; } else { throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); } }; this.deleteUserIdMapping = async function (input) { let querier = querier_1.Querier.getNewInstanceOrThrowError(this); let cdiVersion = await querier.getAPIVersion(input.userContext); if ((0, utils_1.maxVersion)("2.15", cdiVersion) === cdiVersion) { return await querier.sendPostRequest("/recipe/userid/map/remove", { userId: input.userId, userIdType: input.userIdType, force: input.force, }, input.userContext); } else { throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); } }; this.updateOrDeleteUserIdMappingInfo = async function (input) { let querier = querier_1.Querier.getNewInstanceOrThrowError(this); let cdiVersion = await querier.getAPIVersion(input.userContext); if ((0, utils_1.maxVersion)("2.15", cdiVersion) === cdiVersion) { return await querier.sendPutRequest("/recipe/userid/external-user-id-info", { userId: input.userId, userIdType: input.userIdType, externalUserIdInfo: input.externalUserIdInfo || null, }, {}, input.userContext); } else { throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); } }; this.middleware = async (request, response, userContext) => { (0, logger_1.logDebugMessage)("middleware: Started"); let path = this.appInfo.apiGatewayPath.appendPath(new normalisedURLPath_1.default(request.getOriginalURL())); let method = (0, utils_1.normaliseHttpMethod)(request.getMethod()); const handlerFromApis = this.pluginRouteHandlers.find((handler) => handler.path === path.getAsStringDangerous() && handler.method === method); if (handlerFromApis) { let session = undefined; if (handlerFromApis.verifySessionOptions !== undefined) { session = await this.getRecipeInstanceOrThrow("session").verifySession(handlerFromApis.verifySessionOptions, request, response, userContext); } (0, logger_1.logDebugMessage)("middleware: Request being handled by plugin. ID is: " + handlerFromApis.pluginId); try { await handlerFromApis.handler(request, response, session, userContext); } catch (err) { (0, logger_1.logDebugMessage)("middleware: Error from plugin, transforming to SuperTokensError. Plugin ID: " + handlerFromApis.pluginId); // transform errors to SuperTokensError to be handled by the errorHandler throw (0, error_1.transformErrorToSuperTokensError)(err); } return true; } // if the prefix of the URL doesn't match the base path, we skip if (!path.startsWith(this.appInfo.apiBasePath)) { (0, logger_1.logDebugMessage)("middleware: Not handling because request path did not start with config path. Request path: " + path.getAsStringDangerous()); return false; } let requestRID = (0, utils_1.getRidFromHeader)(request); (0, logger_1.logDebugMessage)("middleware: requestRID is: " + requestRID); if (requestRID === "anti-csrf") { // see https://github.com/supertokens/supertokens-node/issues/202 requestRID = undefined; } async function handleWithoutRid(recipeModules) { let bestMatch = undefined; for (let i = 0; i < recipeModules.length; i++) { (0, logger_1.logDebugMessage)("middleware: Checking recipe ID for match: " + recipeModules[i].getRecipeId() + " with path: " + path.getAsStringDangerous() + " and method: " + method); let idResult = await recipeModules[i].returnAPIIdIfCanHandleRequest(path, method, userContext); if (idResult !== undefined) { // The request path may or may not include the tenantId. `returnAPIIdIfCanHandleRequest` handles both cases. // If one recipe matches with tenantId and another matches exactly, we prefer the exact match. if (bestMatch === undefined || idResult.exactMatch) { bestMatch = { recipeModule: recipeModules[i], idResult: idResult, }; } if (idResult.exactMatch) { break; } } } if (bestMatch !== undefined) { const { idResult, recipeModule } = bestMatch; (0, logger_1.logDebugMessage)("middleware: Request being handled by recipe. ID is: " + idResult.id); let requestHandled = await recipeModule.handleAPIRequest(idResult.id, idResult.tenantId, request, response, path, method, userContext); if (!requestHandled) { (0, logger_1.logDebugMessage)("middleware: Not handled because API returned requestHandled as false"); return false; } (0, logger_1.logDebugMessage)("middleware: Ended"); return true; } (0, logger_1.logDebugMessage)("middleware: Not handling because no recipe matched"); return false; } if (requestRID !== undefined) { // We have the below matching based on RID header cause // we still support older FDIs (< 1.20). In the newer FDIs, // the API paths across all recipes are unique. let matchedRecipe = []; // we loop through all recipe modules to find the one with the matching rId for (let i = 0; i < this.recipeModules.length; i++) { (0, logger_1.logDebugMessage)("middleware: Checking recipe ID for match: " + this.recipeModules[i].getRecipeId()); if (this.recipeModules[i].getRecipeId() === requestRID) { matchedRecipe.push(this.recipeModules[i]); } else if (requestRID === "thirdpartyemailpassword") { if (this.recipeModules[i].getRecipeId() === "thirdparty" || this.recipeModules[i].getRecipeId() === "emailpassword") { matchedRecipe.push(this.recipeModules[i]); } } else if (requestRID === "thirdpartypasswordless") { if (this.recipeModules[i].getRecipeId() === "thirdparty" || this.recipeModules[i].getRecipeId() === "passwordless") { matchedRecipe.push(this.recipeModules[i]); } } } if (matchedRecipe.length === 0) { (0, logger_1.logDebugMessage)("middleware: Not handling based on rid match. Trying without rid."); return handleWithoutRid(this.recipeModules); } for (let i = 0; i < matchedRecipe.length; i++) { (0, logger_1.logDebugMessage)("middleware: Matched with recipe IDs: " + matchedRecipe[i].getRecipeId()); } let idResult = undefined; let finalMatchedRecipe = undefined; for (let i = 0; i < matchedRecipe.length; i++) { // Here we assume that if there are multiple recipes that have matched, then // the path and methods of the APIs exposed via those recipes is unique. let currIdResult = await matchedRecipe[i].returnAPIIdIfCanHandleRequest(path, method, userContext); if (currIdResult !== undefined) { if (idResult === undefined || // The request path may or may not include the tenantId. `returnAPIIdIfCanHandleRequest` handles both cases. // If one recipe matches with tenantId and another matches exactly, we prefer the exact match. (currIdResult.exactMatch === true && idResult.exactMatch === false)) { finalMatchedRecipe = matchedRecipe[i]; idResult = currIdResult; } else { throw new Error("Two recipes have matched the same API path and method! This is a bug in the SDK. Please contact support."); } } } if (idResult === undefined || finalMatchedRecipe === undefined) { return handleWithoutRid(this.recipeModules); } (0, logger_1.logDebugMessage)("middleware: Request being handled by recipe. ID is: " + idResult.id); // give task to the matched recipe let requestHandled = await finalMatchedRecipe.handleAPIRequest(idResult.id, idResult.tenantId, request, response, path, method, userContext); if (!requestHandled) { (0, logger_1.logDebugMessage)("middleware: Not handled because API returned requestHandled as false"); return false; } (0, logger_1.logDebugMessage)("middleware: Ended"); return true; } else { return handleWithoutRid(this.recipeModules); } }; this.errorHandler = async (err, request, response, userContext) => { (0, logger_1.logDebugMessage)("errorHandler: Started"); if (error_1.default.isErrorFromSuperTokens(err)) { (0, logger_1.logDebugMessage)("errorHandler: Error is from SuperTokens recipe. Message: " + err.message); if (err.type === error_1.default.BAD_INPUT_ERROR) { (0, logger_1.logDebugMessage)("errorHandler: Sending 400 status code response"); return (0, utils_1.sendNon200ResponseWithMessage)(response, err.message, 400); } if (err.type === error_1.default.PLUGIN_ERROR) { const code = "code" in err && err.code ? err.code : 400; (0, logger_1.logDebugMessage)(`errorHandler: Sending ${code} status code response`); return (0, utils_1.sendNon200ResponseWithMessage)(response, err.message, code); } if (err.type === error_1.default.UNKNOWN_ERROR) { (0, logger_1.logDebugMessage)("errorHandler: Sending 500 status code response"); return (0, utils_1.sendNon200ResponseWithMessage)(response, err.message, 500); } for (let i = 0; i < this.recipeModules.length; i++) { (0, logger_1.logDebugMessage)("errorHandler: Checking recipe for match: " + this.recipeModules[i].getRecipeId()); if (this.recipeModules[i].isErrorFromThisRecipe(err)) { (0, logger_1.logDebugMessage)("errorHandler: Matched with recipeID: " + this.recipeModules[i].getRecipeId()); return await this.recipeModules[i].handleError(err, request, response, userContext); } } } throw err; }; this.getRequestFromUserContext = (userContext) => { if (userContext === undefined) { return undefined; } if (typeof userContext !== "object") { return undefined; } if (userContext._default === undefined) { return undefined; } if (userContext._default.request === undefined) { return undefined; } return userContext._default.request; }; this.getRecipeInstanceOrThrow = (recipeId) => { const recipe = this.recipeModules.find((recipe) => recipe.getRecipeId() === recipeId); if (recipe === undefined) { throw new Error(`Recipe ${recipeId} not initialised. Did you forget to call the add it to the recipe list?`); } return recipe; }; this.getRecipeInstance = (recipeId) => { const recipe = this.recipeModules.find((recipe) => recipe.getRecipeId() === recipeId); if (recipe === undefined) { return undefined; } return recipe; }; this.appInfo = (0, utils_1.normaliseInputAppInfoOrThrowError)(config.appInfo); const { config: _config, pluginRouteHandlers, overrideMaps, } = (0, plugins_1.loadPlugins)({ config, plugins: (_b = (_a = config.experimental) === null || _a === void 0 ? void 0 : _a.plugins) !== null && _b !== void 0 ? _b : [], normalisedAppInfo: this.appInfo, }); config = Object.assign({}, _config); this.pluginRouteHandlers = pluginRouteHandlers; this.pluginOverrideMaps = overrideMaps; if (config.debug === true) { (0, logger_1.enableDebugLogs)(); } (0, logger_1.logDebugMessage)("Started SuperTokens with debug logging (supertokens.init called)"); const originToPrint = config.appInfo.origin === undefined ? undefined : typeof config.appInfo.origin === "string" ? config.appInfo.origin : "function"; (0, logger_1.logDebugMessage)("appInfo: " + JSON.stringify(Object.assign(Object.assign({}, config.appInfo), { origin: originToPrint }))); this.framework = config.framework !== undefined ? config.framework : "express"; (0, logger_1.logDebugMessage)("framework: " + this.framework); this.supertokens = config.supertokens; querier_1.Querier.init((_c = config.supertokens) === null || _c === void 0 ? void 0 : _c.connectionURI.split(";").filter((h) => h !== "").map((h) => { return { domain: new normalisedURLDomain_1.default(h.trim()), basePath: new normalisedURLPath_1.default(h.trim()), }; }), (_d = config.supertokens) === null || _d === void 0 ? void 0 : _d.apiKey, (_e = config.supertokens) === null || _e === void 0 ? void 0 : _e.networkInterceptor, (_f = config.supertokens) === null || _f === void 0 ? void 0 : _f.disableCoreCallCache); if (config.recipeList === undefined || config.recipeList.length === 0) { throw new Error("Please provide at least one recipe to the supertokens.init function call"); } // @ts-ignore if (config.recipeList.includes(undefined)) { // related to issue #270. If user makes mistake by adding empty items in the recipeList, this will catch the mistake and throw relevant error throw new Error("Please remove empty items from recipeList"); } this.isInServerlessEnv = config.isInServerlessEnv === undefined ? false : config.isInServerlessEnv; let multitenancyFound = false; let totpFound = false; let userMetadataFound = false; let multiFactorAuthFound = false; let oauth2Found = false; let openIdFound = false; let jwtFound = false; let accountLinkingFound = false; // Multitenancy recipe is an always initialized recipe and needs to be imported this way // so that there is no circular dependency. Otherwise there would be cyclic dependency // between `supertokens.ts` -> `recipeModule.ts` -> `multitenancy/recipe.ts` let MultitenancyRecipe = require("./recipe/multitenancy/recipe").default; let UserMetadataRecipe = require("./recipe/usermetadata/recipe").default; let MultiFactorAuthRecipe = require("./recipe/multifactorauth/recipe").default; let TotpRecipe = require("./recipe/totp/recipe").default; let OAuth2ProviderRecipe = require("./recipe/oauth2provider/recipe").default; let OpenIdRecipe = require("./recipe/openid/recipe").default; let jwtRecipe = require("./recipe/jwt/recipe").default; let AccountLinkingRecipe = require("./recipe/accountlinking/recipe").default; this.recipeModules = config.recipeList.map((func) => { const recipeModule = func(this, this.appInfo, this.isInServerlessEnv, this.pluginOverrideMaps); if (recipeModule.getRecipeId() === MultitenancyRecipe.RECIPE_ID) { multitenancyFound = true; } else if (recipeModule.getRecipeId() === UserMetadataRecipe.RECIPE_ID) { userMetadataFound = true; } else if (recipeModule.getRecipeId() === MultiFactorAuthRecipe.RECIPE_ID) { multiFactorAuthFound = true; } else if (recipeModule.getRecipeId() === TotpRecipe.RECIPE_ID) { totpFound = true; } else if (recipeModule.getRecipeId() === OAuth2ProviderRecipe.RECIPE_ID) { oauth2Found = true; } else if (recipeModule.getRecipeId() === OpenIdRecipe.RECIPE_ID) { openIdFound = true; } else if (recipeModule.getRecipeId() === jwtRecipe.RECIPE_ID) { jwtFound = true; } else if (recipeModule.getRecipeId() === AccountLinkingRecipe.RECIPE_ID) { accountLinkingFound = true; } return recipeModule; }); if (!accountLinkingFound) { this.recipeModules.push(AccountLinkingRecipe.init()(this, this.appInfo, this.isInServerlessEnv, this.pluginOverrideMaps)); } if (!jwtFound) { this.recipeModules.push(jwtRecipe.init()(this, this.appInfo, this.isInServerlessEnv, this.pluginOverrideMaps)); } if (!openIdFound) { this.recipeModules.push(OpenIdRecipe.init()(this, this.appInfo, this.isInServerlessEnv, this.pluginOverrideMaps)); } if (!multitenancyFound) { this.recipeModules.push(MultitenancyRecipe.init()(this, this.appInfo, this.isInServerlessEnv, this.pluginOverrideMaps)); } if (totpFound && !multiFactorAuthFound) { throw new Error("Please initialize the MultiFactorAuth recipe to use TOTP."); } if (!userMetadataFound) { // Initializing the user metadata recipe shouldn't cause any issues/side effects and it doesn't expose any APIs, // so we can just always initialize it this.recipeModules.push(UserMetadataRecipe.init()(this, this.appInfo, this.isInServerlessEnv, this.pluginOverrideMaps)); } // While for many usecases account linking recipe also has to be initialized for MFA to function well, // the app doesn't have to do that if they only use TOTP (which shouldn't be that uncommon) // To let those cases function without initializing account linking we do not check it here, but when // the authentication endpoints are called. // We've decided to always initialize the OAuth2Provider recipe if (!oauth2Found) { this.recipeModules.push(OAuth2ProviderRecipe.init()(this, this.appInfo, this.isInServerlessEnv, this.pluginOverrideMaps)); } this.telemetryEnabled = config.telemetry === undefined ? !(0, utils_1.isTestEnv)() : config.telemetry; } static init(config) { if (SuperTokens.instance === undefined) { SuperTokens.instance = new SuperTokens(config); postSuperTokensInitCallbacks_1.PostSuperTokensInitCallbacks.runPostInitCallbacks(); } } static reset() { if (!(0, utils_1.isTestEnv)()) { throw new Error("calling testing function in non testing env"); } // We call reset the following recipes because they are auto-initialized // and there is no case where we want to reset the SuperTokens instance but not // the recipes. let OAuth2ProviderRecipe = require("./recipe/oauth2provider/recipe").default; OAuth2ProviderRecipe.reset(); let OpenIdRecipe = require("./recipe/openid/recipe").default; OpenIdRecipe.reset(); let JWTRecipe = require("./recipe/jwt/recipe").default; JWTRecipe.reset(); querier_1.Querier.reset(); SuperTokens.instance = undefined; } static getInstanceOrThrowError() { if (SuperTokens.instance !== undefined) { return SuperTokens.instance; } throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); } } exports.default = SuperTokens;