Merge pull request #638 from modelcontextprotocol/ochafik/auth-resource
RFC 8707 Resource Indicators Implementation
This commit is contained in:
commit
90569d885f
2 changed files with 512 additions and 27 deletions
|
|
@ -1,10 +1,11 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import { AuthorizationParams, OAuthServerProvider } from '../../server/auth/provider.js';
|
||||
import { OAuthRegisteredClientsStore } from '../../server/auth/clients.js';
|
||||
import { OAuthClientInformationFull, OAuthMetadata, OAuthTokens } from 'src/shared/auth.js';
|
||||
import { OAuthClientInformationFull, OAuthMetadata, OAuthTokens } from '../../shared/auth.js';
|
||||
import express, { Request, Response } from "express";
|
||||
import { AuthInfo } from 'src/server/auth/types.js';
|
||||
import { createOAuthMetadata, mcpAuthRouter } from 'src/server/auth/router.js';
|
||||
import { AuthInfo } from '../../server/auth/types.js';
|
||||
import { createOAuthMetadata, mcpAuthRouter } from '../../server/auth/router.js';
|
||||
import { resourceUrlFromServerUrl } from '../../shared/auth-utils.js';
|
||||
|
||||
|
||||
export class DemoInMemoryClientsStore implements OAuthRegisteredClientsStore {
|
||||
|
|
@ -34,6 +35,17 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider {
|
|||
params: AuthorizationParams,
|
||||
client: OAuthClientInformationFull}>();
|
||||
private tokens = new Map<string, AuthInfo>();
|
||||
private validateResource?: (resource?: URL) => boolean;
|
||||
|
||||
constructor({mcpServerUrl}: {mcpServerUrl?: URL} = {}) {
|
||||
if (mcpServerUrl) {
|
||||
const expectedResource = resourceUrlFromServerUrl(mcpServerUrl);
|
||||
this.validateResource = (resource?: URL) => {
|
||||
if (!resource) return false;
|
||||
return resource.toString() === expectedResource.toString();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async authorize(
|
||||
client: OAuthClientInformationFull,
|
||||
|
|
@ -89,6 +101,10 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider {
|
|||
throw new Error(`Authorization code was not issued to this client, ${codeData.client.client_id} != ${client.client_id}`);
|
||||
}
|
||||
|
||||
if (this.validateResource && !this.validateResource(codeData.params.resource)) {
|
||||
throw new Error(`Invalid resource: ${codeData.params.resource}`);
|
||||
}
|
||||
|
||||
this.codes.delete(authorizationCode);
|
||||
const token = randomUUID();
|
||||
|
||||
|
|
@ -97,7 +113,8 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider {
|
|||
clientId: client.client_id,
|
||||
scopes: codeData.params.scopes || [],
|
||||
expiresAt: Date.now() + 3600000, // 1 hour
|
||||
type: 'access'
|
||||
resource: codeData.params.resource,
|
||||
type: 'access',
|
||||
};
|
||||
|
||||
this.tokens.set(token, tokenData);
|
||||
|
|
@ -113,7 +130,8 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider {
|
|||
async exchangeRefreshToken(
|
||||
_client: OAuthClientInformationFull,
|
||||
_refreshToken: string,
|
||||
_scopes?: string[]
|
||||
_scopes?: string[],
|
||||
_resource?: URL
|
||||
): Promise<OAuthTokens> {
|
||||
throw new Error('Not implemented for example demo');
|
||||
}
|
||||
|
|
@ -129,18 +147,19 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider {
|
|||
clientId: tokenData.clientId,
|
||||
scopes: tokenData.scopes,
|
||||
expiresAt: Math.floor(tokenData.expiresAt / 1000),
|
||||
resource: tokenData.resource,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const setupAuthServer = (authServerUrl: URL): OAuthMetadata => {
|
||||
export const setupAuthServer = (authServerUrl: URL, mcpServerUrl: URL): OAuthMetadata => {
|
||||
// Create separate auth server app
|
||||
// NOTE: This is a separate app on a separate port to illustrate
|
||||
// how to separate an OAuth Authorization Server from a Resource
|
||||
// server in the SDK. The SDK is not intended to be provide a standalone
|
||||
// authorization server.
|
||||
const provider = new DemoInMemoryAuthProvider();
|
||||
const provider = new DemoInMemoryAuthProvider({mcpServerUrl});
|
||||
const authApp = express();
|
||||
authApp.use(express.json());
|
||||
// For introspection requests
|
||||
|
|
@ -168,7 +187,8 @@ export const setupAuthServer = (authServerUrl: URL): OAuthMetadata => {
|
|||
active: true,
|
||||
client_id: tokenInfo.clientId,
|
||||
scope: tokenInfo.scopes.join(' '),
|
||||
exp: tokenInfo.expiresAt
|
||||
exp: tokenInfo.expiresAt,
|
||||
aud: tokenInfo.resource,
|
||||
});
|
||||
return
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { OAuthMetadata } from 'src/shared/auth.js';
|
|||
|
||||
// Check for OAuth flag
|
||||
const useOAuth = process.argv.includes('--oauth');
|
||||
const strictOAuth = process.argv.includes('--oauth-strict');
|
||||
|
||||
// Create an MCP server with implementation details
|
||||
const getServer = () => {
|
||||
|
|
@ -428,10 +429,10 @@ app.use(express.json());
|
|||
let authMiddleware = null;
|
||||
if (useOAuth) {
|
||||
// Create auth middleware for MCP endpoints
|
||||
const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}`);
|
||||
const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`);
|
||||
const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`);
|
||||
|
||||
const oauthMetadata: OAuthMetadata = setupAuthServer(authServerUrl);
|
||||
const oauthMetadata: OAuthMetadata = setupAuthServer(authServerUrl, mcpServerUrl);
|
||||
|
||||
const tokenVerifier = {
|
||||
verifyAccessToken: async (token: string) => {
|
||||
|
|
@ -457,6 +458,15 @@ if (useOAuth) {
|
|||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (strictOAuth) {
|
||||
if (!data.aud) {
|
||||
throw new Error(`Resource Indicator (RFC8707) missing`);
|
||||
}
|
||||
if (data.aud !== mcpServerUrl.href) {
|
||||
throw new Error(`Expected resource indicator ${mcpServerUrl}, got: ${data.aud}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the response to AuthInfo format
|
||||
return {
|
||||
|
|
|
|||
Loading…
Reference in a new issue