diff --git a/.changeset/silent-waves-pretend.md b/.changeset/silent-waves-pretend.md
new file mode 100644
index 0000000000..5986579fe6
--- /dev/null
+++ b/.changeset/silent-waves-pretend.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-auth-backend-module-aws-alb-provider': minor
+'@backstage/plugin-auth-backend': patch
+---
+
+Migrated the AWS ALB auth provider to new `@backstage/plugin-auth-backend-module-aws-alb-provider` module package.
diff --git a/plugins/auth-backend-module-aws-alb-provider/.eslintrc.js b/plugins/auth-backend-module-aws-alb-provider/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/auth-backend-module-aws-alb-provider/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/auth-backend-module-aws-alb-provider/README.md b/plugins/auth-backend-module-aws-alb-provider/README.md
new file mode 100644
index 0000000000..dd92ae7ba2
--- /dev/null
+++ b/plugins/auth-backend-module-aws-alb-provider/README.md
@@ -0,0 +1,8 @@
+# Auth Module: AWS ALB Provider
+
+This module provides an GitHub auth provider implementation for `@backstage/plugin-auth-backend`.
+
+## Links
+
+- [Backstage](https://backstage.io)
+- [Repository](https://github.com/backstage/backstage/tree/master/plugins/auth-backend-module-github-provider)
diff --git a/plugins/auth-backend-module-aws-alb-provider/api-report.md b/plugins/auth-backend-module-aws-alb-provider/api-report.md
new file mode 100644
index 0000000000..76a366d2c9
--- /dev/null
+++ b/plugins/auth-backend-module-aws-alb-provider/api-report.md
@@ -0,0 +1,42 @@
+## API Report File for "@backstage/plugin-auth-backend-module-aws-alb-provider"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+import { BackendFeature } from '@backstage/backend-plugin-api';
+import { JWTHeaderParameters } from 'jose';
+import { KeyObject } from 'crypto';
+import type { PassportProfile } from '@backstage/plugin-auth-node/';
+import { ProxyAuthenticator } from '@backstage/plugin-auth-node';
+import { SignInResolverFactory } from '@backstage/plugin-auth-node';
+
+// @public (undocumented)
+export const authModuleAwsAlbProvider: () => BackendFeature;
+
+// @public (undocumented)
+export const awsAlbAuthenticator: ProxyAuthenticator<
+ {
+ issuer: string;
+ getKey: (header: JWTHeaderParameters) => Promise;
+ },
+ AwsAlbResult
+>;
+
+// @public
+export type AwsAlbResult = {
+ fullProfile: PassportProfile;
+ expiresInSeconds?: number;
+ accessToken: string;
+};
+
+// @public
+export namespace awsAlbSignInResolvers {
+ const // (undocumented)
+ emailMatchingUserEntityProfileEmail: SignInResolverFactory<
+ AwsAlbResult,
+ unknown
+ >;
+}
+```
diff --git a/plugins/auth-backend-module-aws-alb-provider/catalog-info.yaml b/plugins/auth-backend-module-aws-alb-provider/catalog-info.yaml
new file mode 100644
index 0000000000..b3ced6b59d
--- /dev/null
+++ b/plugins/auth-backend-module-aws-alb-provider/catalog-info.yaml
@@ -0,0 +1,10 @@
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: backstage-plugin-auth-backend-module-aws-alb-provider
+ title: '@backstage/plugin-auth-backend-module-aws-alb-provider'
+ description: The aws-alb provider module for the Backstage auth backend.
+spec:
+ lifecycle: experimental
+ type: backstage-backend-plugin-module
+ owner: maintainers
diff --git a/plugins/auth-backend-module-aws-alb-provider/package.json b/plugins/auth-backend-module-aws-alb-provider/package.json
new file mode 100644
index 0000000000..6d4a5835bf
--- /dev/null
+++ b/plugins/auth-backend-module-aws-alb-provider/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@backstage/plugin-auth-backend-module-aws-alb-provider",
+ "description": "The aws-alb provider module for the Backstage auth backend.",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin-module"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "plugins/auth-backend-module-aws-alb-provider"
+ },
+ "keywords": [
+ "backstage"
+ ],
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "dependencies": {
+ "@backstage/backend-common": "workspace:^",
+ "@backstage/backend-plugin-api": "workspace:^",
+ "@backstage/errors": "workspace:^",
+ "@backstage/plugin-auth-backend": "workspace:^",
+ "@backstage/plugin-auth-node": "workspace:^",
+ "jose": "^4.6.0",
+ "jwt-decode": "^3.1.0",
+ "node-cache": "^5.1.2"
+ },
+ "devDependencies": {
+ "@backstage/backend-test-utils": "workspace:^",
+ "@backstage/cli": "workspace:^",
+ "@backstage/config": "workspace:^",
+ "express": "^4.18.2"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/auth-backend-module-aws-alb-provider/src/authenticator.test.ts b/plugins/auth-backend-module-aws-alb-provider/src/authenticator.test.ts
new file mode 100644
index 0000000000..bfcbfce622
--- /dev/null
+++ b/plugins/auth-backend-module-aws-alb-provider/src/authenticator.test.ts
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ ALB_ACCESS_TOKEN_HEADER,
+ ALB_JWT_HEADER,
+ awsAlbAuthenticator,
+} from './authenticator';
+import { jwtVerify } from 'jose';
+import express from 'express';
+import { AuthenticationError } from '@backstage/errors';
+import { Config } from '@backstage/config';
+
+const jwtMock = jwtVerify as jest.Mocked;
+const mockJwt =
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IktFWV9JRCIsImlzcyI6IklTU1VFUl9VUkwifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlVzZXIgTmFtZSIsImlhdCI6MTUxNjIzOTAyMn0.uMCSBGhij1xn5pnot8XgD-huQuTIBOFGs6kkW_p_X94';
+const mockAccessToken = 'ACCESS_TOKEN';
+const mockClaims = {
+ sub: '1234567890',
+ name: 'User Name',
+ family_name: 'Name',
+ given_name: 'User',
+ picture: 'PICTURE_URL',
+ email: 'user.name@email.test',
+ exp: 1632833763,
+ iss: 'ISSUER_URL',
+};
+jest.mock('jose');
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+describe('AwsAlbProvider', () => {
+ const mockRequest = {
+ header: jest.fn(name => {
+ if (name === ALB_JWT_HEADER) {
+ return mockJwt;
+ } else if (name === ALB_ACCESS_TOKEN_HEADER) {
+ return mockAccessToken;
+ }
+ return undefined;
+ }),
+ } as unknown as express.Request;
+ const mockRequestWithoutJwt = {
+ header: jest.fn(name => {
+ if (name === ALB_ACCESS_TOKEN_HEADER) {
+ return mockAccessToken;
+ }
+ return undefined;
+ }),
+ } as unknown as express.Request;
+ const mockRequestWithoutAccessToken = {
+ header: jest.fn(name => {
+ if (name === ALB_JWT_HEADER) {
+ return mockJwt;
+ }
+ return undefined;
+ }),
+ } as unknown as express.Request;
+
+ describe('should transform to type AwsAlbResponse', () => {
+ it('when JWT is valid and identity is resolved successfully', async () => {
+ jwtMock.mockReturnValueOnce(Promise.resolve({ payload: mockClaims }));
+
+ const response = await awsAlbAuthenticator.authenticate(
+ { req: mockRequest },
+ { issuer: 'ISSUER_URL', getKey: jest.fn() },
+ );
+ expect(response).toEqual({
+ result: {
+ fullProfile: {
+ provider: 'unknown',
+ id: mockClaims.sub,
+ displayName: mockClaims.name,
+ username: mockClaims.email.split('@')[0].toLowerCase(),
+ name: {
+ familyName: mockClaims.family_name,
+ givenName: mockClaims.given_name,
+ },
+ emails: [{ value: mockClaims.email.toLowerCase() }],
+ photos: [{ value: mockClaims.picture }],
+ },
+ expiresInSeconds: mockClaims.exp,
+ accessToken: mockAccessToken,
+ },
+ });
+ });
+ });
+ describe('should fail when', () => {
+ it('Access token is missing', async () => {
+ await expect(
+ awsAlbAuthenticator.authenticate(
+ { req: mockRequestWithoutAccessToken },
+ { issuer: 'ISSUER_URL', getKey: jest.fn() },
+ ),
+ ).rejects.toThrow(AuthenticationError);
+ });
+
+ it('JWT is missing', async () => {
+ await expect(
+ awsAlbAuthenticator.authenticate(
+ { req: mockRequestWithoutJwt },
+ { issuer: 'ISSUER_URL', getKey: jest.fn() },
+ ),
+ ).rejects.toThrow(AuthenticationError);
+ });
+
+ it('JWT is invalid', async () => {
+ jwtMock.mockImplementationOnce(() => {
+ throw new Error('bad JWT');
+ });
+
+ await expect(
+ awsAlbAuthenticator.authenticate(
+ { req: mockRequest },
+ { issuer: 'ISSUER_URL', getKey: jest.fn() },
+ ),
+ ).rejects.toThrow(
+ 'Exception occurred during JWT processing: Error: bad JWT',
+ );
+ });
+
+ it('issuer is missing', async () => {
+ jwtMock.mockReturnValueOnce({});
+
+ await expect(
+ awsAlbAuthenticator.authenticate(
+ { req: mockRequest },
+ { issuer: 'ISSUER_URL', getKey: jest.fn() },
+ ),
+ ).rejects.toThrow(
+ 'Exception occurred during JWT processing: AuthenticationError: Issuer mismatch on JWT token',
+ );
+ });
+
+ it('issuer is invalid', async () => {
+ jwtMock.mockReturnValueOnce({
+ iss: 'INVALID_ISSUE_URL',
+ });
+
+ await expect(
+ awsAlbAuthenticator.authenticate(
+ { req: mockRequest },
+ { issuer: 'ISSUER_URL', getKey: jest.fn() },
+ ),
+ ).rejects.toThrow(
+ 'Exception occurred during JWT processing: AuthenticationError: Issuer mismatch on JWT token',
+ );
+ });
+ });
+ describe('should initialize', () => {
+ it('with default options', async () => {
+ const config = {
+ config: {
+ getString: jest
+ .fn()
+ .mockReturnValueOnce('ISSUER_URL')
+ .mockReturnValueOnce('TEST_REGION'),
+ } as unknown as Config,
+ };
+
+ expect(awsAlbAuthenticator.initialize(config)).toEqual({
+ issuer: 'ISSUER_URL',
+ getKey: expect.any(Function),
+ });
+ });
+ });
+});
diff --git a/plugins/auth-backend-module-aws-alb-provider/src/authenticator.ts b/plugins/auth-backend-module-aws-alb-provider/src/authenticator.ts
new file mode 100644
index 0000000000..d7ca79927f
--- /dev/null
+++ b/plugins/auth-backend-module-aws-alb-provider/src/authenticator.ts
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AuthenticationError } from '@backstage/errors';
+import { AwsAlbClaims, AwsAlbResult } from './types';
+import { jwtVerify } from 'jose';
+import {
+ PassportProfile,
+ createProxyAuthenticator,
+} from '@backstage/plugin-auth-node';
+import NodeCache from 'node-cache';
+import { makeProfileInfo, provisionKeyCache } from './helpers';
+
+export const ALB_JWT_HEADER = 'x-amzn-oidc-data';
+export const ALB_ACCESS_TOKEN_HEADER = 'x-amzn-oidc-accesstoken';
+
+/** @public */
+export const awsAlbAuthenticator = createProxyAuthenticator({
+ defaultProfileTransform: async (result: AwsAlbResult) => {
+ return {
+ profile: makeProfileInfo(result.fullProfile, result.accessToken),
+ };
+ },
+ initialize({ config }) {
+ const issuer = config.getString('issuer');
+ const region = config.getString('region');
+ const keyCache = new NodeCache({ stdTTL: 3600 });
+ const getKey = provisionKeyCache(region, keyCache);
+ return { issuer, getKey };
+ },
+ async authenticate({ req }, { issuer, getKey }) {
+ const jwt = req.header(ALB_JWT_HEADER);
+ const accessToken = req.header(ALB_ACCESS_TOKEN_HEADER);
+
+ if (jwt === undefined) {
+ throw new AuthenticationError(
+ `Missing ALB OIDC header: ${ALB_JWT_HEADER}`,
+ );
+ }
+
+ if (accessToken === undefined) {
+ throw new AuthenticationError(
+ `Missing ALB OIDC header: ${ALB_ACCESS_TOKEN_HEADER}`,
+ );
+ }
+
+ try {
+ const verifyResult = await jwtVerify(jwt, getKey);
+ const claims = verifyResult.payload as AwsAlbClaims;
+
+ if (issuer && claims?.iss !== issuer) {
+ throw new AuthenticationError('Issuer mismatch on JWT token');
+ }
+
+ const fullProfile: PassportProfile = {
+ provider: 'unknown',
+ id: claims.sub,
+ displayName: claims.name,
+ username: claims.email.split('@')[0].toLowerCase(),
+ name: {
+ familyName: claims.family_name,
+ givenName: claims.given_name,
+ },
+ emails: [{ value: claims.email.toLowerCase() }],
+ photos: [{ value: claims.picture }],
+ };
+
+ return {
+ result: {
+ fullProfile,
+ expiresInSeconds: claims.exp,
+ accessToken,
+ },
+ };
+ } catch (e) {
+ throw new Error(`Exception occurred during JWT processing: ${e}`);
+ }
+ },
+});
diff --git a/plugins/auth-backend-module-aws-alb-provider/src/helpers.test.ts b/plugins/auth-backend-module-aws-alb-provider/src/helpers.test.ts
new file mode 100644
index 0000000000..07ad118cea
--- /dev/null
+++ b/plugins/auth-backend-module-aws-alb-provider/src/helpers.test.ts
@@ -0,0 +1,142 @@
+import NodeCache from 'node-cache';
+import { makeProfileInfo, provisionKeyCache } from './helpers';
+import * as crypto from 'crypto';
+import { JWTHeaderParameters } from 'jose';
+import { PassportProfile } from '@backstage/plugin-auth-node';
+import jwtDecoder from 'jwt-decode';
+
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const mockKey = async () => {
+ return `-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnuN4LlaJhaUpx+qZFTzYCrSBLk0I
+yOlxJ2VW88mLAQGJ7HPAvOdylxZsItMnzCuqNzZvie8m/NJsOjhDncVkrw==
+-----END PUBLIC KEY-----
+`;
+};
+jest.mock('crypto');
+const cryptoMock = crypto as jest.Mocked;
+jest.mock('node-fetch', () => ({
+ __esModule: true,
+ default: async () => {
+ return {
+ text: async () => {
+ return mockKey();
+ },
+ };
+ },
+}));
+
+const jwtMock = jwtDecoder as jest.Mocked;
+jest.mock('jwt-decode');
+
+describe('helpers', () => {
+ const nodeCache = jest.fn() as unknown as NodeCache;
+ nodeCache.set = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ it('should create a key', () => {
+ const getKey = provisionKeyCache('eu-west-1', nodeCache);
+ expect(getKey).toBeDefined();
+ });
+ it('should return a key from cache', async () => {
+ const getKey = provisionKeyCache('eu-west-1', nodeCache);
+
+ cryptoMock.createPublicKey.mockReturnValueOnce('key');
+ nodeCache.get = jest.fn().mockReturnValue('key');
+
+ const key = await getKey({ kid: 'kid' } as unknown as JWTHeaderParameters);
+
+ expect(key).toBe('key');
+ });
+ it('should update cache if key is not found', async () => {
+ const getKey = provisionKeyCache('eu-west-1', nodeCache);
+
+ nodeCache.get = jest.fn().mockReturnValue(undefined);
+ jest.spyOn(nodeCache, 'set');
+ cryptoMock.createPublicKey.mockReturnValue({
+ export: jest.fn().mockReturnValue('key'),
+ });
+
+ await getKey({ kid: 'kid' } as unknown as JWTHeaderParameters);
+ expect(nodeCache.set).toHaveBeenCalledWith('kid', 'key');
+ });
+ it('should throw error if key is not found', async () => {
+ const getKey = provisionKeyCache('eu-west-1', nodeCache);
+
+ nodeCache.get = jest.fn().mockReturnValue(undefined);
+ cryptoMock.createPublicKey.mockReturnValue(undefined);
+
+ await expect(
+ getKey({ kid: 'kid' } as unknown as JWTHeaderParameters),
+ ).rejects.toThrow();
+ });
+ it('should throw if key is not present in request header', async () => {
+ const getKey = provisionKeyCache('eu-west-1', nodeCache);
+
+ nodeCache.get = jest.fn().mockReturnValue(undefined);
+
+ await expect(getKey({} as unknown as JWTHeaderParameters)).rejects.toThrow(
+ 'No key id was specified in header',
+ );
+ });
+});
+
+describe('makeProfileInfo', () => {
+ it('should return profile info', () => {
+ const profile = {
+ id: 'id',
+ displayName: 'displayName',
+ username: 'username',
+ name: {
+ familyName: 'familyName',
+ givenName: 'givenName',
+ },
+ emails: [{ value: 'email' }],
+ photos: [{ value: 'picture' }],
+ } as PassportProfile;
+ const accessToken = 'accessToken';
+ const result = {
+ email: 'email',
+ picture: 'picture',
+ displayName: 'displayName',
+ };
+ expect(makeProfileInfo(profile, accessToken)).toEqual(result);
+ });
+ it('should return profile info from id token', () => {
+ jwtMock.mockReturnValueOnce({
+ email: 'email',
+ picture: 'picture',
+ name: 'displayName',
+ });
+ const profile = {
+ name: {
+ familyName: 'familyName',
+ givenName: 'givenName',
+ },
+ } as PassportProfile;
+ const idToken = 'idToken';
+ const result = {
+ email: 'email',
+ picture: 'picture',
+ displayName: 'displayName',
+ };
+ expect(makeProfileInfo(profile, idToken)).toEqual(result);
+ });
+});
diff --git a/plugins/auth-backend-module-aws-alb-provider/src/helpers.ts b/plugins/auth-backend-module-aws-alb-provider/src/helpers.ts
new file mode 100644
index 0000000000..d97146bb93
--- /dev/null
+++ b/plugins/auth-backend-module-aws-alb-provider/src/helpers.ts
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import type { PassportProfile } from '@backstage/plugin-auth-node/';
+import { ProfileInfo } from '@backstage/plugin-auth-node';
+import { KeyObject } from 'crypto';
+import jwtDecoder from 'jwt-decode';
+import NodeCache from 'node-cache';
+import * as crypto from 'crypto';
+import { JWTHeaderParameters } from 'jose';
+import { AuthenticationError } from '@backstage/errors';
+
+export const makeProfileInfo = (
+ profile: PassportProfile,
+ idToken?: string,
+): ProfileInfo => {
+ let email: string | undefined = undefined;
+ if (profile.emails && profile.emails.length > 0) {
+ const [firstEmail] = profile.emails;
+ email = firstEmail.value;
+ }
+
+ let picture: string | undefined = undefined;
+ if (profile.avatarUrl) {
+ picture = profile.avatarUrl;
+ } else if (profile.photos && profile.photos.length > 0) {
+ const [firstPhoto] = profile.photos;
+ picture = firstPhoto.value;
+ }
+
+ let displayName: string | undefined =
+ profile.displayName ?? profile.username ?? profile.id;
+
+ if ((!email || !picture || !displayName) && idToken) {
+ try {
+ const decoded: Record = jwtDecoder(idToken);
+ if (!email && decoded.email) {
+ email = decoded.email;
+ }
+ if (!picture && decoded.picture) {
+ picture = decoded.picture;
+ }
+ if (!displayName && decoded.name) {
+ displayName = decoded.name;
+ }
+ } catch (e) {
+ throw new Error(`Failed to parse id token and get profile info, ${e}`);
+ }
+ }
+
+ return {
+ email,
+ picture,
+ displayName,
+ };
+};
+
+export const provisionKeyCache = (region: string, keyCache: NodeCache) => {
+ return async (header: JWTHeaderParameters): Promise => {
+ if (!header.kid) {
+ throw new AuthenticationError('No key id was specified in header');
+ }
+ const optionalCacheKey = keyCache.get(header.kid);
+ if (optionalCacheKey) {
+ return crypto.createPublicKey(optionalCacheKey);
+ }
+ const keyText: string = await fetch(
+ `https://public-keys.auth.elb.${encodeURIComponent(
+ region,
+ )}.amazonaws.com/${encodeURIComponent(header.kid)}`,
+ ).then(response => response.text());
+
+ const keyValue = crypto.createPublicKey(keyText);
+ keyCache.set(header.kid, keyValue.export({ format: 'pem', type: 'spki' }));
+ return keyValue;
+ };
+};
diff --git a/plugins/auth-backend-module-aws-alb-provider/src/index.ts b/plugins/auth-backend-module-aws-alb-provider/src/index.ts
new file mode 100644
index 0000000000..779f558f54
--- /dev/null
+++ b/plugins/auth-backend-module-aws-alb-provider/src/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * The auth-backend-module-aws-alb-provider backend module for the auth-backend plugin.
+ *
+ * @packageDocumentation
+ */
+
+export { awsAlbAuthenticator } from './authenticator';
+export { authModuleAwsAlbProvider } from './module';
+export { awsAlbSignInResolvers } from './resolvers';
+export { type AwsAlbResult } from './types';
diff --git a/plugins/auth-backend-module-aws-alb-provider/src/module.ts b/plugins/auth-backend-module-aws-alb-provider/src/module.ts
new file mode 100644
index 0000000000..65289bce10
--- /dev/null
+++ b/plugins/auth-backend-module-aws-alb-provider/src/module.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { createBackendModule } from '@backstage/backend-plugin-api';
+import {
+ authProvidersExtensionPoint,
+ commonSignInResolvers,
+ createProxyAuthProviderFactory,
+} from '@backstage/plugin-auth-node';
+import { awsAlbAuthenticator } from './authenticator';
+import { awsAlbSignInResolvers } from './resolvers';
+
+/** @public */
+export const authModuleAwsAlbProvider = createBackendModule({
+ pluginId: 'auth',
+ moduleId: 'awsAlbProvider',
+ register(reg) {
+ reg.registerInit({
+ deps: {
+ providers: authProvidersExtensionPoint,
+ },
+ async init({ providers }) {
+ providers.registerProvider({
+ providerId: 'awsalb',
+ factory: createProxyAuthProviderFactory({
+ authenticator: awsAlbAuthenticator,
+ signInResolverFactories: {
+ ...commonSignInResolvers,
+ ...awsAlbSignInResolvers,
+ },
+ }),
+ });
+ },
+ });
+ },
+});
diff --git a/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts b/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts
new file mode 100644
index 0000000000..602498ed57
--- /dev/null
+++ b/plugins/auth-backend-module-aws-alb-provider/src/resolvers.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ createSignInResolverFactory,
+ SignInInfo,
+} from '@backstage/plugin-auth-node';
+import { AwsAlbResult } from './types';
+/**
+ * Available sign-in resolvers for the Google auth provider.
+ *
+ * @public
+ */
+export namespace awsAlbSignInResolvers {
+ export const emailMatchingUserEntityProfileEmail =
+ createSignInResolverFactory({
+ create() {
+ return async (info: SignInInfo, ctx) => {
+ if (!info.result.fullProfile.emails) {
+ throw new Error(
+ 'Login failed, user profile does not contain an email',
+ );
+ }
+ return ctx.signInWithCatalogUser({
+ filter: {
+ kind: ['User'],
+ 'spec.profile.email': info.result.fullProfile.emails[0].value,
+ },
+ });
+ };
+ },
+ });
+}
diff --git a/plugins/auth-backend-module-aws-alb-provider/src/types.ts b/plugins/auth-backend-module-aws-alb-provider/src/types.ts
new file mode 100644
index 0000000000..c45640851c
--- /dev/null
+++ b/plugins/auth-backend-module-aws-alb-provider/src/types.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { PassportProfile } from '@backstage/plugin-auth-node/';
+/**
+ * JWT header extraction result, containing the raw value and the parsed JWT
+ * payload.
+ *
+ * @public
+ */
+export type AwsAlbResult = {
+ fullProfile: PassportProfile;
+ expiresInSeconds?: number;
+ accessToken: string;
+};
+/**
+ * @public
+ */
+export type AwsAlbClaims = {
+ sub: string;
+ name: string;
+ family_name: string;
+ given_name: string;
+ picture: string;
+ email: string;
+ exp: number;
+ iss: string;
+};
diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md
index ef7b7909ba..40e5facbbd 100644
--- a/plugins/auth-backend/api-report.md
+++ b/plugins/auth-backend/api-report.md
@@ -8,6 +8,7 @@ import { AuthProviderFactory as AuthProviderFactory_2 } from '@backstage/plugin-
import { AuthProviderRouteHandlers as AuthProviderRouteHandlers_2 } from '@backstage/plugin-auth-node';
import { AuthResolverCatalogUserQuery as AuthResolverCatalogUserQuery_2 } from '@backstage/plugin-auth-node';
import { AuthResolverContext as AuthResolverContext_2 } from '@backstage/plugin-auth-node';
+import { AwsAlbResult as AwsAlbResult_2 } from '@backstage/plugin-auth-backend-module-aws-alb-provider';
import { BackendFeature } from '@backstage/backend-plugin-api';
import { BackstageSignInResult } from '@backstage/plugin-auth-node';
import { CacheService } from '@backstage/backend-plugin-api';
@@ -71,12 +72,8 @@ export type AuthResolverContext = AuthResolverContext_2;
// @public @deprecated (undocumented)
export type AuthResponse = ClientAuthResponse;
-// @public (undocumented)
-export type AwsAlbResult = {
- fullProfile: Profile;
- expiresInSeconds?: number;
- accessToken: string;
-};
+// @public @deprecated
+export type AwsAlbResult = AwsAlbResult_2;
// @public (undocumented)
export type BitbucketOAuthResult = {
@@ -396,9 +393,9 @@ export const providers: Readonly<{
create: (
options?:
| {
- authHandler?: AuthHandler | undefined;
+ authHandler?: AuthHandler | undefined;
signIn: {
- resolver: SignInResolver;
+ resolver: SignInResolver;
};
}
| undefined,
diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json
index 16445713dc..29c34b84e1 100644
--- a/plugins/auth-backend/package.json
+++ b/plugins/auth-backend/package.json
@@ -39,6 +39,7 @@
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-auth-backend-module-atlassian-provider": "workspace:^",
+ "@backstage/plugin-auth-backend-module-aws-alb-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-gcp-iap-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-github-provider": "workspace:^",
"@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^",
diff --git a/plugins/auth-backend/src/providers/aws-alb/index.ts b/plugins/auth-backend/src/providers/aws-alb/index.ts
index 9b3e8920ac..6784888b1f 100644
--- a/plugins/auth-backend/src/providers/aws-alb/index.ts
+++ b/plugins/auth-backend/src/providers/aws-alb/index.ts
@@ -15,4 +15,4 @@
*/
export { awsAlb } from './provider';
-export type { AwsAlbResult } from './provider';
+export type { AwsAlbResult } from './types';
diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts
deleted file mode 100644
index 6ba545191b..0000000000
--- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import express from 'express';
-import { jwtVerify } from 'jose';
-import {
- ALB_ACCESS_TOKEN_HEADER,
- ALB_JWT_HEADER,
- AwsAlbAuthProvider,
-} from './provider';
-import { makeProfileInfo } from '../../lib/passport';
-import { AuthResolverContext } from '../types';
-import { AuthenticationError } from '@backstage/errors';
-
-const jwtMock = jwtVerify as jest.Mocked;
-
-const mockKey = async () => {
- return `-----BEGIN PUBLIC KEY-----
-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnuN4LlaJhaUpx+qZFTzYCrSBLk0I
-yOlxJ2VW88mLAQGJ7HPAvOdylxZsItMnzCuqNzZvie8m/NJsOjhDncVkrw==
------END PUBLIC KEY-----
-`;
-};
-const mockJwt =
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IktFWV9JRCIsImlzcyI6IklTU1VFUl9VUkwifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlVzZXIgTmFtZSIsImlhdCI6MTUxNjIzOTAyMn0.uMCSBGhij1xn5pnot8XgD-huQuTIBOFGs6kkW_p_X94';
-const mockAccessToken = 'ACCESS_TOKEN';
-const mockClaims = {
- sub: '1234567890',
- name: 'User Name',
- family_name: 'Name',
- given_name: 'User',
- picture: 'PICTURE_URL',
- email: 'user.name@email.test',
- exp: 1632833763,
- iss: 'ISSUER_URL',
-};
-
-jest.mock('jose');
-jest.mock('node-fetch', () => ({
- __esModule: true,
- default: async () => {
- return {
- text: async () => {
- return mockKey();
- },
- };
- },
-}));
-
-beforeEach(() => {
- jest.clearAllMocks();
-});
-
-describe('AwsAlbAuthProvider', () => {
- const mockRequest = {
- header: jest.fn(name => {
- if (name === ALB_JWT_HEADER) {
- return mockJwt;
- } else if (name === ALB_ACCESS_TOKEN_HEADER) {
- return mockAccessToken;
- }
- return undefined;
- }),
- } as unknown as express.Request;
- const mockRequestWithoutJwt = {
- header: jest.fn(name => {
- if (name === ALB_ACCESS_TOKEN_HEADER) {
- return mockAccessToken;
- }
- return undefined;
- }),
- } as unknown as express.Request;
- const mockRequestWithoutAccessToken = {
- header: jest.fn(name => {
- if (name === ALB_JWT_HEADER) {
- return mockJwt;
- }
- return undefined;
- }),
- } as unknown as express.Request;
-
- const mockResponse = {
- end: jest.fn(),
- header: () => jest.fn(),
- json: jest.fn().mockReturnThis(),
- status: jest.fn(),
- } as unknown as express.Response;
-
- describe('should transform to type AwsAlbResponse', () => {
- it('when JWT is valid and identity is resolved successfully', async () => {
- const provider = new AwsAlbAuthProvider({
- region: 'eu-west-1',
- issuer: 'ISSUER_URL',
- resolverContext: {} as AuthResolverContext,
- authHandler: async ({ fullProfile }) => ({
- profile: makeProfileInfo(fullProfile),
- }),
- signInResolver: async () => {
- return {
- token:
- 'eyblob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iLCJlbnQiOlsidXNlcjpkZWZhdWx0L2ppbW15bWFya3VtIl19.eyblob',
- };
- },
- });
-
- jwtMock.mockReturnValueOnce(Promise.resolve({ payload: mockClaims }));
-
- await provider.refresh(mockRequest, mockResponse);
-
- expect(mockResponse.json).toHaveBeenCalledWith({
- backstageIdentity: {
- token:
- 'eyblob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iLCJlbnQiOlsidXNlcjpkZWZhdWx0L2ppbW15bWFya3VtIl19.eyblob',
- identity: {
- ownershipEntityRefs: ['user:default/jimmymarkum'],
- type: 'user',
- userEntityRef: 'user:default/jimmymarkum',
- },
- },
- profile: {
- displayName: 'User Name',
- email: 'user.name@email.test',
- picture: 'PICTURE_URL',
- },
- providerInfo: {
- accessToken: mockAccessToken,
- expiresInSeconds: mockClaims.exp,
- },
- });
- });
- });
-
- describe('should fail when', () => {
- it('Access token is missing', async () => {
- const provider = new AwsAlbAuthProvider({
- region: 'eu-west-1',
- issuer: 'ISSUER_URL',
- resolverContext: {} as AuthResolverContext,
- authHandler: async ({ fullProfile }) => ({
- profile: makeProfileInfo(fullProfile),
- }),
- signInResolver: async () => {
- return { id: 'user.name', token: 'TOKEN' };
- },
- });
-
- await expect(
- provider.refresh(mockRequestWithoutAccessToken, mockResponse),
- ).rejects.toThrow(AuthenticationError);
- });
-
- it('JWT is missing', async () => {
- const provider = new AwsAlbAuthProvider({
- region: 'eu-west-1',
- issuer: 'ISSUER_URL',
- resolverContext: {} as AuthResolverContext,
- authHandler: async ({ fullProfile }) => ({
- profile: makeProfileInfo(fullProfile),
- }),
- signInResolver: async () => {
- return { id: 'user.name', token: 'TOKEN' };
- },
- });
-
- await expect(
- provider.refresh(mockRequestWithoutJwt, mockResponse),
- ).rejects.toThrow(AuthenticationError);
- });
-
- it('JWT is invalid', async () => {
- const provider = new AwsAlbAuthProvider({
- region: 'eu-west-1',
- issuer: 'ISSUER_URL',
- resolverContext: {} as AuthResolverContext,
- authHandler: async ({ fullProfile }) => ({
- profile: makeProfileInfo(fullProfile),
- }),
- signInResolver: async () => {
- return { id: 'user.name', token: 'TOKEN' };
- },
- });
-
- jwtMock.mockImplementationOnce(() => {
- throw new Error('bad JWT');
- });
-
- await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow(
- AuthenticationError,
- );
- });
-
- it('issuer is missing', async () => {
- const provider = new AwsAlbAuthProvider({
- region: 'eu-west-1',
- issuer: 'ISSUER_URL',
- resolverContext: {} as AuthResolverContext,
- authHandler: async ({ fullProfile }) => ({
- profile: makeProfileInfo(fullProfile),
- }),
- signInResolver: async () => {
- return { id: 'user.name', token: 'TOKEN' };
- },
- });
-
- jwtMock.mockReturnValueOnce({});
-
- await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow(
- AuthenticationError,
- );
- });
-
- it('issuer is invalid', async () => {
- const provider = new AwsAlbAuthProvider({
- region: 'eu-west-1',
- issuer: 'ISSUER_URL',
- resolverContext: {} as AuthResolverContext,
- authHandler: async ({ fullProfile }) => ({
- profile: makeProfileInfo(fullProfile),
- }),
- signInResolver: async () => {
- return { id: 'user.name', token: 'TOKEN' };
- },
- });
-
- jwtMock.mockReturnValueOnce({
- iss: 'INVALID_ISSUE_URL',
- });
-
- await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow(
- AuthenticationError,
- );
- });
-
- it('SignInResolver rejects', async () => {
- const provider = new AwsAlbAuthProvider({
- region: 'eu-west-1',
- issuer: 'ISSUER_URL',
- resolverContext: {} as AuthResolverContext,
- authHandler: async ({ fullProfile }) => ({
- profile: makeProfileInfo(fullProfile),
- }),
- signInResolver: async () => {
- throw new Error();
- },
- });
-
- jwtMock.mockReturnValueOnce(mockClaims);
-
- await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow(
- AuthenticationError,
- );
- });
-
- it('AuthHandler rejects', async () => {
- const provider = new AwsAlbAuthProvider({
- region: 'eu-west-1',
- issuer: 'ISSUER_URL',
- resolverContext: {} as AuthResolverContext,
- authHandler: async () => {
- throw new Error();
- },
- signInResolver: async () => {
- return { id: 'user.name', token: 'TOKEN' };
- },
- });
-
- jwtMock.mockReturnValueOnce(mockClaims);
-
- await expect(provider.refresh(mockRequest, mockResponse)).rejects.toThrow(
- AuthenticationError,
- );
- });
- });
-});
diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts
index 5af606539e..3883bbc88c 100644
--- a/plugins/auth-backend/src/providers/aws-alb/provider.ts
+++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts
@@ -15,202 +15,13 @@
*/
import {
- AuthHandler,
- AuthProviderRouteHandlers,
- AuthResolverContext,
- AuthResponse,
- SignInResolver,
-} from '../types';
-import express from 'express';
-import fetch from 'node-fetch';
-import * as crypto from 'crypto';
-import { KeyObject } from 'crypto';
-import NodeCache from 'node-cache';
-import { JWTHeaderParameters, jwtVerify } from 'jose';
-import { Profile as PassportProfile } from 'passport';
-import { makeProfileInfo } from '../../lib/passport';
-import { AuthenticationError } from '@backstage/errors';
-import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse';
+ AwsAlbResult,
+ awsAlbAuthenticator,
+} from '@backstage/plugin-auth-backend-module-aws-alb-provider';
+import { createProxyAuthProviderFactory } from '@backstage/plugin-auth-node';
+import { AuthHandler, SignInResolver } from '../types';
import { createAuthProviderIntegration } from '../createAuthProviderIntegration';
-export const ALB_JWT_HEADER = 'x-amzn-oidc-data';
-export const ALB_ACCESS_TOKEN_HEADER = 'x-amzn-oidc-accesstoken';
-
-type Options = {
- region: string;
- issuer?: string;
- authHandler: AuthHandler;
- signInResolver: SignInResolver;
- resolverContext: AuthResolverContext;
-};
-
-export type AwsAlbHeaders = {
- alg: string;
- kid: string;
- signer: string;
- iss: string;
- client: string;
- exp: number;
-};
-
-export type AwsAlbClaims = {
- sub: string;
- name: string;
- family_name: string;
- given_name: string;
- picture: string;
- email: string;
- exp: number;
- iss: string;
-};
-
-/** @public */
-export type AwsAlbResult = {
- fullProfile: PassportProfile;
- expiresInSeconds?: number;
- accessToken: string;
-};
-
-export type AwsAlbProviderInfo = {
- /**
- * An access token issued for the signed in user.
- */
- accessToken: string;
- /**
- * Expiry of the access token in seconds.
- */
- expiresInSeconds?: number;
-};
-
-export type AwsAlbResponse = AuthResponse;
-
-export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
- private readonly region: string;
- private readonly issuer?: string;
- private readonly resolverContext: AuthResolverContext;
- private readonly keyCache: NodeCache;
- private readonly authHandler: AuthHandler;
- private readonly signInResolver: SignInResolver;
-
- constructor(options: Options) {
- this.region = options.region;
- this.issuer = options.issuer;
- this.authHandler = options.authHandler;
- this.signInResolver = options.signInResolver;
- this.resolverContext = options.resolverContext;
- this.keyCache = new NodeCache({ stdTTL: 3600 });
- }
-
- frameHandler(): Promise {
- return Promise.resolve(undefined);
- }
-
- async refresh(req: express.Request, res: express.Response): Promise {
- try {
- const result = await this.getResult(req);
- const response = await this.handleResult(result);
- res.json(response);
- } catch (e) {
- throw new AuthenticationError(
- 'Exception occurred during AWS ALB token refresh',
- e,
- );
- }
- }
-
- start(): Promise {
- return Promise.resolve(undefined);
- }
-
- private async getResult(req: express.Request): Promise {
- const jwt = req.header(ALB_JWT_HEADER);
- const accessToken = req.header(ALB_ACCESS_TOKEN_HEADER);
-
- if (jwt === undefined) {
- throw new AuthenticationError(
- `Missing ALB OIDC header: ${ALB_JWT_HEADER}`,
- );
- }
-
- if (accessToken === undefined) {
- throw new AuthenticationError(
- `Missing ALB OIDC header: ${ALB_ACCESS_TOKEN_HEADER}`,
- );
- }
-
- try {
- const verifyResult = await jwtVerify(jwt, this.getKey);
- const claims = verifyResult.payload as AwsAlbClaims;
-
- if (this.issuer && claims.iss !== this.issuer) {
- throw new AuthenticationError('Issuer mismatch on JWT token');
- }
-
- const fullProfile: PassportProfile = {
- provider: 'unknown',
- id: claims.sub,
- displayName: claims.name,
- username: claims.email.split('@')[0].toLowerCase(),
- name: {
- familyName: claims.family_name,
- givenName: claims.given_name,
- },
- emails: [{ value: claims.email.toLowerCase() }],
- photos: [{ value: claims.picture }],
- };
-
- return {
- fullProfile,
- expiresInSeconds: claims.exp,
- accessToken,
- };
- } catch (e) {
- throw new Error(`Exception occurred during JWT processing: ${e}`);
- }
- }
-
- private async handleResult(result: AwsAlbResult): Promise {
- const { profile } = await this.authHandler(result, this.resolverContext);
- const backstageIdentity = await this.signInResolver(
- {
- result,
- profile,
- },
- this.resolverContext,
- );
-
- return {
- providerInfo: {
- accessToken: result.accessToken,
- expiresInSeconds: result.expiresInSeconds,
- },
- backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity),
- profile,
- };
- }
-
- getKey = async (header: JWTHeaderParameters): Promise => {
- if (!header.kid) {
- throw new AuthenticationError('No key id was specified in header');
- }
- const optionalCacheKey = this.keyCache.get(header.kid);
- if (optionalCacheKey) {
- return crypto.createPublicKey(optionalCacheKey);
- }
- const keyText: string = await fetch(
- `https://public-keys.auth.elb.${encodeURIComponent(
- this.region,
- )}.amazonaws.com/${encodeURIComponent(header.kid)}`,
- ).then(response => response.text());
- const keyValue = crypto.createPublicKey(keyText);
- this.keyCache.set(
- header.kid,
- keyValue.export({ format: 'pem', type: 'spki' }),
- );
- return keyValue;
- };
-}
-
/**
* Auth provider integration for AWS ALB auth
*
@@ -219,13 +30,14 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
export const awsAlb = createAuthProviderIntegration({
create(options?: {
/**
- * The profile transformation function used to verify and convert the auth response
- * into the profile that will be presented to the user.
+ * The profile transformation function used to verify and convert the auth
+ * response into the profile that will be presented to the user. The default
+ * implementation just provides the authenticated email that the IAP
+ * presented.
*/
authHandler?: AuthHandler;
-
/**
- * Configure sign-in for this provider, without it the provider can not be used to sign users in.
+ * Configures sign-in for this provider.
*/
signIn: {
/**
@@ -234,29 +46,10 @@ export const awsAlb = createAuthProviderIntegration({
resolver: SignInResolver;
};
}) {
- return ({ config, resolverContext }) => {
- const region = config.getString('region');
- const issuer = config.getOptionalString('iss');
-
- if (options?.signIn.resolver === undefined) {
- throw new Error(
- 'SignInResolver is required to use this authentication provider',
- );
- }
-
- const authHandler: AuthHandler = options?.authHandler
- ? options.authHandler
- : async ({ fullProfile }) => ({
- profile: makeProfileInfo(fullProfile),
- });
-
- return new AwsAlbAuthProvider({
- region,
- issuer,
- signInResolver: options?.signIn.resolver,
- authHandler,
- resolverContext,
- });
- };
+ return createProxyAuthProviderFactory({
+ authenticator: awsAlbAuthenticator,
+ profileTransform: options?.authHandler,
+ signInResolver: options?.signIn?.resolver,
+ });
},
});
diff --git a/plugins/auth-backend/src/providers/aws-alb/types.ts b/plugins/auth-backend/src/providers/aws-alb/types.ts
new file mode 100644
index 0000000000..2640a4a7be
--- /dev/null
+++ b/plugins/auth-backend/src/providers/aws-alb/types.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AwsAlbResult as _AwsAlbResult } from '@backstage/plugin-auth-backend-module-aws-alb-provider';
+
+/**
+ * The result of the initial auth challenge. This is the input to the auth
+ * callbacks.
+ *
+ * @public
+ * @deprecated import from `@backstage/plugin-auth-backend-module-aws-alb-provider` instead
+ */
+export type AwsAlbResult = _AwsAlbResult;
diff --git a/yarn.lock b/yarn.lock
index 0e3d81bc7f..a055d7fd6d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4591,6 +4591,25 @@ __metadata:
languageName: unknown
linkType: soft
+"@backstage/plugin-auth-backend-module-aws-alb-provider@workspace:^, @backstage/plugin-auth-backend-module-aws-alb-provider@workspace:plugins/auth-backend-module-aws-alb-provider":
+ version: 0.0.0-use.local
+ resolution: "@backstage/plugin-auth-backend-module-aws-alb-provider@workspace:plugins/auth-backend-module-aws-alb-provider"
+ dependencies:
+ "@backstage/backend-common": "workspace:^"
+ "@backstage/backend-plugin-api": "workspace:^"
+ "@backstage/backend-test-utils": "workspace:^"
+ "@backstage/cli": "workspace:^"
+ "@backstage/config": "workspace:^"
+ "@backstage/errors": "workspace:^"
+ "@backstage/plugin-auth-backend": "workspace:^"
+ "@backstage/plugin-auth-node": "workspace:^"
+ express: ^4.18.2
+ jose: ^4.6.0
+ jwt-decode: ^3.1.0
+ node-cache: ^5.1.2
+ languageName: unknown
+ linkType: soft
+
"@backstage/plugin-auth-backend-module-gcp-iap-provider@workspace:^, @backstage/plugin-auth-backend-module-gcp-iap-provider@workspace:plugins/auth-backend-module-gcp-iap-provider":
version: 0.0.0-use.local
resolution: "@backstage/plugin-auth-backend-module-gcp-iap-provider@workspace:plugins/auth-backend-module-gcp-iap-provider"
@@ -4805,6 +4824,7 @@ __metadata:
"@backstage/config": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-auth-backend-module-atlassian-provider": "workspace:^"
+ "@backstage/plugin-auth-backend-module-aws-alb-provider": "workspace:^"
"@backstage/plugin-auth-backend-module-gcp-iap-provider": "workspace:^"
"@backstage/plugin-auth-backend-module-github-provider": "workspace:^"
"@backstage/plugin-auth-backend-module-gitlab-provider": "workspace:^"