fix: simplify identity resolver
This commit is contained in:
@@ -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<any>;
|
||||
|
||||
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<AuthResponse<any>> => {
|
||||
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<any>
|
||||
> => {
|
||||
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 {};
|
||||
});
|
||||
|
||||
|
||||
@@ -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<UserEntityV1alpha1>;
|
||||
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',
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -112,6 +112,11 @@ export interface AuthProviderRouteHandlers {
|
||||
logout?(req: express.Request, res: express.Response): Promise<void>;
|
||||
}
|
||||
|
||||
export type IdentityResolver = (
|
||||
payload: object,
|
||||
catalogApi: CatalogApi,
|
||||
) => Promise<AuthResponse<any>>;
|
||||
|
||||
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 = (
|
||||
|
||||
Reference in New Issue
Block a user