Merge pull request #24676 from backstage/rugvip/legacy-auth

backend-common: fix plugin auth for mixed usage of old and new system
This commit is contained in:
Patrik Oldsberg
2024-05-08 11:02:04 +02:00
committed by GitHub
3 changed files with 159 additions and 2 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Plugins created through the `legacyPlugin` helper are now able to authenticate requests from plugins that are fully implemented using the new backend system. This fixes the `Key for the ES256 algorithm must be one of type KeyObject or CryptoKey. Received an instance of Uint8Array` error.
+118
View File
@@ -0,0 +1,118 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
coreServices,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
import { EventEmitter } from 'events';
import { Router } from 'express';
import { createLegacyAuthAdapters } from './auth';
import { legacyPlugin } from './legacy';
import {
authServiceFactory,
tokenManagerServiceFactory,
} from '@backstage/backend-app-api';
describe('legacyPlugin', () => {
it('can auth across the new and old systems', async () => {
const emitter = new EventEmitter();
const done = new Promise(resolve => {
emitter.once('done', () => {
emitter.once('done', resolve);
});
});
await startTestBackend({
features: [
authServiceFactory,
tokenManagerServiceFactory,
mockServices.rootConfig.factory({
data: {
backend: {
auth: {
keys: [
{
secret: 'test',
},
],
},
},
},
}),
createBackendPlugin({
pluginId: 'new',
register(reg) {
reg.registerInit({
deps: {
auth: coreServices.auth,
discovery: coreServices.discovery,
},
async init({ auth }) {
emitter.once('legacy-token', async otherToken => {
const credentials = await auth.authenticate(otherToken);
expect(credentials.principal).toEqual({
type: 'service',
subject: 'external:backstage-plugin',
});
emitter.emit('done');
});
const { token } = await auth.getPluginRequestToken({
onBehalfOf: await auth.getOwnServiceCredentials(),
targetPluginId: 'old',
});
emitter.emit('new-token', token);
},
});
},
}),
legacyPlugin(
'old',
Promise.resolve({
async default({ tokenManager, identity, discovery }) {
const { auth } = createLegacyAuthAdapters({
tokenManager,
identity,
discovery,
auth: undefined as any as typeof coreServices.auth.T,
httpAuth: undefined as any as typeof coreServices.httpAuth.T,
});
emitter.once('new-token', async otherToken => {
const credentials = await auth.authenticate(otherToken);
expect(credentials.principal).toEqual({
type: 'service',
subject: 'external:backstage-plugin',
});
emitter.emit('done');
});
const { token } = await tokenManager.getToken();
emitter.emit('legacy-token', token);
return Router();
},
}),
),
],
});
await done;
});
});
+36 -2
View File
@@ -15,6 +15,7 @@
*/
import {
AuthService,
coreServices,
createBackendPlugin,
ServiceRef,
@@ -22,6 +23,7 @@ import {
import { RequestHandler } from 'express';
import { cacheToPluginCacheManager } from './cache';
import { loggerToWinstonLogger } from './logging';
import { TokenManager } from './tokens';
/**
* @public
@@ -38,6 +40,31 @@ type TransformedEnv<
: TEnv[key];
};
// Since the plugin will be using the new system our callers will expect us to support the
// new plugin tokens, which we'll also be signaling by supporting the JWKS endpoint through
// the http router.
// This makes sure that we accept the new plugin tokens as valid tokens, but otherwise fall
// back to whatever the token manager is doing.
function wrapTokenManager(tokenManager: TokenManager, auth: AuthService) {
return {
async getToken() {
return tokenManager.getToken();
},
async authenticate(token) {
if (token) {
// Unless it's a valid service token, we'll let the token manager do
// validation. We'll throw if we for example receive an invalid user
// token here, but that's what the token manager does too.
const credentials = await auth.authenticate(token);
if (auth.isPrincipal(credentials, 'service')) {
return;
}
}
await tokenManager.authenticate(token);
},
} satisfies TokenManager;
}
/**
* Creates a new custom plugin compatibility wrapper.
*
@@ -64,8 +91,12 @@ export function makeLegacyPlugin<
pluginId: name,
register(env) {
env.registerInit({
deps: { ...envMapping, _router: coreServices.httpRouter },
async init({ _router, ...envDeps }) {
deps: {
...envMapping,
_router: coreServices.httpRouter,
_auth: coreServices.auth,
},
async init({ _router, _auth, ...envDeps }) {
const { default: createRouter } = await createRouterImport;
const pluginEnv = Object.fromEntries(
Object.entries(envDeps).map(([key, dep]) => {
@@ -73,6 +104,9 @@ export function makeLegacyPlugin<
if (transform) {
return [key, transform(dep)];
}
if (key === 'tokenManager') {
return [key, wrapTokenManager(dep as TokenManager, _auth)];
}
return [key, dep];
}),
);