diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts index 16f05ef9d0..f24264930a 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.test.ts @@ -51,8 +51,9 @@ describe('authServiceFactory', () => { const searchAuth = await tester.get('search'); const catalogAuth = await tester.get('catalog'); - const { token: searchToken } = await searchAuth.issueServiceToken({ - forward: await searchAuth.getOwnCredentials(), + const { token: searchToken } = await searchAuth.getPluginRequestToken({ + onBehalfOf: await searchAuth.getOwnServiceCredentials(), + targetPluginId: 'catalog', }); await expect(searchAuth.authenticate(searchToken)).resolves.toEqual( @@ -81,8 +82,8 @@ describe('authServiceFactory', () => { const catalogAuth = await tester.get('catalog'); await expect( - catalogAuth.issueServiceToken({ - forward: { + catalogAuth.getPluginRequestToken({ + onBehalfOf: { $$type: '@backstage/BackstageCredentials', version: 'v1', authMethod: 'token', @@ -92,6 +93,7 @@ describe('authServiceFactory', () => { userEntityRef: 'user:default/alice', }, } as InternalBackstageCredentials, + targetPluginId: 'catalog', }), ).resolves.toEqual({ token: 'alice-token' }); }); @@ -103,8 +105,8 @@ describe('authServiceFactory', () => { const catalogAuth = await tester.get('catalog'); - const { token } = await catalogAuth.issueServiceToken({ - forward: { + const { token } = await catalogAuth.getPluginRequestToken({ + onBehalfOf: { $$type: '@backstage/BackstageCredentials', version: 'v1', authMethod: 'token', @@ -114,6 +116,7 @@ describe('authServiceFactory', () => { subject: 'external:upstream-service', }, } as InternalBackstageCredentials, + targetPluginId: 'catalog', }); expect(decodeJwt(token)).toEqual( diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts index 7e9ad3077f..dac8ae6d9e 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts @@ -154,16 +154,17 @@ class DefaultAuthService implements AuthService { return true; } - async getOwnCredentials(): Promise< + async getOwnServiceCredentials(): Promise< BackstageCredentials > { return createCredentialsWithServicePrincipal(`plugin:${this.pluginId}`); } - async issueServiceToken(options: { - forward: BackstageCredentials; + async getPluginRequestToken(options: { + onBehalfOf: BackstageCredentials; + targetPluginId: string; }): Promise<{ token: string }> { - const internalForward = toInternalBackstageCredentials(options.forward); + const internalForward = toInternalBackstageCredentials(options.onBehalfOf); const { type } = internalForward.principal; switch (type) { diff --git a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts b/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts index 1723e3b76c..2182e94684 100644 --- a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts @@ -63,7 +63,7 @@ type RequestWithCredentials = Request & { [credentialsSymbol]?: Promise; }; -class DefaultHttpAuthService implements HttpAuthService { +export class DefaultHttpAuthService implements HttpAuthService { constructor( private readonly auth: AuthService, private readonly discovery: DiscoveryService, @@ -132,14 +132,6 @@ class DefaultHttpAuthService implements HttpAuthService { return credentials as any; } - async requestHeaders(options: { - forward: BackstageCredentials; - }): Promise> { - return { - Authorization: `Bearer ${await this.auth.issueServiceToken(options)}`, - }; - } - async issueUserCookie(res: Response): Promise { const credentials = await this.credentials(res.req, { allow: ['user'] }); diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index cd4d5ab309..1940a79b78 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -234,7 +234,7 @@ export function createDatabaseClient( }, ): knexFactory.Knex; -// @public (undocumented) +// @public export function createLegacyAuthAdapters< TOptions extends { auth?: AuthService; diff --git a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts index 932c8c421a..98d21ba559 100644 --- a/packages/backend-common/src/auth/createLegacyAuthAdapters.ts +++ b/packages/backend-common/src/auth/createLegacyAuthAdapters.ts @@ -63,42 +63,43 @@ class AuthCompat implements AuthService { return true; } - async getOwnCredentials(): Promise< + async getOwnServiceCredentials(): Promise< BackstageCredentials > { return createCredentialsWithServicePrincipal('external:backstage-plugin'); } async authenticate(token: string): Promise { - const { sub, aud } = decodeJwt(token); + const { aud } = decodeJwt(token); - // Legacy service-to-service token - if (sub === 'backstage-server' && !aud) { - await this.tokenManager.authenticate(token); - return createCredentialsWithServicePrincipal('external:backstage-plugin'); + if (aud === 'backstage') { + // User Backstage token + const identity = await this.identity.getIdentity({ + request: { + headers: { authorization: `Bearer ${token}` }, + }, + } as IdentityApiGetIdentityRequest); + + if (!identity) { + throw new AuthenticationError('Invalid user token'); + } + + return createCredentialsWithUserPrincipal( + identity.identity.userEntityRef, + token, + ); } - // User Backstage token - const identity = await this.identity.getIdentity({ - request: { - headers: { authorization: `Bearer ${token}` }, - }, - } as IdentityApiGetIdentityRequest); + await this.tokenManager.authenticate(token); - if (!identity) { - throw new AuthenticationError('Invalid user token'); - } - - return createCredentialsWithUserPrincipal( - identity.identity.userEntityRef, - token, - ); + return createCredentialsWithServicePrincipal('external:backstage-plugin'); } - async issueServiceToken(options: { - forward: BackstageCredentials; + async getPluginRequestToken(options: { + onBehalfOf: BackstageCredentials; + targetPluginId: string; }): Promise<{ token: string }> { - const internalForward = toInternalBackstageCredentials(options.forward); + const internalForward = toInternalBackstageCredentials(options.onBehalfOf); const { type } = internalForward.principal; switch (type) { @@ -199,14 +200,6 @@ class HttpAuthCompat implements HttpAuthService { return credentials as any; } - async requestHeaders(options: { - forward: BackstageCredentials; - }): Promise> { - return { - Authorization: `Bearer ${await this.auth.issueServiceToken(options)}`, - }; - } - async issueUserCookie(_res: Response): Promise {} } diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 27dfeb13b6..537e7a8a15 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -22,16 +22,21 @@ export interface AuthService { // (undocumented) authenticate(token: string): Promise; // (undocumented) - getOwnCredentials(): Promise>; + getOwnServiceCredentials(): Promise< + BackstageCredentials + >; + // (undocumented) + getPluginRequestToken(options: { + onBehalfOf: BackstageCredentials; + targetPluginId: string; + }): Promise<{ + token: string; + }>; // (undocumented) isPrincipal( credentials: BackstageCredentials, type: TType, ): credentials is BackstageCredentials; - // (undocumented) - issueServiceToken(options: { forward: BackstageCredentials }): Promise<{ - token: string; - }>; } // @public (undocumented) @@ -295,10 +300,6 @@ export interface HttpAuthService { ): Promise>; // (undocumented) issueUserCookie(res: Response_2): Promise; - // (undocumented) - requestHeaders(options: { - forward: BackstageCredentials; - }): Promise>; } // @public (undocumented) diff --git a/packages/backend-plugin-api/src/services/definitions/AuthService.ts b/packages/backend-plugin-api/src/services/definitions/AuthService.ts index 4566d4a3fa..dc38b572c5 100644 --- a/packages/backend-plugin-api/src/services/definitions/AuthService.ts +++ b/packages/backend-plugin-api/src/services/definitions/AuthService.ts @@ -70,10 +70,12 @@ export interface AuthService { type: TType, ): credentials is BackstageCredentials; - getOwnCredentials(): Promise>; + getOwnServiceCredentials(): Promise< + BackstageCredentials + >; - // TODO: should the caller provide the target plugin ID? - issueServiceToken(options: { - forward: BackstageCredentials; + getPluginRequestToken(options: { + onBehalfOf: BackstageCredentials; + targetPluginId: string; }): Promise<{ token: string }>; } diff --git a/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts b/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts index cc8aace8ba..bcc31880b3 100644 --- a/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts +++ b/packages/backend-plugin-api/src/services/definitions/HttpAuthService.ts @@ -27,9 +27,5 @@ export interface HttpAuthService { }, ): Promise>; - requestHeaders(options: { - forward: BackstageCredentials; - }): Promise>; - issueUserCookie(res: Response): Promise; } diff --git a/packages/backend-test-utils/src/next/services/MockAuthService.ts b/packages/backend-test-utils/src/next/services/MockAuthService.ts index 9af9be4818..925ee1bb6d 100644 --- a/packages/backend-test-utils/src/next/services/MockAuthService.ts +++ b/packages/backend-test-utils/src/next/services/MockAuthService.ts @@ -44,7 +44,7 @@ export class MockAuthService implements AuthService { throw new AuthenticationError('Invalid token'); } - async getOwnCredentials(): Promise< + async getOwnServiceCredentials(): Promise< BackstageCredentials > { return { @@ -68,10 +68,11 @@ export class MockAuthService implements AuthService { return true; } - async issueServiceToken(options: { - forward: BackstageCredentials; + async getPluginRequestToken(options: { + onBehalfOf: BackstageCredentials; + targetPluginId: string; }): Promise<{ token: string }> { - const principal = options.forward.principal as + const principal = options.onBehalfOf.principal as | BackstageUserPrincipal | BackstageServicePrincipal | BackstageNonePrincipal; @@ -80,7 +81,7 @@ export class MockAuthService implements AuthService { case 'user': return { token: 'mock-user-token' }; case 'service': - return { token: 'mock-service-token' }; + return { token: `mock-service-token-for-${options.targetPluginId}` }; default: throw new AuthenticationError( `Refused to issue service token for credential type '${principal.type}'`, diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index ba43aff008..63f8ffbd93 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -180,9 +180,9 @@ export namespace mockServices { }); export const mock = simpleMock(coreServices.auth, () => ({ authenticate: jest.fn(), - getOwnCredentials: jest.fn(), + getOwnServiceCredentials: jest.fn(), isPrincipal: jest.fn() as any, - issueServiceToken: jest.fn(), + getPluginRequestToken: jest.fn(), })); } @@ -217,7 +217,6 @@ export namespace mockServices { export const mock = simpleMock(coreServices.httpAuth, () => ({ credentials: jest.fn(), issueUserCookie: jest.fn(), - requestHeaders: jest.fn(), })); } diff --git a/yarn.lock b/yarn.lock index 6d841a09db..218faf39a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3287,6 +3287,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/integration-aws-node": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" "@backstage/types": "workspace:^" "@google-cloud/storage": ^7.0.0 "@keyv/memcache": ^1.3.5