UNPKG

supertokens-node

Version:
378 lines (377 loc) 20.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getSessionFromRequest = getSessionFromRequest; exports.getAccessTokenFromRequest = getAccessTokenFromRequest; exports.refreshSessionInRequest = refreshSessionInRequest; exports.createNewSessionInRequest = createNewSessionInRequest; const framework_1 = __importDefault(require("../../framework")); const utils_1 = require("../../utils"); const logger_1 = require("../../logger"); const constants_1 = require("./constants"); const cookieAndHeaders_1 = require("./cookieAndHeaders"); const jwt_1 = require("./jwt"); const accessToken_1 = require("./accessToken"); const error_1 = __importDefault(require("./error")); const normalisedURLDomain_1 = require("../../normalisedURLDomain"); // We are defining this here (and not exporting it) to reduce the scope of legacy code const LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME = "sIdRefreshToken"; async function getSessionFromRequest({ req, res, config, recipeInterfaceImpl, recipeInstance, stInstance, options, userContext, }) { (0, logger_1.logDebugMessage)("getSession: Started"); const configuredFramework = stInstance.framework; if (configuredFramework !== "custom") { if (!req.wrapperUsed) { req = framework_1.default[configuredFramework].wrapRequest(req); } if (!res.wrapperUsed) { res = framework_1.default[configuredFramework].wrapResponse(res); } } userContext = (0, utils_1.setRequestInUserContextIfNotDefined)(userContext, req); (0, logger_1.logDebugMessage)("getSession: Wrapping done"); // This token isn't handled by getToken to limit the scope of this legacy/migration code if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { (0, logger_1.logDebugMessage)("getSession: Throwing TRY_REFRESH_TOKEN because the request is using a legacy session"); // This could create a spike on refresh calls during the update of the backend SDK throw new error_1.default({ message: "using legacy session, please call the refresh API", type: error_1.default.TRY_REFRESH_TOKEN, }); } const sessionOptional = (options === null || options === void 0 ? void 0 : options.sessionRequired) === false; (0, logger_1.logDebugMessage)("getSession: optional validation: " + sessionOptional); const allowedTransferMethod = config.getTokenTransferMethod({ req, forCreateNewSession: false, userContext, }); const { requestTransferMethod, accessToken } = getAccessTokenFromRequest(config, req, allowedTransferMethod, userContext); let antiCsrfToken = (0, cookieAndHeaders_1.getAntiCsrfTokenFromHeaders)(req); let doAntiCsrfCheck = options !== undefined ? options.antiCsrfCheck : undefined; if (doAntiCsrfCheck === undefined) { doAntiCsrfCheck = (0, utils_1.normaliseHttpMethod)(req.getMethod()) !== "get"; } if (requestTransferMethod === "header") { doAntiCsrfCheck = false; } // If the token is not present we can ignore the antiCsrf settings. // the getSession implementation will handle checking sessionOptional if (accessToken === undefined) { doAntiCsrfCheck = false; } let antiCsrf = config.antiCsrfFunctionOrString; if (typeof antiCsrf === "function") { antiCsrf = antiCsrf({ request: req, userContext, }); } if (doAntiCsrfCheck && antiCsrf === "VIA_CUSTOM_HEADER") { if (antiCsrf === "VIA_CUSTOM_HEADER") { if ((0, utils_1.getRidFromHeader)(req) === undefined) { (0, logger_1.logDebugMessage)("getSession: Returning TRY_REFRESH_TOKEN because custom header (rid) was not passed"); throw new error_1.default({ message: "anti-csrf check failed. Please pass 'rid: \"session\"' header in the request, or set doAntiCsrfCheck to false for this API", type: error_1.default.TRY_REFRESH_TOKEN, }); } (0, logger_1.logDebugMessage)("getSession: VIA_CUSTOM_HEADER anti-csrf check passed"); doAntiCsrfCheck = false; } } (0, logger_1.logDebugMessage)("getSession: Value of doAntiCsrfCheck is: " + doAntiCsrfCheck); const session = await recipeInterfaceImpl.getSession({ accessToken: accessToken === null || accessToken === void 0 ? void 0 : accessToken.rawTokenString, antiCsrfToken, options: Object.assign(Object.assign({}, options), { antiCsrfCheck: doAntiCsrfCheck }), userContext, }); if (session !== undefined) { const claimValidators = await recipeInstance.getRequiredClaimValidators(session, options === null || options === void 0 ? void 0 : options.overrideGlobalClaimValidators, userContext); await session.assertClaims(claimValidators, userContext); // requestTransferMethod can only be undefined here if the user overridden getSession // to load the session by a custom method in that (very niche) case they also need to // override how the session is attached to the response. // In that scenario the transferMethod passed to attachToRequestResponse likely doesn't // matter, still, we follow the general fallback logic await session.attachToRequestResponse({ req, res, transferMethod: requestTransferMethod !== undefined ? requestTransferMethod : allowedTransferMethod !== "any" ? allowedTransferMethod : "header", }, userContext); } return session; } function getAccessTokenFromRequest(config, req, allowedTransferMethod, userContext) { const accessTokens = {}; // We check all token transfer methods for available access tokens for (const transferMethod of constants_1.availableTokenTransferMethods) { const tokenString = (0, cookieAndHeaders_1.getToken)(config, req, "access", transferMethod, userContext); if (tokenString !== undefined) { try { const info = (0, jwt_1.parseJWTWithoutSignatureVerification)(tokenString); (0, accessToken_1.validateAccessTokenStructure)(info.payload, info.version); (0, logger_1.logDebugMessage)("getSession: got access token from " + transferMethod); accessTokens[transferMethod] = info; } catch (_a) { (0, logger_1.logDebugMessage)(`getSession: ignoring token in ${transferMethod}, because it doesn't match our access token structure`); } } } let requestTransferMethod; let accessToken; if ((allowedTransferMethod === "any" || allowedTransferMethod === "header") && accessTokens["header"] !== undefined) { (0, logger_1.logDebugMessage)("getSession: using header transfer method"); requestTransferMethod = "header"; accessToken = accessTokens["header"]; } else if ((allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && accessTokens["cookie"] !== undefined) { (0, logger_1.logDebugMessage)("getSession: using cookie transfer method"); // If multiple access tokens exist in the request cookie, throw TRY_REFRESH_TOKEN. // This prompts the client to call the refresh endpoint, clearing olderCookieDomain cookies (if set). // ensuring outdated token payload isn't used. const hasMultipleAccessTokenCookies = (0, cookieAndHeaders_1.hasMultipleCookiesForTokenType)(config, req, "access", userContext); if (hasMultipleAccessTokenCookies) { (0, logger_1.logDebugMessage)("getSession: Throwing TRY_REFRESH_TOKEN because multiple access tokens are present in request cookies"); throw new error_1.default({ message: "Multiple access tokens present in the request cookies.", type: error_1.default.TRY_REFRESH_TOKEN, }); } requestTransferMethod = "cookie"; accessToken = accessTokens["cookie"]; } return { requestTransferMethod, accessToken, allowedTransferMethod }; } /* In all cases: if sIdRefreshToken token exists (so it's a legacy session) we clear it. Check http://localhost:3002/docs/contribute/decisions/session/0008 for further details and a table of expected behaviours */ async function refreshSessionInRequest({ res, req, userContext, config, recipeInterfaceImpl, stInstance, }) { (0, logger_1.logDebugMessage)("refreshSession: Started"); const configuredFramework = stInstance.framework; if (configuredFramework !== "custom") { if (!req.wrapperUsed) { req = framework_1.default[configuredFramework].wrapRequest(req); } if (!res.wrapperUsed) { res = framework_1.default[configuredFramework].wrapResponse(res); } } userContext = (0, utils_1.setRequestInUserContextIfNotDefined)(userContext, req); (0, logger_1.logDebugMessage)("refreshSession: Wrapping done"); (0, cookieAndHeaders_1.clearSessionCookiesFromOlderCookieDomain)({ req, res, config, userContext }); const refreshTokens = {}; // We check all token transfer methods for available refresh tokens // We do this so that we can later clear all we are not overwriting for (const transferMethod of constants_1.availableTokenTransferMethods) { refreshTokens[transferMethod] = (0, cookieAndHeaders_1.getToken)(config, req, "refresh", transferMethod, userContext); if (refreshTokens[transferMethod] !== undefined) { (0, logger_1.logDebugMessage)("refreshSession: got refresh token from " + transferMethod); } } const allowedTransferMethod = config.getTokenTransferMethod({ req, forCreateNewSession: false, userContext, }); (0, logger_1.logDebugMessage)("refreshSession: getTokenTransferMethod returned " + allowedTransferMethod); let requestTransferMethod; let refreshToken; if ((allowedTransferMethod === "any" || allowedTransferMethod === "header") && refreshTokens["header"] !== undefined) { (0, logger_1.logDebugMessage)("refreshSession: using header transfer method"); requestTransferMethod = "header"; refreshToken = refreshTokens["header"]; } else if ((allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && refreshTokens["cookie"]) { (0, logger_1.logDebugMessage)("refreshSession: using cookie transfer method"); requestTransferMethod = "cookie"; refreshToken = refreshTokens["cookie"]; } else { // This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { (0, logger_1.logDebugMessage)("refreshSession: cleared legacy id refresh token because refresh token was not found"); (0, cookieAndHeaders_1.setCookie)(config, res, LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, "", 0, "accessTokenPath", req, userContext); } // We need to clear the access token cookie if // - the refresh token is not found, and // - the allowedTransferMethod is 'cookie' or 'any', and // - an access token cookie exists (otherwise it'd be a no-op) // See: https://github.com/supertokens/supertokens-node/issues/790 if ((allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && (0, cookieAndHeaders_1.getToken)(config, req, "access", "cookie", userContext) !== undefined) { (0, logger_1.logDebugMessage)("refreshSession: cleared all session tokens and returning UNAUTHORISED because refresh token in request is undefined"); // We're clearing all session tokens instead of just the access token and then throwing an UNAUTHORISED // error with `clearTokens: false`. This approach avoids confusion and we don't want to retain session // tokens on the client in any case if the refresh API is called without a refresh token but with an access token. throw new error_1.default({ message: "Refresh token not found but access token is present. Clearing all tokens.", payload: { clearTokens: true, }, type: error_1.default.UNAUTHORISED, }); } throw new error_1.default({ message: "Refresh token not found. Are you sending the refresh token in the request?", payload: { clearTokens: true, }, type: error_1.default.UNAUTHORISED, }); } let disableAntiCsrf = requestTransferMethod === "header"; const antiCsrfToken = (0, cookieAndHeaders_1.getAntiCsrfTokenFromHeaders)(req); let antiCsrf = config.antiCsrfFunctionOrString; if (typeof antiCsrf === "function") { antiCsrf = antiCsrf({ request: req, userContext, }); } if (antiCsrf === "VIA_CUSTOM_HEADER" && !disableAntiCsrf) { if ((0, utils_1.getRidFromHeader)(req) === undefined) { (0, logger_1.logDebugMessage)("refreshSession: Returning UNAUTHORISED because custom header (rid) was not passed"); throw new error_1.default({ message: "anti-csrf check failed. Please pass 'rid: \"session\"' header in the request.", type: error_1.default.UNAUTHORISED, payload: { clearTokens: true, // see https://github.com/supertokens/supertokens-node/issues/141 }, }); } disableAntiCsrf = true; } let session; try { session = await recipeInterfaceImpl.refreshSession({ refreshToken: refreshToken, antiCsrfToken, disableAntiCsrf, userContext, }); } catch (ex) { if (error_1.default.isErrorFromSuperTokens(ex) && (ex.type === error_1.default.TOKEN_THEFT_DETECTED || ex.payload.clearTokens === true)) { // We clear the LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME here because we want to limit the scope of this legacy/migration code // so the token clearing functions in the error handlers do not if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { (0, logger_1.logDebugMessage)("refreshSession: cleared legacy id refresh token because refresh is clearing other tokens"); (0, cookieAndHeaders_1.setCookie)(config, res, LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, "", 0, "accessTokenPath", req, userContext); } } throw ex; } (0, logger_1.logDebugMessage)("refreshSession: Attaching refreshed session info as " + requestTransferMethod); // We clear the tokens in all token transfer methods we are not going to overwrite for (const transferMethod of constants_1.availableTokenTransferMethods) { if (transferMethod !== requestTransferMethod && refreshTokens[transferMethod] !== undefined) { (0, cookieAndHeaders_1.clearSession)(config, res, transferMethod, req, userContext); } } await session.attachToRequestResponse({ req, res, transferMethod: requestTransferMethod, }, userContext); (0, logger_1.logDebugMessage)("refreshSession: Success!"); // This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { (0, logger_1.logDebugMessage)("refreshSession: cleared legacy id refresh token after successful refresh"); (0, cookieAndHeaders_1.setCookie)(config, res, LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, "", 0, "accessTokenPath", req, userContext); } return session; } async function createNewSessionInRequest({ req, res, userContext, recipeInstance, stInstance, accessTokenPayload, userId, recipeUserId, config, appInfo, sessionDataInDatabase, tenantId, }) { (0, logger_1.logDebugMessage)("createNewSession: Started"); const configuredFramework = stInstance.framework; if (configuredFramework !== "custom") { if (!req.wrapperUsed) { req = framework_1.default[configuredFramework].wrapRequest(req); } if (!res.wrapperUsed) { res = framework_1.default[configuredFramework].wrapResponse(res); } } (0, logger_1.logDebugMessage)("createNewSession: Wrapping done"); userContext = (0, utils_1.setRequestInUserContextIfNotDefined)(userContext, req); const claimsAddedByOtherRecipes = recipeInstance.getClaimsAddedByOtherRecipes(); const issuer = await stInstance.getRecipeInstanceOrThrow("openid").getIssuer(userContext); let finalAccessTokenPayload = Object.assign(Object.assign({}, accessTokenPayload), { iss: issuer }); for (const prop of constants_1.protectedProps) { delete finalAccessTokenPayload[prop]; } for (const claim of claimsAddedByOtherRecipes) { const update = await claim.build(userId, recipeUserId, tenantId, finalAccessTokenPayload, userContext); finalAccessTokenPayload = Object.assign(Object.assign({}, finalAccessTokenPayload), update); } (0, logger_1.logDebugMessage)("createNewSession: Access token payload built"); let outputTransferMethod = config.getTokenTransferMethod({ req, forCreateNewSession: true, userContext }); if (outputTransferMethod === "any") { const authModeHeader = (0, cookieAndHeaders_1.getAuthModeFromHeader)(req); // We default to header if we can't "parse" it or if it's undefined if (authModeHeader === "cookie") { outputTransferMethod = authModeHeader; } else { outputTransferMethod = "header"; } } (0, logger_1.logDebugMessage)("createNewSession: using transfer method " + outputTransferMethod); if (outputTransferMethod === "cookie" && config.getCookieSameSite({ request: req, userContext, }) === "none" && !config.cookieSecure && !((appInfo.topLevelAPIDomain === "localhost" || (0, normalisedURLDomain_1.isAnIpAddress)(appInfo.topLevelAPIDomain)) && (appInfo.getTopLevelWebsiteDomain({ request: req, userContext, }) === "localhost" || (0, normalisedURLDomain_1.isAnIpAddress)(appInfo.getTopLevelWebsiteDomain({ request: req, userContext, }))))) { // We can allow insecure cookie when both website & API domain are localhost or an IP // When either of them is a different domain, API domain needs to have https and a secure cookie to work throw new Error("Since your API and website domain are different, for sessions to work, please use https on your apiDomain and dont set cookieSecure to false."); } const disableAntiCsrf = outputTransferMethod === "header"; const session = await recipeInstance.recipeInterfaceImpl.createNewSession({ userId, recipeUserId, accessTokenPayload: finalAccessTokenPayload, sessionDataInDatabase, disableAntiCsrf, tenantId, userContext, }); (0, logger_1.logDebugMessage)("createNewSession: Session created in core built"); for (const transferMethod of constants_1.availableTokenTransferMethods) { if (transferMethod !== outputTransferMethod && (0, cookieAndHeaders_1.getToken)(config, req, "access", transferMethod, userContext) !== undefined) { (0, cookieAndHeaders_1.clearSession)(config, res, transferMethod, req, userContext); } } (0, logger_1.logDebugMessage)("createNewSession: Cleared old tokens"); await session.attachToRequestResponse({ req, res, transferMethod: outputTransferMethod, }, userContext); (0, logger_1.logDebugMessage)("createNewSession: Attached new tokens to res"); return session; }