From db6bea66c8e9ff848af70db1d8c026f211189bb7 Mon Sep 17 00:00:00 2001 From: adam-kuhn Date: Fri, 14 Nov 2025 10:03:45 +0200 Subject: [PATCH 1/3] coerce 'expires_in' to be a number --- src/client/auth.test.ts | 41 +++++++++++++++++++++++++++++++++++++++-- src/shared/auth.ts | 2 +- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 8124fe768..451e39bca 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -14,7 +14,7 @@ import { selectClientAuthMethod } from './auth.js'; import { ServerError } from '../server/auth/errors.js'; -import { AuthorizationServerMetadata } from '../shared/auth.js'; +import { AuthorizationServerMetadata, OAuthTokens } from '../shared/auth.js'; // Mock fetch globally const mockFetch = jest.fn(); @@ -1073,7 +1073,7 @@ describe('OAuth Authorization', () => { }); describe('exchangeAuthorization', () => { - const validTokens = { + const validTokens: OAuthTokens = { access_token: 'access123', token_type: 'Bearer', expires_in: 3600, @@ -1132,6 +1132,43 @@ describe('OAuth Authorization', () => { expect(body.get('resource')).toBe('https://api.example.com/mcp-server'); }); + it('allows for string "expires_in" values', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ ...validTokens, expires_in: '3600' }) + }); + + const tokens = await exchangeAuthorization('https://auth.example.com', { + clientInformation: validClientInfo, + authorizationCode: 'code123', + codeVerifier: 'verifier123', + redirectUri: 'http://localhost:3000/callback', + resource: new URL('https://api.example.com/mcp-server') + }); + + expect(tokens).toEqual(validTokens); + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + href: 'https://auth.example.com/token' + }), + expect.objectContaining({ + method: 'POST', + headers: new Headers({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) + }) + ); + + const body = mockFetch.mock.calls[0][1].body as URLSearchParams; + expect(body.get('grant_type')).toBe('authorization_code'); + expect(body.get('code')).toBe('code123'); + expect(body.get('code_verifier')).toBe('verifier123'); + expect(body.get('client_id')).toBe('client123'); + expect(body.get('client_secret')).toBe('secret123'); + expect(body.get('redirect_uri')).toBe('http://localhost:3000/callback'); + expect(body.get('resource')).toBe('https://api.example.com/mcp-server'); + }); it('exchanges code for tokens with auth', async () => { mockFetch.mockResolvedValueOnce({ ok: true, diff --git a/src/shared/auth.ts b/src/shared/auth.ts index 819b33086..77514a66e 100644 --- a/src/shared/auth.ts +++ b/src/shared/auth.ts @@ -136,7 +136,7 @@ export const OAuthTokensSchema = z access_token: z.string(), id_token: z.string().optional(), // Optional for OAuth 2.1, but necessary in OpenID Connect token_type: z.string(), - expires_in: z.number().optional(), + expires_in: z.coerce.number().optional(), scope: z.string().optional(), refresh_token: z.string().optional() }) From 5eadcaf87f05c98d361522b14d97b8a30bb19068 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sun, 16 Nov 2025 10:18:46 +0200 Subject: [PATCH 2/3] fix test --- src/client/auth.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index dea05875e..7effe6273 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -1164,14 +1164,15 @@ describe('OAuth Authorization', () => { href: 'https://auth.example.com/token' }), expect.objectContaining({ - method: 'POST', - headers: new Headers({ - 'Content-Type': 'application/x-www-form-urlencoded' - }) - }) + method: 'POST' + }), ); - const body = mockFetch.mock.calls[0][1].body as URLSearchParams; + const options = mockFetch.mock.calls[0][1]; + expect(options.headers).toBeInstanceOf(Headers); + expect(options.headers.get('Content-Type')).toBe('application/x-www-form-urlencoded'); + + const body = options.body as URLSearchParams; expect(body.get('grant_type')).toBe('authorization_code'); expect(body.get('code')).toBe('code123'); expect(body.get('code_verifier')).toBe('verifier123'); From 75872e65a6860a1347717c07c794e8e765fa974c Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sun, 16 Nov 2025 10:19:56 +0200 Subject: [PATCH 3/3] prettier fix --- src/client/auth.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 7effe6273..d68d72275 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -1165,7 +1165,7 @@ describe('OAuth Authorization', () => { }), expect.objectContaining({ method: 'POST' - }), + }) ); const options = mockFetch.mock.calls[0][1];