backend-defaults: remove support for legacy auth

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-08-21 13:17:23 +02:00
parent dae74b0d05
commit 359fcd72c1
5 changed files with 40 additions and 125 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-defaults': minor
---
**BREAKING**: The backwards compatibility with plugins using legacy auth through the token manage service has been removed. This means that instead of falling back to using the old token manager, requests towards plugins that don't support the new auth system will simply fail. Please make sure that all plugins in your deployment are hosted within a backend instance from the new backend system.
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { TokenManager } from '@backstage/backend-common';
import {
AuthService,
BackstageCredentials,
@@ -22,9 +21,8 @@ import {
BackstagePrincipalTypes,
BackstageServicePrincipal,
BackstageUserPrincipal,
LoggerService,
} from '@backstage/backend-plugin-api';
import { AuthenticationError, ForwardedError } from '@backstage/errors';
import { AuthenticationError } from '@backstage/errors';
import { JsonObject } from '@backstage/types';
import { decodeJwt } from 'jose';
import { ExternalTokenHandler } from './external/ExternalTokenHandler';
@@ -44,11 +42,9 @@ export class DefaultAuthService implements AuthService {
private readonly userTokenHandler: UserTokenHandler,
private readonly pluginTokenHandler: PluginTokenHandler,
private readonly externalTokenHandler: ExternalTokenHandler,
private readonly tokenManager: TokenManager,
private readonly pluginId: string,
private readonly disableDefaultAuthPolicy: boolean,
private readonly pluginKeySource: PluginKeySource,
private readonly logger: LoggerService,
) {}
async authenticate(
@@ -153,55 +149,28 @@ export class DefaultAuthService implements AuthService {
return { token: '' };
}
const targetSupportsNewAuth =
await this.pluginTokenHandler.isTargetPluginSupported(targetPluginId);
// check whether a plugin support the new auth system
// by checking the public keys endpoint existance.
switch (type) {
// TODO: Check whether the principal is ourselves
case 'service':
if (targetSupportsNewAuth) {
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
this.logger.warn(
`DEPRECATION WARNING: A call to the '${targetPluginId}' plugin had to fall back to using deprecated auth via the token manager service. Please migrate all plugins to the new auth service, see https://backstage.io/docs/tutorials/auth-service-migration for more information`,
);
return this.tokenManager.getToken().catch(error => {
throw new ForwardedError(
`Unable to generate legacy token for communication with the '${targetPluginId}' plugin. ` +
`You will typically encounter this error when attempting to call a plugin that does not exist, or is deployed with an old version of Backstage`,
error,
);
return this.pluginTokenHandler.issueToken({
pluginId: this.pluginId,
targetPluginId,
});
case 'user': {
const { token } = internalForward;
if (!token) {
throw new Error('User credentials is unexpectedly missing token');
}
// If the target plugin supports the new auth service we issue a service
// on-behalf-of token rather than forwarding the user token
if (targetSupportsNewAuth) {
const onBehalfOf = await this.userTokenHandler.createLimitedUserToken(
token,
);
return this.pluginTokenHandler.issueToken({
pluginId: this.pluginId,
targetPluginId,
onBehalfOf,
});
}
if (this.userTokenHandler.isLimitedUserToken(token)) {
throw new AuthenticationError(
`Unable to call '${targetPluginId}' plugin on behalf of user, because the target plugin does not support on-behalf-of tokens or the plugin doesn't exist`,
);
}
return { token };
const onBehalfOf = await this.userTokenHandler.createLimitedUserToken(
token,
);
return this.pluginTokenHandler.issueToken({
pluginId: this.pluginId,
targetPluginId,
onBehalfOf,
});
}
default:
throw new AuthenticationError(
@@ -23,13 +23,8 @@ import { tokenManagerServiceFactory } from '@backstage/backend-app-api';
import { authServiceFactory } from './authServiceFactory';
import { base64url, decodeJwt } from 'jose';
import { discoveryServiceFactory } from '../discovery';
import {
BackstageServicePrincipal,
BackstageUserPrincipal,
} from '@backstage/backend-plugin-api';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { InternalBackstageCredentials } from './types';
import { toInternalBackstageCredentials } from './helpers';
const server = setupServer();
@@ -74,12 +69,16 @@ describe('authServiceFactory', () => {
jest.useRealTimers();
});
it('should authenticate issued tokens with legacy auth', async () => {
it('should not support tokens issued with legacy auth', async () => {
server.use(
rest.get(
'http://localhost:7007/api/catalog/.backstage/auth/v1/jwks.json',
(_req, res, ctx) => res(ctx.status(404)),
),
rest.get(
'http://localhost:7007/api/search/.backstage/auth/v1/jwks.json',
(_req, res, ctx) => res(ctx.status(404)),
),
);
const tester = ServiceFactoryTester.from(authServiceFactory, {
@@ -94,21 +93,15 @@ describe('authServiceFactory', () => {
targetPluginId: 'catalog',
});
await expect(searchAuth.authenticate(searchToken)).resolves.toEqual(
expect.objectContaining({
principal: {
type: 'service',
subject: 'external:backstage-plugin',
},
}),
await expect(
searchAuth.authenticate(searchToken),
).rejects.toMatchInlineSnapshot(
`[AuthenticationError: Received a plugin token where the source 'search' 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.]`,
);
await expect(catalogAuth.authenticate(searchToken)).resolves.toEqual(
expect.objectContaining({
principal: {
type: 'service',
subject: 'external:backstage-plugin',
},
}),
await expect(
catalogAuth.authenticate(searchToken),
).rejects.toMatchInlineSnapshot(
`[AuthenticationError: Received a plugin token where the source 'search' 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.]`,
);
});
@@ -151,38 +144,7 @@ describe('authServiceFactory', () => {
);
});
it('should forward user token if target plugin does not support new auth service', async () => {
server.use(
rest.get(
'http://localhost:7007/api/permission/.backstage/auth/v1/jwks.json',
(_req, res, ctx) => res(ctx.status(404)),
),
);
const tester = ServiceFactoryTester.from(authServiceFactory, {
dependencies: mockDeps,
});
const catalogAuth = await tester.getSubject('catalog');
await expect(
catalogAuth.getPluginRequestToken({
onBehalfOf: {
$$type: '@backstage/BackstageCredentials',
version: 'v1',
authMethod: 'token',
token: 'alice-token',
principal: {
type: 'user',
userEntityRef: 'user:default/alice',
},
} as InternalBackstageCredentials<BackstageUserPrincipal>,
targetPluginId: 'permission',
}),
).resolves.toEqual({ token: 'alice-token' });
});
it('should issue a new service token with token manager if target plugin does not support new auth service', async () => {
it('should issue a service token for the new system even if the target plugin does not support it', async () => {
server.use(
rest.get(
'http://localhost:7007/api/permission/.backstage/auth/v1/jwks.json',
@@ -197,22 +159,14 @@ describe('authServiceFactory', () => {
const catalogAuth = await tester.getSubject('catalog');
const { token } = await catalogAuth.getPluginRequestToken({
onBehalfOf: {
$$type: '@backstage/BackstageCredentials',
version: 'v1',
authMethod: 'token',
token: 'some-upstream-service-token',
principal: {
type: 'service',
subject: 'external:upstream-service',
},
} as InternalBackstageCredentials<BackstageServicePrincipal>,
onBehalfOf: await catalogAuth.getOwnServiceCredentials(),
targetPluginId: 'permission',
});
expect(decodeJwt(token)).toEqual(
expect.objectContaining({
sub: 'backstage-server',
sub: 'catalog',
aud: 'permission',
}),
);
});
@@ -415,15 +369,6 @@ describe('authServiceFactory', () => {
targetPluginId: 'permission',
});
expect(decodeJwt(oboToken2).obo).toBe(limitedToken);
await expect(
catalogAuth.getPluginRequestToken({
onBehalfOf: oboCredentials,
targetPluginId: 'kubernetes',
}),
).rejects.toThrow(
"Unable to call 'kubernetes' plugin on behalf of user, because the target plugin does not support on-behalf-of tokens or the plugin doesn't exist",
);
});
it('should eagerly reject access to external access tokens based on plugin id', async () => {
@@ -41,13 +41,8 @@ export const authServiceFactory = createServiceFactory({
discovery: coreServices.discovery,
plugin: coreServices.pluginMetadata,
database: coreServices.database,
// Re-using the token manager makes sure that we use the same generated keys for
// development as plugins that have not yet been migrated. It's important that this
// keeps working as long as there are plugins that have not been migrated to the
// new auth services in the new backend system.
tokenManager: coreServices.tokenManager,
},
async factory({ config, discovery, plugin, tokenManager, logger, database }) {
async factory({ config, discovery, plugin, logger, database }) {
const disableDefaultAuthPolicy =
config.getOptionalBoolean(
'backend.auth.dangerouslyDisableDefaultAuthPolicy',
@@ -84,11 +79,9 @@ export const authServiceFactory = createServiceFactory({
userTokens,
pluginTokens,
externalTokens,
tokenManager,
plugin.getId(),
disableDefaultAuthPolicy,
keySource,
logger,
);
},
});
@@ -146,7 +146,9 @@ export class PluginTokenHandler {
return { token };
}
async isTargetPluginSupported(targetPluginId: string): Promise<boolean> {
private async isTargetPluginSupported(
targetPluginId: string,
): Promise<boolean> {
if (this.supportedTargetPlugins.has(targetPluginId)) {
return true;
}
@@ -199,7 +201,8 @@ export class PluginTokenHandler {
// 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(
`Received a plugin token where the source '${pluginId}' plugin unexpectedly does not have a JWKS endpoint`,
`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.',
);
}