Fix support for cookies bigger than 4KB in size (i.e. refresh tokens).
Signed-off-by: Dominik Bargowski <dominik.bargowski@allegro.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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,126 @@ export class OAuthCookieManager {
|
||||
};
|
||||
}
|
||||
|
||||
setNonce(res: Response, nonce: string, origin?: string) {
|
||||
res.cookie(this.nonceCookie, nonce, {
|
||||
setNonce(res: Response, nonce: string, origin?: string): void {
|
||||
this.setCookie(res, this.nonceCookie, nonce, {
|
||||
maxAge: TEN_MINUTES_MS,
|
||||
...this.getConfig(origin, '/handler'),
|
||||
});
|
||||
}
|
||||
|
||||
setRefreshToken(res: Response, refreshToken: string, origin?: string) {
|
||||
res.cookie(this.refreshTokenCookie, refreshToken, {
|
||||
setRefreshToken(res: Response, refreshToken: string, origin?: string): void {
|
||||
this.setCookie(res, this.refreshTokenCookie, refreshToken, {
|
||||
maxAge: THOUSAND_DAYS_MS,
|
||||
...this.getConfig(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, {
|
||||
setGrantedScopes(res: Response, scope: string, origin?: string): void {
|
||||
this.setCookie(res, this.grantedScopeCookie, scope, {
|
||||
maxAge: THOUSAND_DAYS_MS,
|
||||
...this.getConfig(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,
|
||||
options: CookieOptions,
|
||||
): Response {
|
||||
if (val.length > MAX_COOKIE_SIZE_CHARACTERS) {
|
||||
const chunked = this.splitCookieToChunks(val, MAX_COOKIE_SIZE_CHARACTERS);
|
||||
let output = res;
|
||||
chunked.forEach((value, chunkNumber) => {
|
||||
output = output.cookie(
|
||||
OAuthCookieManager.getCookieChunkName(name, chunkNumber),
|
||||
value,
|
||||
options,
|
||||
);
|
||||
});
|
||||
return output;
|
||||
}
|
||||
return res.cookie(name, val, options);
|
||||
}
|
||||
|
||||
private getCookie(req: Request, name: string): string | undefined {
|
||||
const isChunked =
|
||||
!!req.cookies[OAuthCookieManager.getCookieChunkName(name, 0)];
|
||||
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 = {
|
||||
maxAge: 0,
|
||||
...this.getConfig(origin),
|
||||
};
|
||||
const isChunked =
|
||||
!!req.cookies[OAuthCookieManager.getCookieChunkName(name, 0)];
|
||||
if (isChunked) {
|
||||
const oldFormatExists = !!req.cookies[name];
|
||||
let output: Response = oldFormatExists
|
||||
? 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 getCookieChunkName(name: string, chunkIndex: number): string {
|
||||
return `${name}-${chunkIndex}`;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user