Skip to content

Commit 48f4470

Browse files
pcarletonclaude
andcommitted
Simplify WWW-Authenticate parsing by keeping extractFieldFromWwwAuth private
Instead of exporting extractFieldFromWwwAuth as a separate function, keep it private and extend extractWWWAuthenticateParams to also return the 'error' field. This provides a cleaner API while maintaining all functionality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2b6592c commit 48f4470

File tree

3 files changed

+18
-53
lines changed

3 files changed

+18
-53
lines changed

src/client/auth.test.ts

Lines changed: 10 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
refreshAuthorization,
99
registerClient,
1010
discoverOAuthProtectedResourceMetadata,
11-
extractFieldFromWwwAuth,
1211
extractWWWAuthenticateParams,
1312
auth,
1413
type OAuthClientProvider,
@@ -36,50 +35,6 @@ describe('OAuth Authorization', () => {
3635
mockFetch.mockReset();
3736
});
3837

39-
describe('extractFieldFromWwwAuth', () => {
40-
function mockResponseWithWWWAuthenticate(headerValue: string): Response {
41-
return {
42-
headers: {
43-
get: vi.fn(name => (name === 'WWW-Authenticate' ? headerValue : null))
44-
}
45-
} as unknown as Response;
46-
}
47-
48-
it('returns the value of a quoted field', () => {
49-
const mockResponse = mockResponseWithWWWAuthenticate(`Bearer realm="example", field="value"`);
50-
expect(extractFieldFromWwwAuth(mockResponse, 'field')).toBe('value');
51-
});
52-
53-
it('returns the value of an unquoted field', () => {
54-
const mockResponse = mockResponseWithWWWAuthenticate(`Bearer realm=example, field=value`);
55-
expect(extractFieldFromWwwAuth(mockResponse, 'field')).toBe('value');
56-
});
57-
58-
it('returns the correct value when multiple parameters are present', () => {
59-
const mockResponse = mockResponseWithWWWAuthenticate(
60-
`Bearer realm="api", error="invalid_token", field="test_value", scope="admin"`
61-
);
62-
expect(extractFieldFromWwwAuth(mockResponse, 'field')).toBe('test_value');
63-
});
64-
65-
it('returns null if the field is not present', () => {
66-
const mockResponse = mockResponseWithWWWAuthenticate(`Bearer realm="api", scope="admin"`);
67-
expect(extractFieldFromWwwAuth(mockResponse, 'missing_field')).toBeNull();
68-
});
69-
70-
it('returns null if the WWW-Authenticate header is missing', () => {
71-
const mockResponse = { headers: new Headers() } as unknown as Response;
72-
expect(extractFieldFromWwwAuth(mockResponse, 'field')).toBeNull();
73-
});
74-
75-
it('handles fields with special characters in quotes', () => {
76-
const mockResponse = mockResponseWithWWWAuthenticate(
77-
`Bearer error="invalid_token", error_description="The token has expired, please re-authenticate."`
78-
);
79-
expect(extractFieldFromWwwAuth(mockResponse, 'error_description')).toBe('The token has expired, please re-authenticate.');
80-
});
81-
});
82-
8338
describe('extractWWWAuthenticateParams', () => {
8439
it('returns resource metadata url when present', async () => {
8540
const resourceUrl = 'https://resource.example.com/.well-known/oauth-protected-resource';
@@ -140,6 +95,16 @@ describe('OAuth Authorization', () => {
14095

14196
expect(extractWWWAuthenticateParams(mockResponse)).toEqual({ scope: scope });
14297
});
98+
99+
it('returns error when present', async () => {
100+
const mockResponse = {
101+
headers: {
102+
get: vi.fn(name => (name === 'WWW-Authenticate' ? `Bearer error="insufficient_scope", scope="admin"` : null))
103+
}
104+
} as unknown as Response;
105+
106+
expect(extractWWWAuthenticateParams(mockResponse)).toEqual({ error: 'insufficient_scope', scope: 'admin' });
107+
});
143108
});
144109

145110
describe('discoverOAuthProtectedResourceMetadata', () => {

src/client/auth.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -522,9 +522,9 @@ export async function selectResourceURL(
522522
}
523523

524524
/**
525-
* Extract resource_metadata and scope from WWW-Authenticate header.
525+
* Extract resource_metadata, scope, and error from WWW-Authenticate header.
526526
*/
527-
export function extractWWWAuthenticateParams(res: Response): { resourceMetadataUrl?: URL; scope?: string } {
527+
export function extractWWWAuthenticateParams(res: Response): { resourceMetadataUrl?: URL; scope?: string; error?: string } {
528528
const authenticateHeader = res.headers.get('WWW-Authenticate');
529529
if (!authenticateHeader) {
530530
return {};
@@ -547,10 +547,12 @@ export function extractWWWAuthenticateParams(res: Response): { resourceMetadataU
547547
}
548548

549549
const scope = extractFieldFromWwwAuth(res, 'scope') || undefined;
550+
const error = extractFieldFromWwwAuth(res, 'error') || undefined;
550551

551552
return {
552553
resourceMetadataUrl,
553-
scope
554+
scope,
555+
error
554556
};
555557
}
556558

@@ -561,7 +563,7 @@ export function extractWWWAuthenticateParams(res: Response): { resourceMetadataU
561563
* @param fieldName The name of the field to extract (e.g., "realm", "nonce").
562564
* @returns The field value
563565
*/
564-
export function extractFieldFromWwwAuth(response: Response, fieldName: string): string | null {
566+
function extractFieldFromWwwAuth(response: Response, fieldName: string): string | null {
565567
const wwwAuthHeader = response.headers.get('WWW-Authenticate');
566568
if (!wwwAuthHeader) {
567569
return null;

src/client/streamableHttp.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Transport, FetchLike, createFetchWithInit, normalizeHeaders } from '../shared/transport.js';
22
import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResponse, JSONRPCMessage, JSONRPCMessageSchema } from '../types.js';
3-
import { auth, AuthResult, extractFieldFromWwwAuth, extractWWWAuthenticateParams, OAuthClientProvider, UnauthorizedError } from './auth.js';
3+
import { auth, AuthResult, extractWWWAuthenticateParams, OAuthClientProvider, UnauthorizedError } from './auth.js';
44
import { EventSourceParserStream } from 'eventsource-parser/stream';
55

66
// Default reconnection options for StreamableHTTP connections
@@ -454,7 +454,7 @@ export class StreamableHTTPClientTransport implements Transport {
454454
}
455455

456456
if (response.status === 403 && this._authProvider) {
457-
const error = extractFieldFromWwwAuth(response, 'error');
457+
const { resourceMetadataUrl, scope, error } = extractWWWAuthenticateParams(response);
458458

459459
if (error === 'insufficient_scope') {
460460
const wwwAuthHeader = response.headers.get('WWW-Authenticate');
@@ -464,8 +464,6 @@ export class StreamableHTTPClientTransport implements Transport {
464464
throw new StreamableHTTPError(403, 'Server returned 403 after trying upscoping');
465465
}
466466

467-
const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
468-
469467
if (scope) {
470468
this._scope = scope;
471469
}

0 commit comments

Comments
 (0)