Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions src/plugin/pkce-flow.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { AuthOAuthResult } from './types';
import * as http from 'node:http';
import * as url from 'node:url';
import { beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('../constants', () => ({
Expand Down Expand Up @@ -40,11 +43,98 @@ async function loadSubject() {
return mod.createPkceAuthorizeMethod;
}

async function loadHandleCallbackRequest() {
const mod = await import('./pkce-flow');
return mod.handleCallbackRequest;
}

async function loadExchangeCodeForTokens() {
const mod = await import('./pkce-flow');
return mod.exchangeCodeForTokens;
}

describe('handleCallbackRequest - Issue #4', () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});

it('includes error_description in the error message when present', async () => {
const handleCallbackRequest = await loadHandleCallbackRequest();

const mockResponse = {
end: vi.fn(),
writeHead: vi.fn(),
} as unknown as http.ServerResponse;

const mockServer = {
close: vi.fn(),
} as unknown as http.Server;

const parsedUrl = url.parse(
'/callback?error=access_denied&error_description=User+denied+consent',
true,
);

let resolvedResult: AuthOAuthResult | undefined;

await handleCallbackRequest(
mockResponse,
mockServer,
parsedUrl,
'valid-state',
'verifier',
'http://localhost:8787/callback',
(value) => {
resolvedResult = value as unknown as AuthOAuthResult;
},
);

expect(resolvedResult).toBeDefined();
if (typeof resolvedResult !== 'object' || resolvedResult === null) throw new Error('expected object');
expect((resolvedResult as { type: string }).type).toBe('failed');
if ((resolvedResult as { type: string }).type !== 'failed') throw new Error('expected failed');
expect((resolvedResult as { error?: string }).error).toContain('access_denied');
expect((resolvedResult as { error?: string }).error).toContain('User denied consent');
});

it('falls back to error code alone when error_description is absent', async () => {
const handleCallbackRequest = await loadHandleCallbackRequest();

const mockResponse = {
end: vi.fn(),
writeHead: vi.fn(),
} as unknown as http.ServerResponse;

const mockServer = {
close: vi.fn(),
} as unknown as http.Server;

const parsedUrl = url.parse('/callback?error=invalid_scope', true);

let resolvedResult: AuthOAuthResult | undefined;

await handleCallbackRequest(
mockResponse,
mockServer,
parsedUrl,
'valid-state',
'verifier',
'http://localhost:8787/callback',
(value) => {
resolvedResult = value as unknown as AuthOAuthResult;
},
);

expect(resolvedResult).toBeDefined();
if (typeof resolvedResult !== 'object' || resolvedResult === null) throw new Error('expected object');
expect((resolvedResult as { type: string }).type).toBe('failed');
if ((resolvedResult as { type: string }).type !== 'failed') throw new Error('expected failed');
expect((resolvedResult as { error?: string }).error).toBe('Authentication failed: invalid_scope');
});
});


describe('createPkceAuthorizeMethod - Issue #1', () => {
beforeEach(() => {
vi.clearAllMocks();
Expand Down
9 changes: 6 additions & 3 deletions src/plugin/pkce-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ function generateCodeVerifier(): string {
/**
* Handles the OAuth callback request
*/
async function handleCallbackRequest(
export async function handleCallbackRequest(
response: http.ServerResponse,
server: http.Server,
parsedUrl: url.UrlWithParsedQuery,
Expand All @@ -302,13 +302,16 @@ async function handleCallbackRequest(
const receivedState = parsedUrl.query.state as string;
const code = parsedUrl.query.code as string;
const error = parsedUrl.query.error as string;
const errorDescription = parsedUrl.query.error_description as string | undefined;

if (error) {
const displayMessage = errorDescription ? `${error}: ${errorDescription}` : error;

response.writeHead(200, { 'Content-Type': 'text/html' });
response.end(buildHtmlResponse(false, error));
response.end(buildHtmlResponse(false, displayMessage));
server.close();
resolve({
error: `Authentication failed: ${error}`,
error: `Authentication failed: ${displayMessage}`,
type: 'failed',
});
return;
Expand Down
Loading