feat(auth-backend): add experimental CIMD support (#32307)
Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
@@ -49,6 +49,8 @@
|
||||
"@backstage/backend-defaults": "workspace:^",
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/express": "^4.17.6"
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"supertest": "^7.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
import request from 'supertest';
|
||||
|
||||
describe('Mcp Backend', () => {
|
||||
const mockPluginWithActions = createBackendPlugin({
|
||||
@@ -162,4 +163,91 @@ describe('Mcp Backend', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('OAuth well-known endpoints', () => {
|
||||
it('should not expose oauth endpoints when neither DCR nor CIMD is enabled', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
mcpPlugin,
|
||||
mockPluginWithActions,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
actions: {
|
||||
pluginSources: ['local'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const response = await request(server).get(
|
||||
'/.well-known/oauth-protected-resource',
|
||||
);
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should expose oauth-protected-resource when DCR is enabled', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
mcpPlugin,
|
||||
mockPluginWithActions,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
actions: {
|
||||
pluginSources: ['local'],
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
experimentalDynamicClientRegistration: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const response = await request(server).get(
|
||||
'/.well-known/oauth-protected-resource',
|
||||
);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.resource).toMatch(/\/api\/mcp-actions$/);
|
||||
expect(response.body.authorization_servers).toHaveLength(1);
|
||||
expect(response.body.authorization_servers[0]).toMatch(/\/api\/auth$/);
|
||||
});
|
||||
|
||||
it('should expose oauth-protected-resource when CIMD is enabled', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
mcpPlugin,
|
||||
mockPluginWithActions,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
actions: {
|
||||
pluginSources: ['local'],
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
experimentalClientIdMetadataDocuments: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const response = await request(server).get(
|
||||
'/.well-known/oauth-protected-resource',
|
||||
);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.resource).toMatch(/\/api\/mcp-actions$/);
|
||||
expect(response.body.authorization_servers).toHaveLength(1);
|
||||
expect(response.body.authorization_servers[0]).toMatch(/\/api\/auth$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,13 +79,18 @@ export const mcpPlugin = createBackendPlugin({
|
||||
|
||||
httpRouter.use(router);
|
||||
|
||||
if (
|
||||
const oauthEnabled =
|
||||
config.getOptionalBoolean(
|
||||
'auth.experimentalDynamicClientRegistration.enabled',
|
||||
)
|
||||
) {
|
||||
) ||
|
||||
config.getOptionalBoolean(
|
||||
'auth.experimentalClientIdMetadataDocuments.enabled',
|
||||
);
|
||||
|
||||
if (oauthEnabled) {
|
||||
// OAuth Authorization Server Metadata (RFC 8414)
|
||||
// This should be replaced with throwing a WWW-Authenticate header, but that doesn't seem to be supported by
|
||||
// many of the MCP client as of yet. So this seems to be the oldest version of the spec thats implemented.
|
||||
// many of the MCP clients as of yet. So this seems to be the oldest version of the spec that's implemented.
|
||||
rootRouter.use(
|
||||
'/.well-known/oauth-authorization-server',
|
||||
async (_, res) => {
|
||||
@@ -93,10 +98,26 @@ export const mcpPlugin = createBackendPlugin({
|
||||
const oidcResponse = await fetch(
|
||||
`${authBaseUrl}/.well-known/openid-configuration`,
|
||||
);
|
||||
|
||||
res.json(await oidcResponse.json());
|
||||
},
|
||||
);
|
||||
|
||||
// Protected Resource Metadata (RFC 9728)
|
||||
// https://datatracker.ietf.org/doc/html/rfc9728
|
||||
// This allows MCP clients to discover the authorization server for this resource
|
||||
rootRouter.use(
|
||||
'/.well-known/oauth-protected-resource',
|
||||
async (_, res) => {
|
||||
const [authBaseUrl, mcpBaseUrl] = await Promise.all([
|
||||
discovery.getBaseUrl('auth'),
|
||||
discovery.getBaseUrl('mcp-actions'),
|
||||
]);
|
||||
res.json({
|
||||
resource: mcpBaseUrl,
|
||||
authorization_servers: [authBaseUrl],
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user