coerce 'expires_in' to be a number (#1111)

Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com>
Co-authored-by: Konstantin Konstantinov <konstantin@mach5technology.com>
This commit is contained in:
Adam Kuhn 2025-11-28 16:13:49 +02:00 • committed by GitHub
commit b4c6090bf2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 41 additions and 3 deletions

View file

@ -15,7 +15,7 @@ import {
isHttpsUrl
} from './auth.js';
import { InvalidClientMetadataError, ServerError } from '../server/auth/errors.js';
import { AuthorizationServerMetadata } from '../shared/auth.js';
import { AuthorizationServerMetadata, OAuthTokens } from '../shared/auth.js';
import { expect, vi, type Mock } from 'vitest';
// Mock pkce-challenge
@ -1093,7 +1093,7 @@ describe('OAuth Authorization', () => {
});
describe('exchangeAuthorization', () => {
const validTokens = {
const validTokens: OAuthTokens = {
access_token: 'access123',
token_type: 'Bearer',
expires_in: 3600,
@ -1154,6 +1154,44 @@ 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'
})
);
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');
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,

View file

@ -132,7 +132,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()
})