Skip to content

Commit ba7f4e3

Browse files
author
pmor
committed
fix: bound OAuth authorization to one deadline
1 parent 4649e92 commit ba7f4e3

6 files changed

Lines changed: 39 additions & 9 deletions

File tree

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20609,13 +20609,17 @@ async function discoverOAuthServerInfo(serverUrl, headers = {}) {
2060920609
wwwAuthenticateScope
2061020610
};
2061120611
}
20612-
async function finishOAuthAuthorization(finishAuth, authorizationCode, authTimeoutMs = 3e4) {
20612+
async function finishOAuthAuthorization(finishAuth, authorizationCode, authTimeoutMs = 3e4, authorizationDeadlineMs = Date.now() + authTimeoutMs) {
20613+
const remainingMs = authorizationDeadlineMs - Date.now();
20614+
if (remainingMs <= 0) {
20615+
throw new Error("OAuth authorization deadline expired before token exchange");
20616+
}
2061320617
let timeout;
2061420618
try {
2061520619
await Promise.race([
2061620620
finishAuth(authorizationCode),
2061720621
new Promise((_, reject) => {
20618-
timeout = setTimeout(() => reject(new Error(`OAuth token exchange timed out after ${authTimeoutMs / 1e3} seconds`)), authTimeoutMs);
20622+
timeout = setTimeout(() => reject(new Error(`OAuth token exchange timed out after ${remainingMs / 1e3} seconds`)), remainingMs);
2061920623
})
2062020624
]);
2062120625
} finally {
@@ -20724,11 +20728,17 @@ async function connectToRemoteServer(client, serverUrl, authProvider, headers, a
2072420728
}
2072520729
log("Authentication required. Waiting for authorization...");
2072620730
debugLog("Waiting for auth code from callback server");
20731+
const authorizationDeadlineMs = Date.now() + authTimeoutMs;
2072720732
const code = await waitForAuthCode();
2072820733
debugLog("Received auth code from callback server");
2072920734
try {
2073020735
log("Completing authorization...");
20731-
await finishOAuthAuthorization((authorizationCode) => transport.finishAuth(authorizationCode), code, authTimeoutMs);
20736+
await finishOAuthAuthorization(
20737+
(authorizationCode) => transport.finishAuth(authorizationCode),
20738+
code,
20739+
authTimeoutMs,
20740+
authorizationDeadlineMs
20741+
);
2073220742
markAuthCompleted();
2073320743
debugLog("Authorization completed successfully");
2073420744
if (recursionReasons.has(REASON_AUTH_NEEDED)) {

dist/client.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
parseCommandLineArgs,
1313
setupSignalHandlers,
1414
version
15-
} from "./chunk-QTFHAWVT.js";
15+
} from "./chunk-YZXW5VW3.js";
1616

1717
// src/client.ts
1818
import { EventEmitter } from "events";

dist/proxy.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
mcpProxy,
1212
parseCommandLineArgs,
1313
setupSignalHandlers
14-
} from "./chunk-QTFHAWVT.js";
14+
} from "./chunk-YZXW5VW3.js";
1515

1616
// src/proxy.ts
1717
import { EventEmitter } from "events";
@@ -174,13 +174,14 @@ async function runProxy(serverUrl, callbackPort, headers, transportStrategy = "h
174174
log("OAuth was completed by another process; verifying the shared token");
175175
return;
176176
}
177+
const authorizationDeadlineMs = Date.now() + authState.authTimeoutMs;
177178
const code = await authState.waitForNextAuthCode();
178179
const oauthTransport = remoteTransport;
179180
if (!oauthTransport.finishAuth) {
180181
throw new Error("Remote transport does not support OAuth authorization completion");
181182
}
182183
log("Completing renewed authorization...");
183-
await finishOAuthAuthorization(oauthTransport.finishAuth.bind(oauthTransport), code, authState.authTimeoutMs);
184+
await finishOAuthAuthorization(oauthTransport.finishAuth.bind(oauthTransport), code, authState.authTimeoutMs, authorizationDeadlineMs);
184185
authState.markAuthCompleted();
185186
log("Renewed OAuth token; verifying it with the remote MCP server");
186187
};

