From 359fcd72c1ab0e18e8938fb656abe68df65b926b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 21 Aug 2024 13:17:23 +0200 Subject: [PATCH] backend-defaults: remove support for legacy auth Signed-off-by: Patrik Oldsberg --- .changeset/kind-walls-speak.md | 5 ++ .../entrypoints/auth/DefaultAuthService.ts | 55 +++--------- .../auth/authServiceFactory.test.ts | 89 ++++--------------- .../entrypoints/auth/authServiceFactory.ts | 9 +- .../auth/plugin/PluginTokenHandler.ts | 7 +- 5 files changed, 40 insertions(+), 125 deletions(-) create mode 100644 .changeset/kind-walls-speak.md diff --git a/.changeset/kind-walls-speak.md b/.changeset/kind-walls-speak.md new file mode 100644 index 0000000000..a2470650a4 --- /dev/null +++ b/.changeset/kind-walls-speak.md @@ -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. diff --git a/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts b/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts index 08cfc147f8..05d62a54a2 100644 --- a/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts +++ b/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts @@ -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( diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts index 4fdee1c826..d37e4d1443 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts @@ -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, - 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, + 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 () => { diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts index b66ba7227f..34071ca541 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts @@ -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, ); }, }); diff --git a/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts index a401469fdc..ff594ecbd4 100644 --- a/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts +++ b/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts @@ -146,7 +146,9 @@ export class PluginTokenHandler { return { token }; } - async isTargetPluginSupported(targetPluginId: string): Promise { + private async isTargetPluginSupported( + targetPluginId: string, + ): Promise { 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.', ); }