Skip to content

Commit

Permalink
feat: get cookie name (#957)
Browse files Browse the repository at this point in the history
* feat: make cookie and header names configurable for access and refresh tokens

* test: update the test server to make cookie names overrideable

* refactor: chores and consistency fixes
  • Loading branch information
porcellus authored Nov 20, 2024
1 parent 9937dd2 commit 1ba9f68
Show file tree
Hide file tree
Showing 22 changed files with 268 additions and 138 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

## [21.1.0] - 2024-11-19

- Adds `getCookieNameForTokenType` config option to allow customizing the cookie name for a token type.
- Adds `getResponseHeaderNameForTokenType` config option to allow customizing the response header name for a token type.
- Please note, that using this will require further customizations on the frontend

## [21.0.0] - 2024-10-07

- Added OAuth2Provider recipe
Expand Down
8 changes: 8 additions & 0 deletions lib/build/recipe/session/constants.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@ export declare const availableTokenTransferMethods: TokenTransferMethod[];
export declare const oneYearInMs = 31536000000;
export declare const JWKCacheCooldownInMs = 500;
export declare const protectedProps: string[];
export declare const authorizationHeaderKey = "authorization";
export declare const accessTokenHeaderKey = "st-access-token";
export declare const accessTokenCookieKey = "sAccessToken";
export declare const refreshTokenCookieKey = "sRefreshToken";
export declare const refreshTokenHeaderKey = "st-refresh-token";
export declare const antiCsrfHeaderKey = "anti-csrf";
export declare const frontTokenHeaderKey = "front-token";
export declare const authModeHeaderKey = "st-auth-mode";
10 changes: 9 additions & 1 deletion lib/build/recipe/session/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.protectedProps = exports.JWKCacheCooldownInMs = exports.oneYearInMs = exports.availableTokenTransferMethods = exports.SIGNOUT_API_PATH = exports.REFRESH_API_PATH = void 0;
exports.authModeHeaderKey = exports.frontTokenHeaderKey = exports.antiCsrfHeaderKey = exports.refreshTokenHeaderKey = exports.refreshTokenCookieKey = exports.accessTokenCookieKey = exports.accessTokenHeaderKey = exports.authorizationHeaderKey = exports.protectedProps = exports.JWKCacheCooldownInMs = exports.oneYearInMs = exports.availableTokenTransferMethods = exports.SIGNOUT_API_PATH = exports.REFRESH_API_PATH = void 0;
exports.REFRESH_API_PATH = "/session/refresh";
exports.SIGNOUT_API_PATH = "/signout";
exports.availableTokenTransferMethods = ["cookie", "header"];
Expand All @@ -32,3 +32,11 @@ exports.protectedProps = [
"tId",
"stt",
];
exports.authorizationHeaderKey = "authorization";
exports.accessTokenHeaderKey = "st-access-token";
exports.accessTokenCookieKey = "sAccessToken";
exports.refreshTokenCookieKey = "sRefreshToken";
exports.refreshTokenHeaderKey = "st-refresh-token";
exports.antiCsrfHeaderKey = "anti-csrf";
exports.frontTokenHeaderKey = "front-token";
exports.authModeHeaderKey = "st-auth-mode";
19 changes: 12 additions & 7 deletions lib/build/recipe/session/cookieAndHeaders.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,27 @@ import { TokenTransferMethod, TokenType, TypeNormalisedInput } from "./types";
export declare function clearSessionFromAllTokenTransferMethods(
config: TypeNormalisedInput,
res: BaseResponse,
request: BaseRequest | undefined,
request: BaseRequest,
userContext: UserContext
): void;
export declare function clearSession(
config: TypeNormalisedInput,
res: BaseResponse,
transferMethod: TokenTransferMethod,
request: BaseRequest | undefined,
request: BaseRequest,
userContext: UserContext
): void;
export declare function getAntiCsrfTokenFromHeaders(req: BaseRequest): string | undefined;
export declare function setAntiCsrfTokenInHeaders(res: BaseResponse, antiCsrfToken: string): void;
export declare function buildFrontToken(userId: string, atExpiry: number, accessTokenPayload: any): string;
export declare function setFrontTokenInHeaders(res: BaseResponse, frontToken: string): void;
export declare function getCORSAllowedHeaders(): string[];
export declare function getCookieNameFromTokenType(tokenType: TokenType): "sAccessToken" | "sRefreshToken";
export declare function getResponseHeaderNameForTokenType(tokenType: TokenType): "st-access-token" | "st-refresh-token";
export declare function getToken(
config: TypeNormalisedInput,
req: BaseRequest,
tokenType: TokenType,
transferMethod: TokenTransferMethod
transferMethod: TokenTransferMethod,
userContext: UserContext
): string | undefined;
export declare function setToken(
config: TypeNormalisedInput,
Expand All @@ -34,7 +34,7 @@ export declare function setToken(
value: string,
expires: number,
transferMethod: TokenTransferMethod,
req: BaseRequest | undefined,
req: BaseRequest,
userContext: UserContext
): void;
export declare function setHeader(res: BaseResponse, name: string, value: string): void;
Expand Down Expand Up @@ -83,4 +83,9 @@ export declare function clearSessionCookiesFromOlderCookieDomain({
config: TypeNormalisedInput;
userContext: UserContext;
}): void;
export declare function hasMultipleCookiesForTokenType(req: BaseRequest, tokenType: TokenType): boolean;
export declare function hasMultipleCookiesForTokenType(
config: TypeNormalisedInput,
req: BaseRequest,
tokenType: TokenType,
userContext: UserContext
): boolean;
75 changes: 26 additions & 49 deletions lib/build/recipe/session/cookieAndHeaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ var __importDefault =
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.hasMultipleCookiesForTokenType = exports.clearSessionCookiesFromOlderCookieDomain = exports.getAuthModeFromHeader = exports.setCookie = exports.setHeader = exports.setToken = exports.getToken = exports.getResponseHeaderNameForTokenType = exports.getCookieNameFromTokenType = exports.getCORSAllowedHeaders = exports.setFrontTokenInHeaders = exports.buildFrontToken = exports.setAntiCsrfTokenInHeaders = exports.getAntiCsrfTokenFromHeaders = exports.clearSession = exports.clearSessionFromAllTokenTransferMethods = void 0;
exports.hasMultipleCookiesForTokenType = exports.clearSessionCookiesFromOlderCookieDomain = exports.getAuthModeFromHeader = exports.setCookie = exports.setHeader = exports.setToken = exports.getToken = exports.getCORSAllowedHeaders = exports.setFrontTokenInHeaders = exports.buildFrontToken = exports.setAntiCsrfTokenInHeaders = exports.getAntiCsrfTokenFromHeaders = exports.clearSession = exports.clearSessionFromAllTokenTransferMethods = void 0;
/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
Expand All @@ -25,14 +25,6 @@ const logger_1 = require("../../logger");
const utils_1 = require("../../utils");
const constants_2 = require("./constants");
const error_1 = __importDefault(require("./error"));
const authorizationHeaderKey = "authorization";
const accessTokenCookieKey = "sAccessToken";
const accessTokenHeaderKey = "st-access-token";
const refreshTokenCookieKey = "sRefreshToken";
const refreshTokenHeaderKey = "st-refresh-token";
const antiCsrfHeaderKey = "anti-csrf";
const frontTokenHeaderKey = "front-token";
const authModeHeaderKey = "st-auth-mode";
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:
Expand All @@ -51,19 +43,19 @@ function clearSession(config, res, transferMethod, request, userContext) {
for (const token of tokenTypes) {
setToken(config, res, token, "", 0, transferMethod, request, userContext);
}
res.removeHeader(antiCsrfHeaderKey);
res.removeHeader(constants_2.antiCsrfHeaderKey);
// This can be added multiple times in some cases, but that should be OK
res.setHeader(frontTokenHeaderKey, "remove", false);
res.setHeader("Access-Control-Expose-Headers", frontTokenHeaderKey, true);
res.setHeader(constants_2.frontTokenHeaderKey, "remove", false);
res.setHeader("Access-Control-Expose-Headers", constants_2.frontTokenHeaderKey, true);
}
exports.clearSession = clearSession;
function getAntiCsrfTokenFromHeaders(req) {
return req.getHeaderValue(antiCsrfHeaderKey);
return req.getHeaderValue(constants_2.antiCsrfHeaderKey);
}
exports.getAntiCsrfTokenFromHeaders = getAntiCsrfTokenFromHeaders;
function setAntiCsrfTokenInHeaders(res, antiCsrfToken) {
res.setHeader(antiCsrfHeaderKey, antiCsrfToken, false);
res.setHeader("Access-Control-Expose-Headers", antiCsrfHeaderKey, true);
res.setHeader(constants_2.antiCsrfHeaderKey, antiCsrfToken, false);
res.setHeader("Access-Control-Expose-Headers", constants_2.antiCsrfHeaderKey, true);
}
exports.setAntiCsrfTokenInHeaders = setAntiCsrfTokenInHeaders;
function buildFrontToken(userId, atExpiry, accessTokenPayload) {
Expand All @@ -76,41 +68,24 @@ function buildFrontToken(userId, atExpiry, accessTokenPayload) {
}
exports.buildFrontToken = buildFrontToken;
function setFrontTokenInHeaders(res, frontToken) {
res.setHeader(frontTokenHeaderKey, frontToken, false);
res.setHeader("Access-Control-Expose-Headers", frontTokenHeaderKey, true);
res.setHeader(constants_2.frontTokenHeaderKey, frontToken, false);
res.setHeader("Access-Control-Expose-Headers", constants_2.frontTokenHeaderKey, true);
}
exports.setFrontTokenInHeaders = setFrontTokenInHeaders;
function getCORSAllowedHeaders() {
return [antiCsrfHeaderKey, constants_1.HEADER_RID, authorizationHeaderKey, authModeHeaderKey];
return [
constants_2.antiCsrfHeaderKey,
constants_1.HEADER_RID,
constants_2.authorizationHeaderKey,
constants_2.authModeHeaderKey,
];
}
exports.getCORSAllowedHeaders = getCORSAllowedHeaders;
function getCookieNameFromTokenType(tokenType) {
switch (tokenType) {
case "access":
return accessTokenCookieKey;
case "refresh":
return refreshTokenCookieKey;
default:
throw new Error("Unknown token type, should never happen.");
}
}
exports.getCookieNameFromTokenType = getCookieNameFromTokenType;
function getResponseHeaderNameForTokenType(tokenType) {
switch (tokenType) {
case "access":
return accessTokenHeaderKey;
case "refresh":
return refreshTokenHeaderKey;
default:
throw new Error("Unknown token type, should never happen.");
}
}
exports.getResponseHeaderNameForTokenType = getResponseHeaderNameForTokenType;
function getToken(req, tokenType, transferMethod) {
function getToken(config, req, tokenType, transferMethod, userContext) {
if (transferMethod === "cookie") {
return req.getCookieValue(getCookieNameFromTokenType(tokenType));
return req.getCookieValue(config.getCookieNameForTokenType(req, tokenType, userContext));
} else if (transferMethod === "header") {
const value = req.getHeaderValue(authorizationHeaderKey);
const value = req.getHeaderValue(constants_2.authorizationHeaderKey);
if (value === undefined || !value.startsWith("Bearer ")) {
return undefined;
}
Expand All @@ -126,15 +101,15 @@ function setToken(config, res, tokenType, value, expires, transferMethod, req, u
setCookie(
config,
res,
getCookieNameFromTokenType(tokenType),
config.getCookieNameForTokenType(req, tokenType, userContext),
value,
expires,
tokenType === "refresh" ? "refreshTokenPath" : "accessTokenPath",
req,
userContext
);
} else if (transferMethod === "header") {
setHeader(res, getResponseHeaderNameForTokenType(tokenType), value);
setHeader(res, config.getResponseHeaderNameForTokenType(req, tokenType, userContext), value);
}
}
exports.setToken = setToken;
Expand Down Expand Up @@ -174,7 +149,9 @@ function setCookie(config, res, name, value, expires, pathType, req, userContext
exports.setCookie = setCookie;
function getAuthModeFromHeader(req) {
var _a;
return (_a = req.getHeaderValue(authModeHeaderKey)) === null || _a === void 0 ? void 0 : _a.toLowerCase();
return (_a = req.getHeaderValue(constants_2.authModeHeaderKey)) === null || _a === void 0
? void 0
: _a.toLowerCase();
}
exports.getAuthModeFromHeader = getAuthModeFromHeader;
/**
Expand Down Expand Up @@ -202,7 +179,7 @@ function clearSessionCookiesFromOlderCookieDomain({ req, res, config, userContex
let didClearCookies = false;
const tokenTypes = ["access", "refresh"];
for (const token of tokenTypes) {
if (hasMultipleCookiesForTokenType(req, token)) {
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,
Expand Down Expand Up @@ -237,13 +214,13 @@ function clearSessionCookiesFromOlderCookieDomain({ req, res, config, userContex
}
}
exports.clearSessionCookiesFromOlderCookieDomain = clearSessionCookiesFromOlderCookieDomain;
function hasMultipleCookiesForTokenType(req, tokenType) {
function hasMultipleCookiesForTokenType(config, req, tokenType, userContext) {
const cookieString = req.getHeaderValue("cookie");
if (cookieString === undefined) {
return false;
}
const cookies = parseCookieStringFromRequestHeaderAllowingDuplicates(cookieString);
const cookieName = getCookieNameFromTokenType(tokenType);
const cookieName = config.getCookieNameForTokenType(req, tokenType, userContext);
return cookies[cookieName] !== undefined && cookies[cookieName].length > 1;
}
exports.hasMultipleCookiesForTokenType = hasMultipleCookiesForTokenType;
Expand Down
4 changes: 3 additions & 1 deletion lib/build/recipe/session/sessionRequestFunctions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ export declare function getSessionFromRequest({
userContext: UserContext;
}): Promise<SessionContainerInterface | undefined>;
export declare function getAccessTokenFromRequest(
config: TypeNormalisedInput,
req: any,
allowedTransferMethod: TokenTransferMethod | "any"
allowedTransferMethod: TokenTransferMethod | "any",
userContext: UserContext
): {
requestTransferMethod: TokenTransferMethod | undefined;
accessToken: ParsedJWTInfo | undefined;
Expand Down
30 changes: 23 additions & 7 deletions lib/build/recipe/session/sessionRequestFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ async function getSessionFromRequest({ req, res, config, recipeInterfaceImpl, op
forCreateNewSession: false,
userContext,
});
const { requestTransferMethod, accessToken } = getAccessTokenFromRequest(req, allowedTransferMethod);
const { requestTransferMethod, accessToken } = getAccessTokenFromRequest(
config,
req,
allowedTransferMethod,
userContext
);
let antiCsrfToken = cookieAndHeaders_1.getAntiCsrfTokenFromHeaders(req);
let doAntiCsrfCheck = options !== undefined ? options.antiCsrfCheck : undefined;
if (doAntiCsrfCheck === undefined) {
Expand Down Expand Up @@ -123,11 +128,11 @@ async function getSessionFromRequest({ req, res, config, recipeInterfaceImpl, op
return session;
}
exports.getSessionFromRequest = getSessionFromRequest;
function getAccessTokenFromRequest(req, allowedTransferMethod) {
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 = cookieAndHeaders_1.getToken(req, "access", transferMethod);
const tokenString = cookieAndHeaders_1.getToken(config, req, "access", transferMethod, userContext);
if (tokenString !== undefined) {
try {
const info = jwt_1.parseJWTWithoutSignatureVerification(tokenString);
Expand Down Expand Up @@ -158,7 +163,12 @@ function getAccessTokenFromRequest(req, allowedTransferMethod) {
// 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 = cookieAndHeaders_1.hasMultipleCookiesForTokenType(req, "access");
const hasMultipleAccessTokenCookies = cookieAndHeaders_1.hasMultipleCookiesForTokenType(
config,
req,
"access",
userContext
);
if (hasMultipleAccessTokenCookies) {
logger_1.logDebugMessage(
"getSession: Throwing TRY_REFRESH_TOKEN because multiple access tokens are present in request cookies"
Expand Down Expand Up @@ -196,7 +206,13 @@ async function refreshSessionInRequest({ res, req, userContext, config, recipeIn
// 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] = cookieAndHeaders_1.getToken(req, "refresh", transferMethod);
refreshTokens[transferMethod] = cookieAndHeaders_1.getToken(
config,
req,
"refresh",
transferMethod,
userContext
);
if (refreshTokens[transferMethod] !== undefined) {
logger_1.logDebugMessage("refreshSession: got refresh token from " + transferMethod);
}
Expand Down Expand Up @@ -244,7 +260,7 @@ async function refreshSessionInRequest({ res, req, userContext, config, recipeIn
// See: https://github.com/supertokens/supertokens-node/issues/790
if (
(allowedTransferMethod === "any" || allowedTransferMethod === "cookie") &&
cookieAndHeaders_1.getToken(req, "access", "cookie") !== undefined
cookieAndHeaders_1.getToken(config, req, "access", "cookie", userContext) !== undefined
) {
logger_1.logDebugMessage(
"refreshSession: cleared all session tokens and returning UNAUTHORISED because refresh token in request is undefined"
Expand Down Expand Up @@ -446,7 +462,7 @@ async function createNewSessionInRequest({
for (const transferMethod of constants_1.availableTokenTransferMethods) {
if (
transferMethod !== outputTransferMethod &&
cookieAndHeaders_1.getToken(req, "access", transferMethod) !== undefined
cookieAndHeaders_1.getToken(config, req, "access", transferMethod, userContext) !== undefined
) {
cookieAndHeaders_1.clearSession(config, res, transferMethod, req, userContext);
}
Expand Down
4 changes: 4 additions & 0 deletions lib/build/recipe/session/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export declare type TypeInput = {
forCreateNewSession: boolean;
userContext: UserContext;
}) => TokenTransferMethod | "any";
getCookieNameForTokenType?: (req: BaseRequest, tokenType: TokenType, userContext: UserContext) => string;
getResponseHeaderNameForTokenType?: (req: BaseRequest, tokenType: TokenType, userContext: UserContext) => string;
errorHandlers?: ErrorHandlers;
antiCsrf?: "VIA_TOKEN" | "VIA_CUSTOM_HEADER" | "NONE";
exposeAccessTokenToFrontendInCookieBasedAuth?: boolean;
Expand All @@ -74,6 +76,8 @@ export declare type TypeNormalisedInput = {
userContext: UserContext;
}) => "strict" | "lax" | "none";
cookieSecure: boolean;
getCookieNameForTokenType: (req: BaseRequest, tokenType: TokenType, userContext: UserContext) => string;
getResponseHeaderNameForTokenType: (req: BaseRequest, tokenType: TokenType, userContext: UserContext) => string;
sessionExpiredStatusCode: number;
errorHandlers: NormalisedErrorHandlers;
antiCsrfFunctionOrString:
Expand Down
Loading

0 comments on commit 1ba9f68

Please sign in to comment.