diff --git a/src/plugin/pkce-flow.test.ts b/src/plugin/pkce-flow.test.ts index 5672838..341d57e 100644 --- a/src/plugin/pkce-flow.test.ts +++ b/src/plugin/pkce-flow.test.ts @@ -64,7 +64,7 @@ describe('handleCallbackRequest - Issue #4', () => { const mockResponse = { end: vi.fn(), - writeHead: vi.fn(), + writeHead: vi.fn() as unknown as http.ServerResponse['writeHead'], } as unknown as http.ServerResponse; const mockServer = { @@ -103,7 +103,7 @@ describe('handleCallbackRequest - Issue #4', () => { const mockResponse = { end: vi.fn(), - writeHead: vi.fn(), + writeHead: vi.fn() as unknown as http.ServerResponse['writeHead'], } as unknown as http.ServerResponse; const mockServer = { @@ -132,6 +132,37 @@ describe('handleCallbackRequest - Issue #4', () => { if ((resolvedResult as { type: string }).type !== 'failed') throw new Error('expected failed'); expect((resolvedResult as { error?: string }).error).toBe('Authentication failed: invalid_scope'); }); + + it('sets Cache-Control: no-store on error callback response', async () => { + const handleCallbackRequest = await loadHandleCallbackRequest(); + + const mockResponse = { + end: vi.fn(), + writeHead: vi.fn() as unknown as http.ServerResponse['writeHead'], + } as unknown as http.ServerResponse; + + const mockServer = { + close: vi.fn(), + } as unknown as http.Server; + + const parsedUrl = url.parse('/callback?error=access_denied', true); + + await handleCallbackRequest( + mockResponse, + mockServer, + parsedUrl, + 'valid-state', + 'verifier', + 'http://localhost:8787/callback', + () => {}, + ); + + expect(mockResponse.writeHead).toHaveBeenCalledTimes(1); + const [, headers] = (mockResponse.writeHead as unknown as ReturnType).mock.calls[0]; + expect(headers['Cache-Control']).toBe('no-store, no-cache, must-revalidate, proxy-revalidate'); + expect(headers['Pragma']).toBe('no-cache'); + expect(headers['Expires']).toBe('0'); + }); }); diff --git a/src/plugin/pkce-flow.ts b/src/plugin/pkce-flow.ts index 31ce4ce..dd415ad 100644 --- a/src/plugin/pkce-flow.ts +++ b/src/plugin/pkce-flow.ts @@ -297,6 +297,19 @@ export function generateCodeVerifier(): string { return Buffer.from(bytes).toString('base64url'); } +/** + * Sends an HTML response with cache-control headers appropriate for OAuth callbacks + */ +function writeHtmlResponse(response: http.ServerResponse, html: string): void { + response.writeHead(200, { + 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', + 'Content-Type': 'text/html', + 'Expires': '0', + 'Pragma': 'no-cache', + }); + response.end(html); +} + /** * Handles the OAuth callback request */ @@ -317,8 +330,7 @@ export async function handleCallbackRequest( if (error) { const displayMessage = errorDescription ? `${error}: ${errorDescription}` : error; - response.writeHead(200, { 'Content-Type': 'text/html' }); - response.end(buildHtmlResponse(false, displayMessage)); + writeHtmlResponse(response, buildHtmlResponse(false, displayMessage)); server.close(); resolve({ error: `Authentication failed: ${displayMessage}`, @@ -328,8 +340,7 @@ export async function handleCallbackRequest( } if (receivedState !== state) { - response.writeHead(200, { 'Content-Type': 'text/html' }); - response.end(buildHtmlResponse(false, 'Invalid state parameter')); + writeHtmlResponse(response, buildHtmlResponse(false, 'Invalid state parameter')); server.close(); resolve({ error: 'Invalid state parameter. Please try again.', @@ -339,8 +350,7 @@ export async function handleCallbackRequest( } if (!code) { - response.writeHead(200, { 'Content-Type': 'text/html' }); - response.end(buildHtmlResponse(false, 'No authorization code received')); + writeHtmlResponse(response, buildHtmlResponse(false, 'No authorization code received')); server.close(); resolve({ error: 'No authorization code received.', @@ -350,9 +360,8 @@ export async function handleCallbackRequest( } // Exchange code for tokens - response.writeHead(200, { 'Content-Type': 'text/html' }); - response.end(buildHtmlResponse(true, 'You can close this window and return to OpenCode.')); - server.close(); + writeHtmlResponse(response, buildHtmlResponse(true, 'You can close this window and return to OpenCode.')); + server.close(); const result = await exchangeCodeForTokens(code, codeVerifier, redirectUri); resolve(result);