backend-app-api: detect legacy auth

Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
Vincenzo Scamporlino
2024-04-04 14:34:30 +02:00
parent ad8f173b78
commit 39de4a7924
2 changed files with 66 additions and 6 deletions
@@ -55,6 +55,10 @@ export class PluginTokenHandler {
private keyExpiry?: Date;
private jwksMap = new Map<string, JwksClient>();
// Tracking state for isTargetPluginSupported
private supportedTargetPlugins = new Set<string>();
private targetPluginInflightChecks = new Map<string, Promise<boolean>>();
static create(options: Options) {
return new PluginTokenHandler(
options.logger,
@@ -140,13 +144,63 @@ export class PluginTokenHandler {
return { token };
}
async isTargetPluginSupported(targetPluginId: string): Promise<boolean> {
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(
@@ -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');