diff --git a/packages/backend-app-api/src/services/implementations/auth/PluginTokenHandler.ts b/packages/backend-app-api/src/services/implementations/auth/PluginTokenHandler.ts index 5a20368dfd..1436412c3e 100644 --- a/packages/backend-app-api/src/services/implementations/auth/PluginTokenHandler.ts +++ b/packages/backend-app-api/src/services/implementations/auth/PluginTokenHandler.ts @@ -22,19 +22,22 @@ import { JWK, importJWK, SignJWT, + decodeProtectedHeader, } from 'jose'; import { DateTime } from 'luxon'; import { v4 as uuid } from 'uuid'; import { InternalKey, KeyStore } from './types'; import { AuthenticationError } from '@backstage/errors'; import { createRemoteJWKSet, jwtVerify } from 'jose'; +import { tokenTypes } from '@backstage/plugin-auth-node'; + +const ALLOWED_PLUGIN_ID_PATTERN = /^[a-z0-9_-]+$/i; type Options = { + ownPluginId: string; publicKeyStore: KeyStore; discovery: DiscoveryService; logger: LoggerService; - /** Value of the issuer claim in issued tokens */ - issuer: string; /** Expiration time of signing keys in seconds */ keyDurationSeconds: number; /** JWS "alg" (Algorithm) Header Parameter value. Defaults to ES256. @@ -54,6 +57,7 @@ export class PluginTokenHandler { static create(options: Options) { return new PluginTokenHandler( options.logger, + options.ownPluginId, options.publicKeyStore, options.keyDurationSeconds, options.algorithm ?? 'ES256', @@ -63,36 +67,43 @@ export class PluginTokenHandler { private constructor( readonly logger: LoggerService, + readonly ownPluginId: string, readonly publicKeyStore: KeyStore, readonly keyDurationSeconds: number, readonly algorithm: string, readonly discovery: DiscoveryService, ) {} - async verifyToken(token: string): Promise<{ subject: string }> { - const claims = decodeJwt(token); - const pluginId = claims.sub; + async verifyToken(token: string): Promise<{ subject: string } | undefined> { + try { + const { typ } = decodeProtectedHeader(token); + if (typ !== tokenTypes.plugin.typParam) { + return undefined; + } + } catch { + return undefined; + } + + const pluginId = String(decodeJwt(token).sub); if (!pluginId) { - throw new AuthenticationError('Invalid subject'); + throw new AuthenticationError('Invalid plugin token: missing subject'); + } + if (!ALLOWED_PLUGIN_ID_PATTERN.test(pluginId)) { + throw new AuthenticationError( + 'Invalid plugin token: forbidden subject format', + ); } const JWKS = await this.getJWKS(pluginId); - try { - const { payload, protectedHeader } = await jwtVerify(token, JWKS, { - issuer: 'backstage-plugin', - // TODO(vinzscam): add audience verification - }); + const { payload } = await jwtVerify<{ sub: string }>(token, JWKS, { + typ: tokenTypes.plugin.typParam, + audience: this.ownPluginId, + requiredClaims: ['iat', 'exp', 'sub', 'aud'], + }).catch(e => { + throw new AuthenticationError('Invalid plugin token', e); + }); - if (!payload.sub) { - throw new AuthenticationError('Missing subject'); - } - console.log('payload', payload); - console.log('protected header', protectedHeader); - return { subject: payload.sub }; - } catch (e) { - // TODO(vinzscam): here we need to handle errors properly - throw e; - } + return { subject: payload.sub }; } async issueToken(options: { @@ -101,18 +112,18 @@ export class PluginTokenHandler { }): Promise<{ token: string }> { const key = await this.getKey(); - const iss = 'backstage-plugin'; const sub = options.pluginId; const aud = options.targetPluginId; const iat = Math.floor(Date.now() / 1000); const exp = iat + this.keyDurationSeconds; - this.logger.info(`Issuing token for ${sub} with audentice ${aud}`); - - const claims = { iss, sub, aud, iat, exp }; + const claims = { sub, aud, iat, exp }; const token = await new SignJWT(claims) - .setProtectedHeader({ alg: this.algorithm, kid: key.kid }) - .setIssuer(iss) + .setProtectedHeader({ + typ: tokenTypes.plugin.typParam, + alg: this.algorithm, + kid: key.kid, + }) .setAudience(aud) .setSubject(sub) .setIssuedAt(iat) diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts index 3b7a7322cb..f9b3181d61 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts @@ -122,23 +122,9 @@ class DefaultAuthService implements AuthService { // allowLimitedAccess is currently ignored, since we currently always use the full user tokens async authenticate(token: string): Promise { - const { sub, aud, iss } = decodeJwt(token); - console.log(`DEBUG: iss=`, iss); - - if (iss === 'backstage-plugin') { - console.log('DO THE STUFF!'); - const { subject } = await this.pluginTokenHandler.verifyToken(token); - return createCredentialsWithServicePrincipal(subject); - } - - // # identify new token - // 1. generate and store public keys in database - // 2. verification of token, by fetching all public keys - - // Legacy service-to-service token - if (sub === 'backstage-server' && !aud) { - await this.tokenManager.authenticate(token); - return createCredentialsWithServicePrincipal('external:backstage-plugin'); + const pluginResult = await this.pluginTokenHandler.verifyToken(token); + if (pluginResult) { + return createCredentialsWithServicePrincipal(pluginResult.subject); } const userResult = await this.userTokenHandler.verifyToken(token); @@ -150,6 +136,13 @@ class DefaultAuthService implements AuthService { ); } + // Legacy service-to-service token + const { sub, aud } = decodeJwt(token); + if (sub === 'backstage-server' && !aud) { + await this.tokenManager.authenticate(token); + return createCredentialsWithServicePrincipal('external:backstage-plugin'); + } + throw new AuthenticationError('Unknown token'); } @@ -277,8 +270,8 @@ export const authServiceFactory = createServiceFactory({ disableDefaultAuthPolicy, publicKeyStore, PluginTokenHandler.create({ + ownPluginId: plugin.getId(), keyDurationSeconds: 60 * 60, - issuer: `plugin:${plugin.getId()}`, logger, publicKeyStore, discovery, diff --git a/packages/backend-app-api/src/services/implementations/auth/index.ts b/packages/backend-app-api/src/services/implementations/auth/index.ts index 4bcf535e5b..1b55d46a83 100644 --- a/packages/backend-app-api/src/services/implementations/auth/index.ts +++ b/packages/backend-app-api/src/services/implementations/auth/index.ts @@ -15,4 +15,3 @@ */ export { authServiceFactory } from './authServiceFactory'; -export { createAuthIntegrationRouter } from './createAuthIntegrationRouter'; diff --git a/packages/backend-app-api/src/services/implementations/auth/createAuthIntegrationRouter.ts b/packages/backend-app-api/src/services/implementations/httpRouter/createAuthIntegrationRouter.ts similarity index 99% rename from packages/backend-app-api/src/services/implementations/auth/createAuthIntegrationRouter.ts rename to packages/backend-app-api/src/services/implementations/httpRouter/createAuthIntegrationRouter.ts index 0fd417b611..94650199f3 100644 --- a/packages/backend-app-api/src/services/implementations/auth/createAuthIntegrationRouter.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/createAuthIntegrationRouter.ts @@ -24,6 +24,7 @@ export function createAuthIntegrationRouter(options: { router.get('/.backstage/auth/v1/jwks.json', async (_req, res) => { const { keys } = await options.auth.listPublicServiceKeys(); + res.json({ keys }); }); diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts index 30878e3e22..5b881dca4c 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts @@ -23,7 +23,7 @@ import { Handler } from 'express'; import PromiseRouter from 'express-promise-router'; import { createLifecycleMiddleware } from './createLifecycleMiddleware'; import { createCredentialsBarrier } from './createCredentialsBarrier'; -import { createAuthIntegrationRouter } from '../auth'; +import { createAuthIntegrationRouter } from './createAuthIntegrationRouter'; /** * @public diff --git a/plugins/auth-node/src/types.ts b/plugins/auth-node/src/types.ts index 8cc48aacc4..39172437ec 100644 --- a/plugins/auth-node/src/types.ts +++ b/plugins/auth-node/src/types.ts @@ -389,7 +389,7 @@ export const tokenTypes = Object.freeze({ limitedUser: Object.freeze({ typParam: 'vnd.backstage.limited-user', }), - service: Object.freeze({ - typParam: 'vnd.backstage.service', + plugin: Object.freeze({ + typParam: 'vnd.backstage.plugin', }), });