supertokens-node
Version:
NodeJS driver for SuperTokens core
378 lines (377 loc) • 20.4 kB
JavaScript
;
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;
}