src/lib/utils.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ import { OAuthAuthorizationPendingError } from './types'
1818
// All sanitizeUrl tests have been moved to the strict-url-sanitise package
1919

2020
describe('finishOAuthAuthorization', () => {
21+
it('rejects immediately when the shared authorization deadline has already expired', async () => {
22+
await expect(finishOAuthAuthorization(async () => undefined, 'authorization-code', 20, Date.now() - 1)).rejects.toThrow(
23+
'OAuth authorization deadline expired before token exchange',
24+
)
25+
})
26+
2127
it('rejects when token exchange does not complete before the authorization deadline', async () => {
2228
await expect(finishOAuthAuthorization(() => new Promise<void>(() => {}), 'authorization-code', 20)).rejects.toThrow(
2329
'OAuth token exchange timed out after 0.02 seconds',

src/lib/utils.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -547,13 +547,19 @@ export async function finishOAuthAuthorization(
547547
finishAuth: (authorizationCode: string) => Promise<void>,
548548
authorizationCode: string,
549549
authTimeoutMs: number = 30000,
550+
authorizationDeadlineMs: number = Date.now() + authTimeoutMs,
550551
): Promise<void> {
552+
const remainingMs = authorizationDeadlineMs - Date.now()
553+
if (remainingMs <= 0) {
554+
throw new Error('OAuth authorization deadline expired before token exchange')
555+
}
556+
551557
let timeout: ReturnType<typeof setTimeout> | undefined
552558
try {
553559
await Promise.race([
554560
finishAuth(authorizationCode),
555561
new Promise<void>((_, reject) => {
556-
timeout = setTimeout(() => reject(new Error(`OAuth token exchange timed out after ${authTimeoutMs / 1000} seconds`)), authTimeoutMs)
562+
timeout = setTimeout(() => reject(new Error(`OAuth token exchange timed out after ${remainingMs / 1000} seconds`)), remainingMs)
557563
}),
558564
])
559565
} finally {
@@ -728,12 +734,18 @@ export async function connectToRemoteServer(
728734

729735
// Wait for the authorization code from the callback
730736
debugLog('Waiting for auth code from callback server')
737+
const authorizationDeadlineMs = Date.now() + authTimeoutMs
731738
const code = await waitForAuthCode()
732739
debugLog('Received auth code from callback server')
733740

734741
try {
735742
log('Completing authorization...')
736-
await finishOAuthAuthorization((authorizationCode) => transport.finishAuth(authorizationCode), code, authTimeoutMs)
743+
await finishOAuthAuthorization(
744+
(authorizationCode) => transport.finishAuth(authorizationCode),
745+
code,
746+
authTimeoutMs,
747+
authorizationDeadlineMs,
748+
)
737749
markAuthCompleted()
738750
debugLog('Authorization completed successfully')
739751

src/proxy.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ async function runProxy(
130130
return
131131
}
132132

133+
const authorizationDeadlineMs = Date.now() + authState.authTimeoutMs
133134
const code = await authState.waitForNextAuthCode()
134135
const oauthTransport = remoteTransport as typeof remoteTransport & {
135136
finishAuth?: (authorizationCode: string) => Promise<void>
@@ -138,7 +139,7 @@ async function runProxy(
138139
throw new Error('Remote transport does not support OAuth authorization completion')
139140
}
140141
log('Completing renewed authorization...')
141-
await finishOAuthAuthorization(oauthTransport.finishAuth.bind(oauthTransport), code, authState.authTimeoutMs)
142+
await finishOAuthAuthorization(oauthTransport.finishAuth.bind(oauthTransport), code, authState.authTimeoutMs, authorizationDeadlineMs)
142143
authState.markAuthCompleted()
143144
log('Renewed OAuth token; verifying it with the remote MCP server')
144145
}

0 commit comments

Comments
 (0)