Skip to content
42 changes: 40 additions & 2 deletions src/client/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
import { expect, vi, type Mock } from 'vitest';

// Mock pkce-challenge
Expand Down Expand Up @@ -1082,7 +1082,7 @@ describe('OAuth Authorization', () => {
});

describe('exchangeAuthorization', () => {
const validTokens = {
const validTokens: OAuthTokens = {
access_token: 'access123',
token_type: 'Bearer',
expires_in: 3600,
Expand Down Expand Up @@ -1143,6 +1143,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,
Expand Down
2 changes: 1 addition & 1 deletion src/shared/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Expand Down
Loading