From 0d142d95ec0e770ca1d255d14ccfe7cfb8770e75 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 3 Jul 2025 11:16:51 +0200 Subject: [PATCH] chore: implementing the register and code exchange Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- .../auth-backend/src/database/OidcDatabase.ts | 18 +++++-- .../auth-backend/src/service/OidcRouter.ts | 49 +++++++++++++++---- plugins/auth-backend/src/service/router.ts | 1 + plugins/mcp-actions-backend/src/plugin.ts | 27 +++++++++- 4 files changed, 80 insertions(+), 15 deletions(-) diff --git a/plugins/auth-backend/src/database/OidcDatabase.ts b/plugins/auth-backend/src/database/OidcDatabase.ts index cbaf101ed9..2eeacb7ffe 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.ts @@ -93,11 +93,8 @@ type AccessToken = { }; /** - * This class is an implementation for the Database operations that power the OIDC sign-in flow. - * * This class provides database operations for OpenID Connect (OIDC) authentication flows. - * It manages OIDC clients, authorization codes, and access tokens in the database, as well as the consent requests - * for the frontend plugin to accept. + * It manages OIDC clients, authorization codes, and access tokens in the database. */ export class OidcDatabase { private constructor(private readonly db: Knex) {} @@ -109,7 +106,18 @@ export class OidcDatabase { async createClient(client: Omit) { const now = DateTime.now().toString(); - + console.log({ + client_id: client.clientId, + client_secret: client.clientSecret, + client_name: client.clientName, + created_at: now, + expires_at: client.expiresAt, + response_types: JSON.stringify(client.responseTypes), + grant_types: JSON.stringify(client.grantTypes), + redirect_uris: JSON.stringify(client.redirectUris), + scope: client.scope, + metadata: JSON.stringify(client.metadata), + }); await this.db('oidc_clients').insert({ client_id: client.clientId, client_secret: client.clientSecret, diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index 721dedf8c3..9706554ba5 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -16,27 +16,34 @@ import Router from 'express-promise-router'; import { OidcService } from './OidcService'; import { AuthenticationError, isError } from '@backstage/errors'; -import { AuthService } from '@backstage/backend-plugin-api'; +import { AuthService, LoggerService } from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; import { OidcDatabase } from '../database/OidcDatabase'; +import { json } from 'express'; export class OidcRouter { - private constructor(private readonly oidc: OidcService) {} + private constructor( + private readonly oidc: OidcService, + private readonly logger: LoggerService, + ) {} static create(options: { auth: AuthService; tokenIssuer: TokenIssuer; baseUrl: string; + logger: LoggerService; userInfo: UserInfoDatabase; oidc: OidcDatabase; }) { - return new OidcRouter(OidcService.create(options)); + return new OidcRouter(OidcService.create(options), options.logger); } public getRouter() { const router = Router(); + router.use(json()); + router.get('/.well-known/openid-configuration', (_req, res) => { res.json(this.oidc.getConfiguration()); }); @@ -60,6 +67,7 @@ export class OidcRouter { } = req.query; if (!clientId || !redirectUri || !responseType) { + this.logger.error(`Failed to authorize: Missing required parameters`); return res.status(400).json({ error: 'invalid_request', error_description: @@ -68,8 +76,8 @@ export class OidcRouter { } try { - // For simplicity, we'll use a default user entity ref for now - // In a real implementation, this should be obtained from the authenticated user + // use default user entity ref for now, as we need a redirect to the frontend plugin + // for the consent flow in order to issue the right token for the right user. const userEntityRef = 'user:default/guest'; const { redirectUrl } = await this.oidc.authorize({ @@ -117,6 +125,9 @@ export class OidcRouter { } = req.body; if (!grantType || !code || !clientId || !clientSecret || !redirectUri) { + this.logger.error( + `Failed to exchange code for token: Missing required parameters`, + ); return res.status(400).json({ error: 'invalid_request', error_description: 'Missing required parameters', @@ -135,6 +146,12 @@ export class OidcRouter { return res.json(result); } catch (error) { + const description = isError(error) ? error.message : 'Unknown error'; + this.logger.error( + `Failed to exchange code for token: ${description}`, + error, + ); + if (isError(error)) { if (error.name === 'AuthenticationError') { return res.status(401).json({ @@ -180,6 +197,7 @@ export class OidcRouter { router.post('/v1/register', async (req, res) => { // todo(blam): maybe add zod types for validating input const registrationRequest = req.body; + if (!registrationRequest.redirect_uris?.length) { res.status(400).json({ error: 'invalid_request', @@ -189,13 +207,26 @@ export class OidcRouter { } try { - res.json(await this.oidc.registerClient(registrationRequest)); + const client = await this.oidc.registerClient({ + clientName: registrationRequest.client_name, + redirectUris: registrationRequest.redirect_uris, + responseTypes: registrationRequest.response_types, + grantTypes: registrationRequest.grant_types, + scope: registrationRequest.scope, + }); + + res.status(201).json({ + client_id: client.clientId, + redirect_uris: client.redirectUris, + client_secret: client.clientSecret, + }); } catch (e) { + const description = isError(e) ? e.message : 'Unknown error'; + this.logger.error(`Failed to register client: ${description}`, e); + res.status(500).json({ error: 'server_error', - error_description: `Failed to register client: ${ - isError(e) ? e.message : 'Unknown error' - }`, + error_description: `Failed to register client: ${description}`, }); } }); diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index f81390daf8..5eb3450451 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -156,6 +156,7 @@ export async function createRouter( baseUrl: authUrl, userInfo, oidc, + logger, }); router.use(oidcRouter.getRouter()); diff --git a/plugins/mcp-actions-backend/src/plugin.ts b/plugins/mcp-actions-backend/src/plugin.ts index f1e89417e9..2e29847964 100644 --- a/plugins/mcp-actions-backend/src/plugin.ts +++ b/plugins/mcp-actions-backend/src/plugin.ts @@ -42,8 +42,17 @@ export const mcpPlugin = createBackendPlugin({ httpRouter: coreServices.httpRouter, actions: actionsServiceRef, registry: actionsRegistryServiceRef, + rootRouter: coreServices.rootHttpRouter, + discovery: coreServices.discovery, }, - async init({ actions, logger, httpRouter, httpAuth }) { + async init({ + actions, + logger, + httpRouter, + httpAuth, + rootRouter, + discovery, + }) { const mcpService = await McpService.create({ actions, }); @@ -66,6 +75,22 @@ export const mcpPlugin = createBackendPlugin({ router.use('/v1', streamableRouter); httpRouter.use(router); + + // todo(blam): there's probably a better way to proxy this, but it's required + // for mcp auth spec that it lives on the root of the mcp entrypoint server. + const authRouter = Router(); + authRouter.use('/', async (_, res) => { + const authBaseUrl = await discovery.getBaseUrl('auth'); + + const oidcResponse = await fetch( + `${authBaseUrl}/.well-known/openid-configuration`, + ); + + const oidcResponseJson = await oidcResponse.json(); + + res.json(oidcResponseJson); + }); + rootRouter.use('/.well-known/oauth-authorization-server', authRouter); }, }); },