fix: simplify identity resolver

This commit is contained in:
Jonah Back
2021-01-12 15:17:28 -08:00
committed by Jonah Back
parent 6ef5428e73
commit 0d6ad133c7
4 changed files with 68 additions and 104 deletions
@@ -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 = (