From 6fc7847e2f422860a5798854c1b5714224d1d077 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Wed, 2 Jul 2025 19:47:44 +0200 Subject: [PATCH] chore: another small cleanup Signed-off-by: benjdlambert --- ...OidcService.test.ts => OidcRouter.test.ts} | 9 ++- .../auth-backend/src/service/OidcRouter.ts | 73 +++++++++++++++++++ .../auth-backend/src/service/OidcService.ts | 71 ++++++------------ plugins/auth-backend/src/service/router.ts | 17 +++-- 4 files changed, 108 insertions(+), 62 deletions(-) rename plugins/auth-backend/src/service/{OidcService.test.ts => OidcRouter.test.ts} (96%) create mode 100644 plugins/auth-backend/src/service/OidcRouter.ts diff --git a/plugins/auth-backend/src/service/OidcService.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts similarity index 96% rename from plugins/auth-backend/src/service/OidcService.test.ts rename to plugins/auth-backend/src/service/OidcRouter.test.ts index ea6248e400..4edfd99464 100644 --- a/plugins/auth-backend/src/service/OidcService.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -21,10 +21,11 @@ import { import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import Router from 'express-promise-router'; import request from 'supertest'; -import { OidcService } from './OidcService'; +import { OidcRouter } from './OidcRouter'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; +import { OidcService } from './OidcService'; -describe('OidcService', () => { +describe('OidcRouter', () => { describe('/v1/userinfo', () => { it('should return user info for full tokens', async () => { const auth = mockServices.auth.mock(); @@ -48,7 +49,7 @@ describe('OidcService', () => { const router = Router(); router.use( - OidcService.create({ + OidcRouter.create({ auth, tokenIssuer: {} as any, baseUrl: 'http://localhost:7000', @@ -110,7 +111,7 @@ describe('OidcService', () => { const router = Router(); router.use( - OidcService.create({ + OidcRouter.create({ auth, tokenIssuer: {} as any, baseUrl: 'http://localhost:7000', diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts new file mode 100644 index 0000000000..6ada071c44 --- /dev/null +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Router from 'express-promise-router'; +import { OidcService } from './OidcService'; +import { AuthenticationError } from '@backstage/errors'; +import { AuthService } from '@backstage/backend-plugin-api'; +import { TokenIssuer } from '../identity/types'; +import { UserInfoDatabase } from '../database/UserInfoDatabase'; + +export class OidcRouter { + private constructor(private readonly oidc: OidcService) {} + + static create(options: { + auth: AuthService; + tokenIssuer: TokenIssuer; + baseUrl: string; + userInfo: UserInfoDatabase; + }) { + return new OidcRouter(OidcService.create(options)); + } + + public getRouter() { + const router = Router(); + + router.get('/.well-known/openid-configuration', (_req, res) => { + res.json(this.oidc.getConfiguration()); + }); + + router.get('/.well-known/jwks.json', async (_req, res) => { + const { keys } = await this.oidc.listPublicKeys(); + res.json({ keys }); + }); + + router.get('/v1/token', (_req, res) => { + res.status(501).send('Not Implemented'); + }); + + // This endpoint doesn't use the regular HttpAuthoidc, since the contract + // is specifically for the header to be communicated in the Authorization + // header, regardless of token type + router.get('/v1/userinfo', async (req, res) => { + const matches = req.headers.authorization?.match(/^Bearer[ ]+(\S+)$/i); + const token = matches?.[1]; + if (!token) { + throw new AuthenticationError('No token provided'); + } + + const userInfo = await this.oidc.getUserInfo({ token }); + + if (!userInfo) { + res.status(404).send('User info not found'); + return; + } + + res.json(userInfo); + }); + + return router; + } +} diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index 7af4a1e8ed..5024b2cc8c 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -16,8 +16,7 @@ import { AuthService } from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; -import Router from 'express-promise-router'; -import { AuthenticationError, InputError } from '@backstage/errors'; +import { InputError } from '@backstage/errors'; import { decodeJwt } from 'jose'; export class OidcService { @@ -42,10 +41,8 @@ export class OidcService { ); } - public getRouter() { - const router = Router(); - - const config = { + public getConfiguration() { + return { issuer: this.baseUrl, token_endpoint: `${this.baseUrl}/v1/token`, userinfo_endpoint: `${this.baseUrl}/v1/userinfo`, @@ -69,53 +66,27 @@ export class OidcService { claims_supported: ['sub', 'ent'], grant_types_supported: [], }; + } - router.get('/.well-known/openid-configuration', (_req, res) => { - res.json(config); + public async listPublicKeys() { + return await this.tokenIssuer.listPublicKeys(); + } + + public async getUserInfo({ token }: { token: string }) { + const credentials = await this.auth.authenticate(token, { + allowLimitedAccess: true, }); + if (!this.auth.isPrincipal(credentials, 'user')) { + throw new InputError( + 'Userinfo endpoint must be called with a token that represents a user principal', + ); + } - router.get('/.well-known/jwks.json', async (_req, res) => { - const { keys } = await this.tokenIssuer.listPublicKeys(); - res.json({ keys }); - }); + const { sub: userEntityRef } = decodeJwt(token); - router.get('/v1/token', (_req, res) => { - res.status(501).send('Not Implemented'); - }); - - // This endpoint doesn't use the regular HttpAuthService, since the contract - // is specifically for the header to be communicated in the Authorization - // header, regardless of token type - router.get('/v1/userinfo', async (req, res) => { - const matches = req.headers.authorization?.match(/^Bearer[ ]+(\S+)$/i); - const token = matches?.[1]; - if (!token) { - throw new AuthenticationError('No token provided'); - } - - const credentials = await this.auth.authenticate(token, { - allowLimitedAccess: true, - }); - if (!this.auth.isPrincipal(credentials, 'user')) { - throw new InputError( - 'Userinfo endpoint must be called with a token that represents a user principal', - ); - } - - const { sub: userEntityRef } = decodeJwt(token); - - if (typeof userEntityRef !== 'string') { - throw new Error('Invalid user token, user entity ref must be a string'); - } - const userInfo = await this.userInfo.getUserInfo(userEntityRef); - if (!userInfo) { - res.status(404).send('User info not found'); - return; - } - - res.json(userInfo); - }); - - return router; + if (typeof userEntityRef !== 'string') { + throw new Error('Invalid user token, user entity ref must be a string'); + } + return await this.userInfo.getUserInfo(userEntityRef); } } diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index ce7d563817..2723af4775 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -40,6 +40,7 @@ import { StaticTokenIssuer } from '../identity/StaticTokenIssuer'; import { StaticKeyStore } from '../identity/StaticKeyStore'; import { bindProviderRouters, ProviderFactories } from '../providers/router'; import { OidcService } from './OidcService'; +import { OidcRouter } from './OidcRouter'; interface RouterOptions { logger: LoggerService; @@ -148,14 +149,14 @@ export async function createRouter( auth: options.auth, }); - router.use( - OidcService.create({ - auth: options.auth, - tokenIssuer, - baseUrl: authUrl, - userInfo, - }).getRouter(), - ); + const oidcRouter = OidcRouter.create({ + auth: options.auth, + tokenIssuer, + baseUrl: authUrl, + userInfo, + }); + + router.use(oidcRouter.getRouter()); // Gives a more helpful error message than a plain 404 router.use('/:provider/', req => {