Merge branch 'main' into debugger-auth-svr-metadata

This commit is contained in:
Ola Hungerford 2025-08-12 06:44:32 -07:00 • committed by GitHub
commit e604321024
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 29 additions and 8 deletions

View file

@ -8,6 +8,7 @@ import {
OAuthMetadata,
} from "@modelcontextprotocol/sdk/shared/auth.js";
import { SESSION_KEYS, getServerSpecificKey } from "./constants";
import { generateOAuthState } from "@/utils/oauthUtils";
export const getClientInformationFromSessionStorage = async ({
serverUrl,
@ -86,6 +87,10 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
};
}
state(): string | Promise<string> {
return generateOAuthState();
}
async clientInformation() {
// Try to get the preregistered client information from session storage first
const preregisteredClientInformation =

View file

@ -12,6 +12,7 @@ import {
OAuthMetadataSchema,
OAuthProtectedResourceMetadata,
} from "@modelcontextprotocol/sdk/shared/auth.js";
import { generateOAuthState } from "@/utils/oauthUtils";
export interface StateMachineContext {
state: AuthDebuggerState;
@ -113,13 +114,6 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
scope = metadata.scopes_supported.join(" ");
}
// Generate a random state
const array = new Uint8Array(32);
crypto.getRandomValues(array);
const state = Array.from(array, (byte) =>
byte.toString(16).padStart(2, "0"),
).join("");
const { authorizationUrl, codeVerifier } = await startAuthorization(
context.serverUrl,
{
@ -127,7 +121,7 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
clientInformation,
redirectUrl: context.provider.redirectUrl,
scope,
state: state,
state: generateOAuthState(),
resource: context.state.resource ?? undefined,
},
);

View file

@ -0,0 +1,86 @@
import {
generateOAuthErrorDescription,
parseOAuthCallbackParams,
generateOAuthState,
} from "@/utils/oauthUtils.ts";
describe("parseOAuthCallbackParams", () => {
it("Returns successful: true and code when present", () => {
expect(parseOAuthCallbackParams("?code=fake-code")).toEqual({
successful: true,
code: "fake-code",
});
});
it("Returns successful: false and error when error is present", () => {
expect(parseOAuthCallbackParams("?error=access_denied")).toEqual({
successful: false,
error: "access_denied",
error_description: null,
error_uri: null,
});
});
it("Returns optional error metadata fields when present", () => {
const search =
"?error=access_denied&" +
"error_description=User%20Denied%20Request&" +
"error_uri=https%3A%2F%2Fexample.com%2Ferror-docs";
expect(parseOAuthCallbackParams(search)).toEqual({
successful: false,
error: "access_denied",
error_description: "User Denied Request",
error_uri: "https://example.com/error-docs",
});
});
it("Returns error when nothing present", () => {
expect(parseOAuthCallbackParams("?")).toEqual({
successful: false,
error: "invalid_request",
error_description: "Missing code or error in response",
error_uri: null,
});
});
});
describe("generateOAuthErrorDescription", () => {
it("When only error is present", () => {
expect(
generateOAuthErrorDescription({
successful: false,
error: "invalid_request",
error_description: null,
error_uri: null,
}),
).toBe("Error: invalid_request.");
});
it("When error description is present", () => {
expect(
generateOAuthErrorDescription({
successful: false,
error: "invalid_request",
error_description: "The request could not be completed as dialed",
error_uri: null,
}),
).toEqual(
"Error: invalid_request.\nDetails: The request could not be completed as dialed.",
);
});
it("When all fields present", () => {
expect(
generateOAuthErrorDescription({
successful: false,
error: "invalid_request",
error_description: "The request could not be completed as dialed",
error_uri: "https://example.com/error-docs",
}),
).toEqual(
"Error: invalid_request.\nDetails: The request could not be completed as dialed.\nMore info: https://example.com/error-docs.",
);
});
describe("generateOAuthState", () => {
it("Returns a string", () => {
expect(generateOAuthState()).toBeDefined();
expect(generateOAuthState()).toHaveLength(64);
});
});
});

View file

@ -48,6 +48,20 @@ export const parseOAuthCallbackParams = (location: string): CallbackParams => {
};
};
/**
* Generate a random state for the OAuth 2.0 flow.
*
* @returns A random state for the OAuth 2.0 flow.
*/
export const generateOAuthState = () => {
// Generate a random state
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
"",
);
};
export const generateOAuthErrorDescription = (
params: Extract<CallbackParams, { successful: false }>,
): string => {