Merge pull request #25952 from allegro-dbargowski/cookie-chunking-for-too-big-cookies-auth-node

Fix support for cookies bigger than 4KB in size in plugin-auth-node
This commit is contained in:
Patrik Oldsberg
2024-10-22 11:27:18 +02:00
committed by GitHub
3 changed files with 276 additions and 30 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-node': patch
---
Browsers silently drop cookies that exceed 4KB, which can be problematic for refresh tokens and other large cookies.This update ensures that large cookies, like refresh tokens, are not dropped by browsers, maintaining the integrity of the authentication process. The changes include both the implementation of the cookie splitting logic and corresponding tests to validate the new functionality.
+151 -29
View File
@@ -14,12 +14,14 @@
* limitations under the License.
*/
import { Request, Response } from 'express';
import { CookieOptions, Request, Response } from 'express';
import { CookieConfigurer } from '../types';
const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000;
const TEN_MINUTES_MS = 600 * 1000;
const MAX_COOKIE_SIZE_CHARACTERS = 4000;
const defaultCookieConfigurer: CookieConfigurer = ({
callbackUrl,
providerId,
@@ -85,50 +87,170 @@ export class OAuthCookieManager {
};
}
setNonce(res: Response, nonce: string, origin?: string) {
res.cookie(this.nonceCookie, nonce, {
maxAge: TEN_MINUTES_MS,
...this.getConfig(origin, '/handler'),
});
setNonce(res: Response, nonce: string, origin?: string): void {
this.setCookie(
res,
this.nonceCookie,
nonce,
TEN_MINUTES_MS,
origin,
'/handler',
);
}
setRefreshToken(res: Response, refreshToken: string, origin?: string) {
res.cookie(this.refreshTokenCookie, refreshToken, {
maxAge: THOUSAND_DAYS_MS,
...this.getConfig(origin),
});
setRefreshToken(res: Response, refreshToken: string, origin?: string): void {
this.setCookie(
res,
this.refreshTokenCookie,
refreshToken,
THOUSAND_DAYS_MS,
origin,
);
}
removeRefreshToken(res: Response, origin?: string) {
res.cookie(this.refreshTokenCookie, '', {
maxAge: 0,
...this.getConfig(origin),
});
removeRefreshToken(res: Response, origin?: string): void {
this.removeCookie(res, this.refreshTokenCookie, origin);
}
removeGrantedScopes(res: Response, origin?: string) {
res.cookie(this.grantedScopeCookie, '', {
maxAge: 0,
...this.getConfig(origin),
});
removeGrantedScopes(res: Response, origin?: string): void {
this.removeCookie(res, this.grantedScopeCookie, origin);
}
setGrantedScopes(res: Response, scope: string, origin?: string) {
res.cookie(this.grantedScopeCookie, scope, {
maxAge: THOUSAND_DAYS_MS,
...this.getConfig(origin),
});
setGrantedScopes(res: Response, scope: string, origin?: string): void {
this.setCookie(
res,
this.grantedScopeCookie,
scope,
THOUSAND_DAYS_MS,
origin,
);
}
getNonce(req: Request): string | undefined {
return req.cookies[this.nonceCookie];
return this.getCookie(req, this.nonceCookie);
}
getRefreshToken(req: Request): string | undefined {
return req.cookies[this.refreshTokenCookie];
return this.getCookie(req, this.refreshTokenCookie);
}
getGrantedScopes(req: Request): string | undefined {
return req.cookies[this.grantedScopeCookie];
return this.getCookie(req, this.grantedScopeCookie);
}
private setCookie(
res: Response,
name: string,
val: string,
maxAge: number,
origin?: string,
pathSuffix: string = '',
): Response {
const options = {
maxAge,
...this.getConfig(origin, pathSuffix),
};
const req = res.req;
let output = res;
if (val.length > MAX_COOKIE_SIZE_CHARACTERS) {
const nonChunkedFormatExists = !!req.cookies[name];
if (nonChunkedFormatExists) {
output = output.cookie(name, '', this.getRemoveCookieOptions());
}
const chunked = this.splitCookieToChunks(val, MAX_COOKIE_SIZE_CHARACTERS);
chunked.forEach((value, chunkNumber) => {
output = output.cookie(
OAuthCookieManager.getCookieChunkName(name, chunkNumber),
value,
options,
);
});
return output;
}
const chunkedFormatExists = OAuthCookieManager.chunkedCookieExists(
req,
name,
);
if (chunkedFormatExists) {
for (let chunkNumber = 0; ; chunkNumber++) {
const key = OAuthCookieManager.getCookieChunkName(name, chunkNumber);
const exists = !!req.cookies[key];
if (!exists) {
break;
}
output = output.cookie(key, '', this.getRemoveCookieOptions());
}
}
return output.cookie(name, val, options);
}
private getCookie(req: Request, name: string): string | undefined {
const isChunked = OAuthCookieManager.chunkedCookieExists(req, name);
if (isChunked) {
const chunks: string[] = [];
let chunkNumber = 0;
let chunk =
req.cookies[OAuthCookieManager.getCookieChunkName(name, chunkNumber)];
while (chunk) {
chunks.push(chunk);
chunkNumber++;
chunk =
req.cookies[OAuthCookieManager.getCookieChunkName(name, chunkNumber)];
}
return chunks.join('');
}
return req.cookies[name];
}
private removeCookie(res: Response, name: string, origin?: string): Response {
const req = res.req;
const options = this.getRemoveCookieOptions(origin);
const isChunked = OAuthCookieManager.chunkedCookieExists(req, name);
if (isChunked) {
const nonChunkedFormatExists = !!req.cookies[name];
let output: Response = nonChunkedFormatExists
? res.cookie(name, '', options)
: res;
for (let chunkNumber = 0; ; chunkNumber++) {
const key = OAuthCookieManager.getCookieChunkName(name, chunkNumber);
const exists = !!req.cookies[key];
if (!exists) {
break;
}
output = output.cookie(key, '', options);
}
return output;
}
return res.cookie(name, '', options);
}
private splitCookieToChunks(val: string, chunkSize: number): string[] {
const numChunks = Math.ceil(val.length / chunkSize);
const chunks: string[] = Array<string>(numChunks);
let offset: number = 0;
for (let i = 0; i < numChunks; i++) {
chunks[i] = val.substring(offset, offset + chunkSize);
offset += chunkSize;
}
return chunks;
}
private static chunkedCookieExists(req: Request, name: string): boolean {
return !!req.cookies[OAuthCookieManager.getCookieChunkName(name, 0)];
}
private static getCookieChunkName(name: string, chunkIndex: number): string {
return `${name}-${chunkIndex}`;
}
private getRemoveCookieOptions(origin?: string): CookieOptions {
return {
maxAge: 0,
...this.getConfig(origin),
};
}
}
File diff suppressed because one or more lines are too long