diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index 55c27fd7ff..93649ce8d7 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -18,7 +18,9 @@ import express from 'express'; import * as jwtVerify from 'jose/jwt/verify'; import { AwsAlbAuthProvider } from './provider'; -import { UserEntityV1alpha1 } from '@backstage/catalog-model'; +import { AuthResponse } from '../types'; + +const mockedJwtVerify = jwtVerify as jest.Mocked; const mockKey = async () => { return `-----BEGIN PUBLIC KEY----- @@ -44,26 +46,21 @@ jest.mock('jose/jwt/verify', () => { }; }); -const identityResolutionCallbackMock = async (): Promise< - UserEntityV1alpha1 -> => { +const identityResolutionCallbackMock = async (): Promise> => { return { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name: 'foo', + backstageIdentity: { + id: 'foo', + idToken: '', }, - spec: { - memberOf: [], - profile: { - displayName: 'Foo Bar', - }, + profile: { + displayName: 'Foo Bar', }, + providerInfo: {}, }; }; const identityResolutionCallbackRejectedMock = async (): Promise< - UserEntityV1alpha1 + AuthResponse > => { throw new Error('failed'); }; @@ -83,13 +80,6 @@ describe('AwsALBAuthProvider', () => { getEntityByName: jest.fn(), }; - const tokenIssuer = { - issueToken: async () => { - return ''; - }, - listPublicKeys: jest.fn(), - }; - const mockResponseSend = jest.fn(); const mockRequest = ({ header: jest.fn(() => { @@ -108,18 +98,13 @@ describe('AwsALBAuthProvider', () => { describe('should transform to type OAuthResponse', () => { it('when JWT is valid and identity is resolved successfully', async () => { - const provider = new AwsAlbAuthProvider( - getVoidLogger(), - catalogApi, - tokenIssuer, - { - region: 'us-west-2', - identityResolutionCallback: identityResolutionCallbackMock, - issuer: 'foo', - }, - ); + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { + region: 'us-west-2', + identityResolutionCallback: identityResolutionCallbackMock, + issuer: 'foo', + }); - jwtVerify.default.mockImplementationOnce(async () => { + mockedJwtVerify.default.mockImplementationOnce(async () => { return { payload: { sub: 'foo', @@ -143,16 +128,11 @@ describe('AwsALBAuthProvider', () => { }); describe('should fail when', () => { it('JWT is missing', async () => { - const provider = new AwsAlbAuthProvider( - getVoidLogger(), - catalogApi, - tokenIssuer, - { - region: 'us-west-2', - identityResolutionCallback: identityResolutionCallbackMock, - issuer: 'foo', - }, - ); + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { + region: 'us-west-2', + identityResolutionCallback: identityResolutionCallbackMock, + issuer: 'foo', + }); await provider.refresh(mockRequestWithoutJwt, mockResponse); @@ -160,18 +140,13 @@ describe('AwsALBAuthProvider', () => { }); it('JWT is invalid', async () => { - const provider = new AwsAlbAuthProvider( - getVoidLogger(), - catalogApi, - tokenIssuer, - { - region: 'us-west-2', - identityResolutionCallback: identityResolutionCallbackMock, - issuer: 'foo', - }, - ); + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { + region: 'us-west-2', + identityResolutionCallback: identityResolutionCallbackMock, + issuer: 'foo', + }); - jwtVerify.default.mockImplementationOnce(async () => { + mockedJwtVerify.default.mockImplementationOnce(async () => { throw new Error('bad JWT'); }); @@ -181,18 +156,13 @@ describe('AwsALBAuthProvider', () => { }); it('issuer is invalid', async () => { - const provider = new AwsAlbAuthProvider( - getVoidLogger(), - catalogApi, - tokenIssuer, - { - region: 'us-west-2', - identityResolutionCallback: identityResolutionCallbackMock, - issuer: 'foobar', - }, - ); + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { + region: 'us-west-2', + identityResolutionCallback: identityResolutionCallbackMock, + issuer: 'foobar', + }); - jwtVerify.default.mockImplementationOnce(async () => { + mockedJwtVerify.default.mockImplementationOnce(async () => { return {}; }); @@ -202,18 +172,13 @@ describe('AwsALBAuthProvider', () => { }); it('identity resolution callback rejects', async () => { - const provider = new AwsAlbAuthProvider( - getVoidLogger(), - catalogApi, - tokenIssuer, - { - region: 'us-west-2', - identityResolutionCallback: identityResolutionCallbackRejectedMock, - issuer: 'foo', - }, - ); + const provider = new AwsAlbAuthProvider(getVoidLogger(), catalogApi, { + region: 'us-west-2', + identityResolutionCallback: identityResolutionCallbackRejectedMock, + issuer: 'foo', + }); - jwtVerify.default.mockImplementationOnce(async () => { + mockedJwtVerify.default.mockImplementationOnce(async () => { return {}; }); diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index f00543302b..568fc4dd46 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -16,30 +16,26 @@ import { AuthProviderFactoryOptions, AuthProviderRouteHandlers, + IdentityResolver, } from '../types'; -import { TokenIssuer } from '../../identity'; import express from 'express'; +// @ts-ignore no types available for R2 import r2 from 'r2'; import * as crypto from 'crypto'; import { KeyObject } from 'crypto'; import { Logger } from 'winston'; import jwtVerify from 'jose/jwt/verify'; import { CatalogApi } from '@backstage/catalog-client'; -import { UserEntityV1alpha1 } from '@backstage/catalog-model'; const ALB_JWT_HEADER = 'x-amzn-oidc-data'; /** * A callback function that receives a verified JWT and returns a UserEntity * @param {payload} The verified JWT payload */ -type IdentityResolutionCallback = ( - payload: object, - catalogApi: CatalogApi, -) => Promise; type AwsAlbAuthProviderOptions = { region: string; issuer: string; - identityResolutionCallback: IdentityResolutionCallback; + identityResolutionCallback: IdentityResolver; }; export const getJWTHeaders = (input: string) => { const encoded = input.split('.')[0]; @@ -49,19 +45,16 @@ export const getJWTHeaders = (input: string) => { export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { private logger: Logger; private readonly catalogClient: CatalogApi; - private tokenIssuer: TokenIssuer; private options: AwsAlbAuthProviderOptions; private readonly keyCache: { [key: string]: KeyObject }; constructor( logger: Logger, catalogClient: CatalogApi, - tokenIssuer: TokenIssuer, options: AwsAlbAuthProviderOptions, ) { this.logger = logger; this.catalogClient = catalogClient; - this.tokenIssuer = tokenIssuer; this.options = options; this.keyCache = {}; } @@ -88,16 +81,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { verifiedToken.payload, this.catalogClient, ); - res.send({ - providerInfo: {}, - profile: resolvedEntity?.spec?.profile, - backstageIdentity: { - id: resolvedEntity?.metadata?.name, - idToken: await this.tokenIssuer.issueToken({ - claims: { sub: resolvedEntity?.metadata?.name }, - }), - }, - }); + res.send(resolvedEntity); } catch (e) { this.logger.error('exception occurred during JWT processing', e); res.send(401); @@ -124,15 +108,22 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { } } -export const createAwsAlbProvider = ( - { logger, catalogApi, tokenIssuer, config }: AuthProviderFactoryOptions, - identityResolver: IdentityResolutionCallback, -) => { +export const createAwsAlbProvider = ({ + logger, + catalogApi, + config, + identityResolver, +}: AuthProviderFactoryOptions) => { const region = config.getString('region'); const issuer = config.getString('iss'); - return new AwsAlbAuthProvider(logger, catalogApi, tokenIssuer, { - region, - issuer, - identityResolutionCallback: identityResolver, - }); + if (identityResolver !== undefined) { + return new AwsAlbAuthProvider(logger, catalogApi, { + region, + issuer, + identityResolutionCallback: identityResolver, + }); + } + throw new Error( + 'Identity resolver is required to use this authentication provider', + ); }; diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 7bad4f4d81..619fb1c706 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -25,6 +25,7 @@ import { createAuth0Provider } from './auth0'; import { createMicrosoftProvider } from './microsoft'; import { createOneLoginProvider } from './onelogin'; import { AuthProviderFactory } from './types'; +import { createAwsAlbProvider } from './aws-alb'; export const factories: { [providerId: string]: AuthProviderFactory } = { google: createGoogleProvider, @@ -37,4 +38,5 @@ export const factories: { [providerId: string]: AuthProviderFactory } = { oauth2: createOAuth2Provider, oidc: createOidcProvider, onelogin: createOneLoginProvider, + awsalb: createAwsAlbProvider, }; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index a40f2a96ee..51c1ee336e 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -112,6 +112,11 @@ export interface AuthProviderRouteHandlers { logout?(req: express.Request, res: express.Response): Promise; } +export type IdentityResolver = ( + payload: object, + catalogApi: CatalogApi, +) => Promise>; + export type AuthProviderFactoryOptions = { providerId: string; globalConfig: AuthProviderConfig; @@ -120,6 +125,7 @@ export type AuthProviderFactoryOptions = { tokenIssuer: TokenIssuer; discovery: PluginEndpointDiscovery; catalogApi: CatalogApi; + identityResolver?: IdentityResolver; }; export type AuthProviderFactory = (