From a0a9a4a8f532e36df771b33228ba4e6c2ce884a7 Mon Sep 17 00:00:00 2001 From: Dominik Bargowski Date: Fri, 2 Aug 2024 14:00:42 +0200 Subject: [PATCH 1/2] Fix support for cookies bigger than 4KB in size (i.e. refresh tokens). Signed-off-by: Dominik Bargowski --- .changeset/rare-otters-judge.md | 5 + .../auth-node/src/oauth/OAuthCookieManager.ts | 118 ++++++++++++++--- .../oauth/createOAuthRouteHandlers.test.ts | 121 +++++++++++++++++- 3 files changed, 223 insertions(+), 21 deletions(-) create mode 100644 .changeset/rare-otters-judge.md diff --git a/.changeset/rare-otters-judge.md b/.changeset/rare-otters-judge.md new file mode 100644 index 0000000000..3c3f91e094 --- /dev/null +++ b/.changeset/rare-otters-judge.md @@ -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. diff --git a/plugins/auth-node/src/oauth/OAuthCookieManager.ts b/plugins/auth-node/src/oauth/OAuthCookieManager.ts index 1d06242a2e..ac9531a8aa 100644 --- a/plugins/auth-node/src/oauth/OAuthCookieManager.ts +++ b/plugins/auth-node/src/oauth/OAuthCookieManager.ts @@ -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(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}`; } } diff --git a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts index 7da5519655..0c94171881 100644 --- a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts +++ b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts @@ -49,6 +49,9 @@ const mockSession = { refreshToken: 'refresh-token', }; +const fiveKilobyteRefreshToken = + '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'; + const baseConfig = { authenticator: mockAuthenticator, appUrl: 'http://127.0.0.1', @@ -91,7 +94,15 @@ function getNonceCookie(test: SuperAgentTest) { }); } -function getRefreshTokenCookie(test: SuperAgentTest) { +function getRefreshTokenCookie(test: SuperAgentTest, chunkNumber?: number) { + if (chunkNumber !== undefined) { + return test.jar.getCookie(`my-provider-refresh-token-${chunkNumber}`, { + domain: '127.0.0.1', + path: '/my-provider', + script: false, + secure: false, + }); + } return test.jar.getCookie('my-provider-refresh-token', { domain: '127.0.0.1', path: '/my-provider', @@ -267,6 +278,60 @@ describe('createOAuthRouteHandlers', () => { expect(getGrantedScopesCookie(agent)).toBeUndefined(); }); + it('should authenticate when refresh token is bigger than 4KB', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-nonce=123', + '127.0.0.1', + '/my-provider/handler', + ); + + mockAuthenticator.authenticate.mockResolvedValue({ + fullProfile: { id: 'id' } as PassportProfile, + session: { + ...mockSession, + refreshToken: fiveKilobyteRefreshToken, + }, + }); + + const res = await agent.get('/my-provider/handler/frame').query({ + state: encodeOAuthState({ + env: 'development', + nonce: '123', + } as OAuthState), + }); + + expect(mockAuthenticator.authenticate).toHaveBeenCalledWith( + { req: expect.anything() }, + { ctx: 'authenticator' }, + ); + + expect(res.status).toBe(200); + expect(parseWebMessageResponse(res.text).response).toEqual({ + type: 'authorization_response', + response: { + profile: {}, + providerInfo: { + accessToken: 'access-token', + expiresInSeconds: 3, + idToken: 'id-token', + scope: 'my-scope', + }, + }, + }); + + expect(getRefreshTokenCookie(agent, 0).value).toBe( + fiveKilobyteRefreshToken.slice(0, 4000), + ); + expect(getRefreshTokenCookie(agent, 1).value).toBe( + fiveKilobyteRefreshToken.slice(4000), + ); + expect(getGrantedScopesCookie(agent)).toBeUndefined(); + }); + it('should authenticate with sign-in, profile transform, and persisted scopes', async () => { const agent = request.agent( wrapInApp( @@ -556,6 +621,60 @@ describe('createOAuthRouteHandlers', () => { }); }); + it('should refresh for chunked token', async () => { + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + `my-provider-refresh-token-0=${fiveKilobyteRefreshToken.slice( + 0, + 4000, + )}`, + '127.0.0.1', + '/my-provider', + ); + agent.jar.setCookie( + `my-provider-refresh-token-1=${fiveKilobyteRefreshToken.slice(4000)}`, + '127.0.0.1', + '/my-provider', + ); + + mockAuthenticator.refresh.mockImplementation(async ({ scope }) => ({ + fullProfile: { id: 'id' } as PassportProfile, + session: { + ...mockSession, + scope, + refreshToken: fiveKilobyteRefreshToken, + }, + })); + + const res = await agent + .post('/my-provider/refresh') + .set('X-Requested-With', 'XMLHttpRequest') + .query({ scope: 'my-scope' }); + + expect(mockAuthenticator.refresh).toHaveBeenCalledWith( + { + req: expect.anything(), + refreshToken: fiveKilobyteRefreshToken, + scope: 'my-scope', + }, + { ctx: 'authenticator' }, + ); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + profile: {}, + providerInfo: { + accessToken: 'access-token', + expiresInSeconds: 3, + idToken: 'id-token', + scope: 'my-scope', + }, + }); + }); + it('should refresh with sign-in, profile transform, and persisted scopes', async () => { const agent = request.agent( wrapInApp( From 7c22e9458c7cb1742f9f4eb4856ad8da0cb66301 Mon Sep 17 00:00:00 2001 From: Dominik Bargowski Date: Wed, 4 Sep 2024 11:01:18 +0200 Subject: [PATCH 2/2] Handle edge cases related to refresh token cookie going under/over the limit. Signed-off-by: Dominik Bargowski --- .../auth-node/src/oauth/OAuthCookieManager.ts | 94 ++++++++++++++----- 1 file changed, 69 insertions(+), 25 deletions(-) diff --git a/plugins/auth-node/src/oauth/OAuthCookieManager.ts b/plugins/auth-node/src/oauth/OAuthCookieManager.ts index ac9531a8aa..d594d7e78b 100644 --- a/plugins/auth-node/src/oauth/OAuthCookieManager.ts +++ b/plugins/auth-node/src/oauth/OAuthCookieManager.ts @@ -88,17 +88,24 @@ export class OAuthCookieManager { } setNonce(res: Response, nonce: string, origin?: string): void { - this.setCookie(res, this.nonceCookie, nonce, { - maxAge: TEN_MINUTES_MS, - ...this.getConfig(origin, '/handler'), - }); + this.setCookie( + res, + this.nonceCookie, + nonce, + TEN_MINUTES_MS, + origin, + '/handler', + ); } setRefreshToken(res: Response, refreshToken: string, origin?: string): void { - this.setCookie(res, this.refreshTokenCookie, refreshToken, { - maxAge: THOUSAND_DAYS_MS, - ...this.getConfig(origin), - }); + this.setCookie( + res, + this.refreshTokenCookie, + refreshToken, + THOUSAND_DAYS_MS, + origin, + ); } removeRefreshToken(res: Response, origin?: string): void { @@ -110,10 +117,13 @@ export class OAuthCookieManager { } setGrantedScopes(res: Response, scope: string, origin?: string): void { - this.setCookie(res, this.grantedScopeCookie, scope, { - maxAge: THOUSAND_DAYS_MS, - ...this.getConfig(origin), - }); + this.setCookie( + res, + this.grantedScopeCookie, + scope, + THOUSAND_DAYS_MS, + origin, + ); } getNonce(req: Request): string | undefined { @@ -132,11 +142,23 @@ export class OAuthCookieManager { res: Response, name: string, val: string, - options: CookieOptions, + 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); - let output = res; chunked.forEach((value, chunkNumber) => { output = output.cookie( OAuthCookieManager.getCookieChunkName(name, chunkNumber), @@ -146,12 +168,27 @@ export class OAuthCookieManager { }); return output; } - return res.cookie(name, val, options); + + 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 = - !!req.cookies[OAuthCookieManager.getCookieChunkName(name, 0)]; + const isChunked = OAuthCookieManager.chunkedCookieExists(req, name); if (isChunked) { const chunks: string[] = []; let chunkNumber = 0; @@ -170,15 +207,11 @@ export class OAuthCookieManager { 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)]; + const options = this.getRemoveCookieOptions(origin); + const isChunked = OAuthCookieManager.chunkedCookieExists(req, name); if (isChunked) { - const oldFormatExists = !!req.cookies[name]; - let output: Response = oldFormatExists + const nonChunkedFormatExists = !!req.cookies[name]; + let output: Response = nonChunkedFormatExists ? res.cookie(name, '', options) : res; for (let chunkNumber = 0; ; chunkNumber++) { @@ -206,7 +239,18 @@ export class OAuthCookieManager { 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), + }; + } }