Skip to content

Commit 2db8380

Browse files
committed
fix: Implement client credentials token exchange directly
1 parent ab75d7c commit 2db8380

File tree

1 file changed

+82
-6
lines changed

1 file changed

+82
-6
lines changed

e2e_tests/typescript/src/server_clients/automated_oauth.ts

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "@modelcontextprotocol/sdk/shared/auth.js";
1616
import {
1717
OAuthClientProvider,
18+
discoverOAuthMetadata,
1819
discoverOAuthProtectedResourceMetadata,
1920
extractResourceMetadataUrl,
2021
} from "@modelcontextprotocol/sdk/client/auth.js";
@@ -386,6 +387,9 @@ export class AutomatedOAuthClient extends Server {
386387
throw new Error("OAuth provider not initialized");
387388
}
388389

390+
// Perform client credentials flow directly for machine-to-machine authentication
391+
await this.performClientCredentialsFlow();
392+
389393
logger.debug("Creating transport with automated OAuth provider...");
390394
const baseUrl = new URL(this.config.serverUrl);
391395
const transport = new StreamableHTTPClientTransport(baseUrl, {
@@ -396,16 +400,85 @@ export class AutomatedOAuthClient extends Server {
396400
await this.client.connect(transport);
397401
logger.debug("Connected successfully with automated OAuth");
398402
}
403+
404+
/**
405+
* Performs the client credentials OAuth flow to obtain access tokens
406+
*/
407+
private async performClientCredentialsFlow(): Promise<void> {
408+
if (!this.oauthProvider) {
409+
throw new Error("OAuth provider not initialized");
410+
}
411+
412+
try {
413+
// Check if we already have a tokens
414+
if (this.oauthProvider.tokens()?.access_token) {
415+
logger.debug("Using existing access token");
416+
return;
417+
}
418+
419+
logger.debug("Performing client credentials flow...");
420+
421+
// Discover OAuth metadata
422+
const metadata = await discoverOAuthMetadata(this.config.serverUrl);
423+
424+
if (!metadata?.token_endpoint) {
425+
throw new Error("No token endpoint found in OAuth metadata");
426+
}
427+
428+
const clientInfo = this.oauthProvider.clientInformation();
429+
if (!clientInfo) {
430+
throw new Error("No client information available");
431+
}
432+
433+
// Perform client credentials token request
434+
const tokenUrl = new URL(metadata.token_endpoint);
435+
const params = new URLSearchParams({
436+
grant_type: "client_credentials",
437+
client_id: clientInfo.client_id,
438+
client_secret: clientInfo.client_secret!,
439+
scope: this.oauthProvider.clientMetadata.scope || "",
440+
});
441+
442+
logger.debug(`Making token request to: ${tokenUrl}`);
443+
const response = await fetch(tokenUrl, {
444+
method: "POST",
445+
headers: {
446+
"Content-Type": "application/x-www-form-urlencoded",
447+
},
448+
body: params,
449+
});
450+
451+
if (!response.ok) {
452+
const errorText = await response.text();
453+
throw new Error(
454+
`Token request failed: HTTP ${response.status} - ${errorText}`
455+
);
456+
}
457+
458+
const tokenResponse = await response.json();
459+
460+
logger.debug(
461+
"Successfully obtained access token via client credentials flow"
462+
);
463+
464+
// Save the tokens (saveTokens will automatically track the issued time)
465+
this.oauthProvider.saveTokens(tokenResponse);
466+
} catch (error) {
467+
logger.error("Client credentials flow failed:", error);
468+
throw error;
469+
}
470+
}
399471
}
400472

401473
/**
402474
* OAuth client provider for automated (client credentials) OAuth flows.
403475
* This provider handles machine-to-machine authentication without user interaction.
476+
* For the purposes of the integration tests, this implementation does not
477+
* refresh tokens and assumes that the intial token will not expire before the test completes.
404478
*/
405479
class AutomatedOAuthClientProvider implements OAuthClientProvider {
406480
private _clientInformation: OAuthClientInformationFull;
407481
private _tokens?: OAuthTokens;
408-
private _codeVerifier?: string;
409482

410483
constructor(
411484
private readonly _clientMetadata: OAuthClientMetadata,
@@ -452,13 +525,16 @@ class AutomatedOAuthClientProvider implements OAuthClientProvider {
452525
}
453526

454527
saveCodeVerifier(codeVerifier: string): void {
455-
this._codeVerifier = codeVerifier;
528+
// Not used in client credentials flow
529+
throw new Error(
530+
"saveCodeVerifier should not be called in automated OAuth flow"
531+
);
456532
}
457533

458534
codeVerifier(): string {
459-
if (!this._codeVerifier) {
460-
throw new Error("No code verifier saved");
461-
}
462-
return this._codeVerifier;
535+
// Not used in client credentials flow
536+
throw new Error(
537+
"codeVerifier should not be called in automated OAuth flow"
538+
);
463539
}
464540
}

0 commit comments

Comments
 (0)