feat(auth-backend): add experimental CIMD support (#32307)

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
Ben Lambert
2026-02-17 17:00:49 +01:00
committed by GitHub
parent 29fc1f8f23
commit 31de2c9b3a
17 changed files with 1967 additions and 157 deletions
+3 -1
View File
@@ -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$/);
});
});
});
+26 -5
View File
@@ -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],
});
},
);
}
},
});