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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user