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:^"