Make authorizationUrl and tokenUrl mandatory

This commit is contained in:
Allen Zhou 2025-02-26 14:00:34 -08:00
commit 57f59c69a1
4 changed files with 36 additions and 83 deletions

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "@modelcontextprotocol/sdk",
"version": "1.5.0",
"version": "1.6.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@modelcontextprotocol/sdk",
"version": "1.5.0",
"version": "1.6.0",
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",

View file

@ -298,6 +298,7 @@ describe('Token Handler', () => {
const proxyProvider = new ProxyOAuthServerProvider({
endpoints: {
authorizationUrl: 'https://example.com/authorize',
tokenUrl: 'https://example.com/token'
},
verifyAccessToken: async (token) => ({

View file

@ -22,7 +22,7 @@ describe("Proxy OAuth Server Provider", () => {
// Mock provider functions
const mockVerifyToken = jest.fn();
const mockGetClient = jest.fn();
// Base provider options
const baseOptions: ProxyOptions = {
endpoints: {
@ -66,7 +66,7 @@ describe("Proxy OAuth Server Provider", () => {
// Add helper function for failed responses
const mockFailedResponse = () => {
(global.fetch as jest.Mock).mockImplementation(() =>
(global.fetch as jest.Mock).mockImplementation(() =>
Promise.resolve({
ok: false,
status: 400,
@ -103,20 +103,6 @@ describe("Proxy OAuth Server Provider", () => {
expect(mockResponse.redirect).toHaveBeenCalledWith(expectedUrl.toString());
});
it("throws error when authorization endpoint is not configured", async () => {
const providerWithoutAuth = new ProxyOAuthServerProvider({
...baseOptions,
endpoints: { ...baseOptions.endpoints, authorizationUrl: undefined },
});
await expect(
providerWithoutAuth.authorize(validClient, {
redirectUri: "https://example.com/callback",
codeChallenge: "test-challenge",
}, mockResponse)
).rejects.toThrow("No authorization endpoint configured");
});
});
describe("token exchange", () => {
@ -128,7 +114,7 @@ describe("Proxy OAuth Server Provider", () => {
};
beforeEach(() => {
(global.fetch as jest.Mock).mockImplementation(() =>
(global.fetch as jest.Mock).mockImplementation(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockTokenResponse),
@ -176,23 +162,6 @@ describe("Proxy OAuth Server Provider", () => {
expect(tokens).toEqual(mockTokenResponse);
});
it("throws error when token endpoint is not configured", async () => {
const providerWithoutToken = new ProxyOAuthServerProvider({
...baseOptions,
endpoints: { ...baseOptions.endpoints, tokenUrl: undefined },
});
await expect(
providerWithoutToken.exchangeAuthorizationCode(validClient, "test-code")
).rejects.toThrow("No token endpoint configured");
});
it("handles token exchange failure", async () => {
mockFailedResponse();
await expect(
provider.exchangeAuthorizationCode(validClient, "invalid-code")
).rejects.toThrow(ServerError);
});
});
describe("client registration", () => {
@ -202,7 +171,7 @@ describe("Proxy OAuth Server Provider", () => {
redirect_uris: ["https://new-client.com/callback"],
};
(global.fetch as jest.Mock).mockImplementation(() =>
(global.fetch as jest.Mock).mockImplementation(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(newClient),
@ -239,7 +208,7 @@ describe("Proxy OAuth Server Provider", () => {
describe("token revocation", () => {
it("revokes token", async () => {
(global.fetch as jest.Mock).mockImplementation(() =>
(global.fetch as jest.Mock).mockImplementation(() =>
Promise.resolve({
ok: true,
})

View file

@ -1,9 +1,9 @@
import { Response } from "express";
import { OAuthRegisteredClientsStore } from "./clients.js";
import {
OAuthClientInformationFull,
OAuthClientInformationFullSchema,
OAuthTokenRevocationRequest,
import {
OAuthClientInformationFull,
OAuthClientInformationFullSchema,
OAuthTokenRevocationRequest,
OAuthTokens,
OAuthTokensSchema,
} from "./../../shared/auth.js";
@ -12,8 +12,8 @@ import { AuthorizationParams, OAuthServerProvider } from "./provider.js";
import { ServerError } from "./errors.js";
export type ProxyEndpoints = {
authorizationUrl?: string;
tokenUrl?: string;
authorizationUrl: string;
tokenUrl: string;
revocationUrl?: string;
registrationUrl?: string;
};
@ -24,14 +24,14 @@ export type ProxyOptions = {
*/
endpoints: ProxyEndpoints;
/**
* Function to verify access tokens and return auth info
*/
verifyAccessToken: (token: string) => Promise<AuthInfo>;
/**
* Function to verify access tokens and return auth info
*/
verifyAccessToken: (token: string) => Promise<AuthInfo>;
/**
* Function to fetch client information from the upstream server
*/
/**
* Function to fetch client information from the upstream server
*/
getClient: (clientId: string) => Promise<OAuthClientInformationFull | undefined>;
};
@ -45,7 +45,7 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
protected readonly _getClient: (clientId: string) => Promise<OAuthClientInformationFull | undefined>;
revokeToken?: (
client: OAuthClientInformationFull,
client: OAuthClientInformationFull,
request: OAuthTokenRevocationRequest
) => Promise<void>;
@ -55,15 +55,15 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
this._getClient = options.getClient;
if (options.endpoints?.revocationUrl) {
this.revokeToken = async (
client: OAuthClientInformationFull,
client: OAuthClientInformationFull,
request: OAuthTokenRevocationRequest
) => {
const revocationUrl = this._endpoints.revocationUrl;
if (!revocationUrl) {
throw new Error("No revocation endpoint configured");
}
const params = new URLSearchParams();
params.set("token", request.token);
params.set("client_id", client.client_id);
@ -73,7 +73,7 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
if (request.token_type_hint) {
params.set("token_type_hint", request.token_type_hint);
}
const response = await fetch(revocationUrl, {
method: "POST",
headers: {
@ -81,7 +81,7 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
},
body: params.toString(),
});
if (!response.ok) {
throw new ServerError(`Token revocation failed: ${response.status}`);
}
@ -115,18 +115,12 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
}
async authorize(
client: OAuthClientInformationFull,
params: AuthorizationParams,
client: OAuthClientInformationFull,
params: AuthorizationParams,
res: Response
): Promise<void> {
const authorizationUrl = this._endpoints.authorizationUrl;
if (!authorizationUrl) {
throw new Error("No authorization endpoint configured");
}
// Start with required OAuth parameters
const targetUrl = new URL(authorizationUrl);
const targetUrl = new URL(this._endpoints.authorizationUrl);
const searchParams = new URLSearchParams({
client_id: client.client_id,
response_type: "code",
@ -144,7 +138,7 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
}
async challengeForAuthorizationCode(
_client: OAuthClientInformationFull,
_client: OAuthClientInformationFull,
_authorizationCode: string
): Promise<string> {
// In a proxy setup, we don't store the code challenge ourselves
@ -153,16 +147,10 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
}
async exchangeAuthorizationCode(
client: OAuthClientInformationFull,
client: OAuthClientInformationFull,
authorizationCode: string,
codeVerifier?: string
): Promise<OAuthTokens> {
const tokenUrl = this._endpoints.tokenUrl;
if (!tokenUrl) {
throw new Error("No token endpoint configured");
}
const params = new URLSearchParams({
grant_type: "authorization_code",
client_id: client.client_id,
@ -177,7 +165,7 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
params.append("code_verifier", codeVerifier);
}
const response = await fetch(tokenUrl, {
const response = await fetch(this._endpoints.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
@ -195,15 +183,10 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
}
async exchangeRefreshToken(
client: OAuthClientInformationFull,
client: OAuthClientInformationFull,
refreshToken: string,
scopes?: string[]
): Promise<OAuthTokens> {
const tokenUrl = this._endpoints.tokenUrl;
if (!tokenUrl) {
throw new Error("No token endpoint configured");
}
const params = new URLSearchParams({
grant_type: "refresh_token",
@ -219,7 +202,7 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
params.set("scope", scopes.join(" "));
}
const response = await fetch(tokenUrl, {
const response = await fetch(this._endpoints.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
@ -237,5 +220,5 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
async verifyAccessToken(token: string): Promise<AuthInfo> {
return this._verifyAccessToken(token);
}
}
}