@backstage/backend-defaults
Version:
Backend defaults used by Backstage backend apps
156 lines (152 loc) • 5.32 kB
JavaScript
;
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