From 39de4a792445a1d176e3d6957576852f7fdfea75 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 4 Apr 2024 14:34:30 +0200 Subject: [PATCH] backend-app-api: detect legacy auth Co-authored-by: Patrik Oldsberg Signed-off-by: Vincenzo Scamporlino --- .../auth/PluginTokenHandler.ts | 56 ++++++++++++++++++- .../auth/authServiceFactory.ts | 16 ++++-- 2 files changed, 66 insertions(+), 6 deletions(-) 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 15ffcac980..2872e7f1f9 100644 --- a/packages/backend-app-api/src/services/implementations/auth/PluginTokenHandler.ts +++ b/packages/backend-app-api/src/services/implementations/auth/PluginTokenHandler.ts @@ -55,6 +55,10 @@ export class PluginTokenHandler { private keyExpiry?: Date; private jwksMap = new Map(); + // Tracking state for isTargetPluginSupported + private supportedTargetPlugins = new Set(); + private targetPluginInflightChecks = new Map>(); + static create(options: Options) { return new PluginTokenHandler( options.logger, @@ -140,13 +144,63 @@ export class PluginTokenHandler { return { token }; } + async isTargetPluginSupported(targetPluginId: string): Promise { + if (this.supportedTargetPlugins.has(targetPluginId)) { + return true; + } + const inFlight = this.targetPluginInflightChecks.get(targetPluginId); + if (inFlight) { + return inFlight; + } + + const doCheck = async () => { + try { + const res = await fetch( + `${await this.discovery.getBaseUrl( + targetPluginId, + )}/.backstage/auth/v1/jwks.json`, + ); + if (res.status === 404) { + return false; + } + + if (!res.ok) { + throw new Error(`Failed to fetch jwks.json, ${res.status}`); + } + + const data = await res.json(); + if (!data.keys) { + throw new Error(`Invalid jwks.json response, missing keys`); + } + + this.supportedTargetPlugins.add(targetPluginId); + return true; + } catch (error) { + this.logger.error('Unexpected failure for target JWKS check', error); + return false; + } finally { + this.targetPluginInflightChecks.delete(targetPluginId); + } + }; + + const check = doCheck(); + this.targetPluginInflightChecks.set(targetPluginId, check); + return check; + } + private async getJwksClient(pluginId: string) { - // TODO(Rugvip): Make this more resilient to forged tokens, making sure we don't fill up the map const client = this.jwksMap.get(pluginId); if (client) { return client; } + // Double check that the target plugin has a valid JWKS endpoint, otherwise avoid creating a remote key set + if (!(await this.isTargetPluginSupported(pluginId))) { + throw new AuthenticationError( + 'Target plugin does not support self-signed tokens', + ); + } + const newClient = new JwksClient(async () => { return new URL( `${await this.discovery.getBaseUrl( 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 f9b3181d61..f6dd2902b9 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts @@ -181,6 +181,7 @@ class DefaultAuthService implements AuthService { onBehalfOf: BackstageCredentials; targetPluginId: string; }): Promise<{ token: string }> { + const { targetPluginId } = options; const internalForward = toInternalBackstageCredentials(options.onBehalfOf); const { type } = internalForward.principal; @@ -198,11 +199,16 @@ class DefaultAuthService implements AuthService { switch (type) { // TODO: Check whether the principal is ourselves case 'service': - return this.pluginTokenHandler.issueToken({ - pluginId: this.pluginId, - targetPluginId: options.targetPluginId, - }); - // return this.tokenManager.getToken(); + if ( + await this.pluginTokenHandler.isTargetPluginSupported(targetPluginId) + ) { + return this.pluginTokenHandler.issueToken({ + pluginId: this.pluginId, + targetPluginId, + }); + } + // If the target plugin does not support the new auth service, fall back to using old token format + return this.tokenManager.getToken(); case 'user': if (!internalForward.token) { throw new Error('User credentials is unexpectedly missing token');