UNPKG

@backstage/backend-defaults

Version:

Backend defaults used by Backstage backend apps

156 lines (152 loc) 5.32 kB
'use strict'; var jose = require('jose'); var errors = require('@backstage/errors'); var pluginAuthNode = require('@backstage/plugin-auth-node'); var JwksClient = require('../JwksClient.cjs.js'); var types = require('@backstage/types'); const SECONDS_IN_MS = 1e3; const ALLOWED_PLUGIN_ID_PATTERN = /^[a-z0-9_-]+$/i; class DefaultPluginTokenHandler { jwksMap = /* @__PURE__ */ new Map(); // Tracking state for isTargetPluginSupported supportedTargetPlugins = /* @__PURE__ */ new Set(); targetPluginInflightChecks = /* @__PURE__ */ new Map(); static create(options) { return new DefaultPluginTokenHandler( options.logger, options.ownPluginId, options.keySource, options.algorithm ?? "ES256", Math.round(types.durationToMilliseconds(options.keyDuration) / 1e3), options.discovery ); } logger; ownPluginId; keySource; algorithm; keyDurationSeconds; discovery; constructor(logger, ownPluginId, keySource, algorithm, keyDurationSeconds, discovery) { this.logger = logger; this.ownPluginId = ownPluginId; this.keySource = keySource; this.algorithm = algorithm; this.keyDurationSeconds = keyDurationSeconds; this.discovery = discovery; } async verifyToken(token) { try { const { typ } = jose.decodeProtectedHeader(token); if (typ !== pluginAuthNode.tokenTypes.plugin.typParam) { return void 0; } } catch { return void 0; } const pluginId = String(jose.decodeJwt(token).sub); if (!pluginId) { throw new errors.AuthenticationError("Invalid plugin token: missing subject"); } if (!ALLOWED_PLUGIN_ID_PATTERN.test(pluginId)) { throw new errors.AuthenticationError( "Invalid plugin token: forbidden subject format" ); } const jwksClient = await this.getJwksClient(pluginId); await jwksClient.refreshKeyStore(token); const { payload } = await jose.jwtVerify( token, jwksClient.getKey, { typ: pluginAuthNode.tokenTypes.plugin.typParam, audience: this.ownPluginId, requiredClaims: ["iat", "exp", "sub", "aud"] } ).catch((e) => { this.logger.warn("Failed to verify incoming plugin token", e); throw new errors.AuthenticationError("Failed plugin token verification"); }); return { subject: `plugin:${payload.sub}`, limitedUserToken: payload.obo }; } async issueToken(options) { const { pluginId, targetPluginId, onBehalfOf } = options; const key = await this.keySource.getPrivateSigningKey(); const sub = pluginId; const aud = targetPluginId; const iat = Math.floor(Date.now() / SECONDS_IN_MS); const ourExp = iat + this.keyDurationSeconds; const exp = onBehalfOf ? Math.min( ourExp, Math.floor(onBehalfOf.expiresAt.getTime() / SECONDS_IN_MS) ) : ourExp; const claims = { sub, aud, iat, exp, obo: onBehalfOf?.limitedUserToken }; const token = await new jose.SignJWT(claims).setProtectedHeader({ typ: pluginAuthNode.tokenTypes.plugin.typParam, alg: this.algorithm, kid: key.kid }).setAudience(aud).setSubject(sub).setIssuedAt(iat).setExpirationTime(exp).sign(await jose.importJWK(key)); return { token }; } async isTargetPluginSupported(targetPluginId) { 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) { errors.assertError(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; } async getJwksClient(pluginId) { const client = this.jwksMap.get(pluginId); if (client) { return client; } if (!await this.isTargetPluginSupported(pluginId)) { throw new errors.AuthenticationError( `Received a plugin token where the source '${pluginId}' plugin unexpectedly does not have a JWKS endpoint. The target plugin needs to be migrated to be installed in an app using the new backend system.` ); } const newClient = new JwksClient.JwksClient(async () => { return new URL( `${await this.discovery.getBaseUrl( pluginId )}/.backstage/auth/v1/jwks.json` ); }); this.jwksMap.set(pluginId, newClient); return newClient; } } exports.DefaultPluginTokenHandler = DefaultPluginTokenHandler; //# sourceMappingURL=PluginTokenHandler.cjs.js.map