supertokens-node
Version:
NodeJS driver for SuperTokens core
217 lines (216 loc) • 10.8 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.clearSessionFromAllTokenTransferMethods = clearSessionFromAllTokenTransferMethods;
exports.clearSession = clearSession;
exports.getAntiCsrfTokenFromHeaders = getAntiCsrfTokenFromHeaders;
exports.setAntiCsrfTokenInHeaders = setAntiCsrfTokenInHeaders;
exports.buildFrontToken = buildFrontToken;
exports.setFrontTokenInHeaders = setFrontTokenInHeaders;
exports.getCORSAllowedHeaders = getCORSAllowedHeaders;
exports.getToken = getToken;
exports.setToken = setToken;
exports.setHeader = setHeader;
exports.setCookie = setCookie;
exports.getAuthModeFromHeader = getAuthModeFromHeader;
exports.clearSessionCookiesFromOlderCookieDomain = clearSessionCookiesFromOlderCookieDomain;
exports.hasMultipleCookiesForTokenType = hasMultipleCookiesForTokenType;
/* 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.
*/
const constants_1 = require("../../constants");
const logger_1 = require("../../logger");
const utils_1 = require("../../utils");
const constants_2 = require("./constants");
const error_1 = __importDefault(require("./error"));
function clearSessionFromAllTokenTransferMethods(config, res, request, userContext) {
// We are clearing the session in all transfermethods to be sure to override cookies in case they have been already added to the response.
// This is done to handle the following use-case:
// If the app overrides signInPOST to check the ban status of the user after the original implementation and throwing an UNAUTHORISED error
// In this case: the SDK has attached cookies to the response, but none was sent with the request
// We can't know which to clear since we can't reliably query or remove the set-cookie header added to the response (causes issues in some frameworks, i.e.: hapi)
// The safe solution in this case is to overwrite all the response cookies/headers with an empty value, which is what we are doing here
for (const transferMethod of constants_2.availableTokenTransferMethods) {
clearSession(config, res, transferMethod, request, userContext);
}
}
function clearSession(config, res, transferMethod, request, userContext) {
// If we can be specific about which transferMethod we want to clear, there is no reason to clear the other ones
const tokenTypes = ["access", "refresh"];
for (const token of tokenTypes) {
setToken(config, res, token, "", 0, transferMethod, request, userContext);
}
res.removeHeader(constants_2.antiCsrfHeaderKey);
// This can be added multiple times in some cases, but that should be OK
res.setHeader(constants_2.frontTokenHeaderKey, "remove", false);
res.setHeader("Access-Control-Expose-Headers", constants_2.frontTokenHeaderKey, true);
}
function getAntiCsrfTokenFromHeaders(req) {
return req.getHeaderValue(constants_2.antiCsrfHeaderKey);
}
function setAntiCsrfTokenInHeaders(res, antiCsrfToken) {
res.setHeader(constants_2.antiCsrfHeaderKey, antiCsrfToken, false);
res.setHeader("Access-Control-Expose-Headers", constants_2.antiCsrfHeaderKey, true);
}
function buildFrontToken(userId, atExpiry, accessTokenPayload) {
const tokenInfo = {
uid: userId,
ate: atExpiry,
up: accessTokenPayload,
};
return (0, utils_1.encodeBase64)(JSON.stringify(tokenInfo));
}
function setFrontTokenInHeaders(res, frontToken) {
res.setHeader(constants_2.frontTokenHeaderKey, frontToken, false);
res.setHeader("Access-Control-Expose-Headers", constants_2.frontTokenHeaderKey, true);
}
function getCORSAllowedHeaders() {
return [constants_2.antiCsrfHeaderKey, constants_1.HEADER_RID, constants_2.authorizationHeaderKey, constants_2.authModeHeaderKey];
}
function getToken(config, req, tokenType, transferMethod, userContext) {
if (transferMethod === "cookie") {
return req.getCookieValue(config.getCookieNameForTokenType(req, tokenType, userContext));
}
else if (transferMethod === "header") {
const value = req.getHeaderValue(constants_2.authorizationHeaderKey);
if (value === undefined || !value.startsWith("Bearer ")) {
return undefined;
}
return value.replace(/^Bearer /, "").trim();
}
else {
throw new Error("Should never happen: Unknown transferMethod: " + transferMethod);
}
}
function setToken(config, res, tokenType, value, expires, transferMethod, req, userContext) {
(0, logger_1.logDebugMessage)(`setToken: Setting ${tokenType} token as ${transferMethod}`);
if (transferMethod === "cookie") {
setCookie(config, res, config.getCookieNameForTokenType(req, tokenType, userContext), value, expires, tokenType === "refresh" ? "refreshTokenPath" : "accessTokenPath", req, userContext);
}
else if (transferMethod === "header") {
setHeader(res, config.getResponseHeaderNameForTokenType(req, tokenType, userContext), value);
}
}
function setHeader(res, name, value) {
res.setHeader(name, value, false);
res.setHeader("Access-Control-Expose-Headers", name, true);
}
/**
*
* @param res
* @param name
* @param value
* @param domain
* @param secure
* @param httpOnly
* @param expires
* @param path
*/
function setCookie(config, res, name, value, expires, pathType, req, userContext) {
let domain = config.cookieDomain;
let secure = config.cookieSecure;
let sameSite = config.getCookieSameSite({
request: req,
userContext,
});
let path = "";
if (pathType === "refreshTokenPath") {
path = config.refreshTokenPath.getAsStringDangerous();
}
else if (pathType === "accessTokenPath") {
path =
config.accessTokenPath.getAsStringDangerous() === "" ? "/" : config.accessTokenPath.getAsStringDangerous();
}
let httpOnly = true;
return res.setCookie(name, value, domain, secure, httpOnly, expires, path, sameSite);
}
function getAuthModeFromHeader(req) {
var _a;
return (_a = req.getHeaderValue(constants_2.authModeHeaderKey)) === null || _a === void 0 ? void 0 : _a.toLowerCase();
}
/**
* This function addresses an edge case where changing the cookieDomain config on the server can
* lead to session integrity issues. For instance, if the API server URL is 'api.example.com'
* with a cookie domain of '.example.com', and the server updates the cookie domain to 'api.example.com',
* the client may retain cookies with both '.example.com' and 'api.example.com' domains.
*
* Consequently, if the server chooses the older cookie, session invalidation occurs, potentially
* resulting in an infinite refresh loop. To fix this, users are asked to specify "olderCookieDomain" in
* the config.
*
* This function checks for multiple cookies with the same name and clears the cookies for the older domain
*/
function clearSessionCookiesFromOlderCookieDomain({ req, res, config, userContext, }) {
const allowedTransferMethod = config.getTokenTransferMethod({
req,
forCreateNewSession: false,
userContext,
});
// If the transfer method is 'header', there's no need to clear cookies immediately, even if there are multiple in the request.
if (allowedTransferMethod === "header") {
return;
}
let didClearCookies = false;
const tokenTypes = ["access", "refresh"];
for (const token of tokenTypes) {
if (hasMultipleCookiesForTokenType(config, req, token, userContext)) {
// If a request has multiple session cookies and 'olderCookieDomain' is
// unset, we can't identify the correct cookie for refreshing the session.
// Using the wrong cookie can cause an infinite refresh loop. To avoid this,
// we throw a 500 error asking the user to set 'olderCookieDomain'.
if (config.olderCookieDomain === undefined) {
throw new Error(`The request contains multiple session cookies. This may happen if you've changed the 'cookieDomain' value in your configuration. To clear tokens from the previous domain, set 'olderCookieDomain' in your config.`);
}
(0, logger_1.logDebugMessage)(`clearSessionCookiesFromOlderCookieDomain: Clearing duplicate ${token} cookie with domain ${config.olderCookieDomain}`);
setToken(Object.assign(Object.assign({}, config), { cookieDomain: config.olderCookieDomain }), res, token, "", 0, "cookie", req, userContext);
didClearCookies = true;
}
}
if (didClearCookies) {
throw new error_1.default({
message: "The request contains multiple session cookies. We are clearing the cookie from olderCookieDomain. Session will be refreshed in the next refresh call.",
type: error_1.default.CLEAR_DUPLICATE_SESSION_COOKIES,
});
}
}
function hasMultipleCookiesForTokenType(config, req, tokenType, userContext) {
const cookieString = req.getHeaderValue("cookie");
if (cookieString === undefined) {
return false;
}
const cookieNames = getCookieNamesFromRequestHeaderAllowingDuplicates(cookieString);
const cookieName = config.getCookieNameForTokenType(req, tokenType, userContext);
return cookieNames.filter((name) => name === cookieName).length > 1;
}
// This function is required because cookies library (and most of the popular libraries in npm)
// does not support parsing multiple cookies with the same name.
function getCookieNamesFromRequestHeaderAllowingDuplicates(cookieString) {
const cookieNames = [];
const cookiePairs = cookieString.split(";");
for (const cookiePair of cookiePairs) {
const [name, _] = cookiePair.trim().split("=");
// Try to decode the name or fallback to the original name
let decodedName = name;
try {
decodedName = decodeURIComponent(name);
}
catch (e) {
(0, logger_1.logDebugMessage)(`getCookieNamesFromRequestHeaderAllowingDuplicates: Error decoding cookie name: ${name}`);
}
cookieNames.push(decodedName);
}
return cookieNames;
}