chore: implementing the register and code exchange

Signed-off-by: benjdlambert <ben@blam.sh>

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
benjdlambert
2025-07-03 11:16:51 +02:00
parent 628322d19b
commit 0d142d95ec
4 changed files with 80 additions and 15 deletions
@@ -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<Client, 'createdAt'>) {
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<OidcClientRow>('oidc_clients').insert({
client_id: client.clientId,
client_secret: client.clientSecret,
+40 -9
View File
@@ -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}`,
});
}
});
@@ -156,6 +156,7 @@ export async function createRouter(
baseUrl: authUrl,
userInfo,
oidc,
logger,
});
router.use(oidcRouter.getRouter());
+26 -1
View File
@@ -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);
},
});
},