From 079fdc22523f288727cdd3a54c69939decd3a2e0 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Wed, 15 Nov 2023 18:43:20 -0500 Subject: [PATCH 01/19] feat: Add VMWare CSP auth backend module provider Signed-off-by: Carlos Esteban Lopez --- .../.eslintrc.js | 1 + .../README.md | 7 + .../config.d.ts | 31 +++ .../dev/index.ts | 24 ++ .../package.json | 44 ++++ .../src/authenticator.ts | 221 ++++++++++++++++++ .../src/index.ts | 25 ++ .../src/module.ts | 46 ++++ .../src/resolvers.ts | 75 ++++++ yarn.lock | 19 ++ 10 files changed, 493 insertions(+) create mode 100644 plugins/auth-backend-module-vmware-csp-provider/.eslintrc.js create mode 100644 plugins/auth-backend-module-vmware-csp-provider/README.md create mode 100644 plugins/auth-backend-module-vmware-csp-provider/config.d.ts create mode 100644 plugins/auth-backend-module-vmware-csp-provider/dev/index.ts create mode 100644 plugins/auth-backend-module-vmware-csp-provider/package.json create mode 100644 plugins/auth-backend-module-vmware-csp-provider/src/authenticator.ts create mode 100644 plugins/auth-backend-module-vmware-csp-provider/src/index.ts create mode 100644 plugins/auth-backend-module-vmware-csp-provider/src/module.ts create mode 100644 plugins/auth-backend-module-vmware-csp-provider/src/resolvers.ts diff --git a/plugins/auth-backend-module-vmware-csp-provider/.eslintrc.js b/plugins/auth-backend-module-vmware-csp-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-vmware-csp-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-vmware-csp-provider/README.md b/plugins/auth-backend-module-vmware-csp-provider/README.md new file mode 100644 index 0000000000..75ee770828 --- /dev/null +++ b/plugins/auth-backend-module-vmware-csp-provider/README.md @@ -0,0 +1,7 @@ +# Auth Module: VMWare CSP Provider + +This module provides an VMWare CSP auth provider implementation for `@backstage/plugin-auth-backend`. + +## Links + +- [Backstage](https://backstage.io) diff --git a/plugins/auth-backend-module-vmware-csp-provider/config.d.ts b/plugins/auth-backend-module-vmware-csp-provider/config.d.ts new file mode 100644 index 0000000000..10b3566899 --- /dev/null +++ b/plugins/auth-backend-module-vmware-csp-provider/config.d.ts @@ -0,0 +1,31 @@ +/* + * 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. + */ + +export interface Config { + auth?: { + providers?: { + /** @visibility frontend */ + vmwareCloudServices?: { + [authEnv: string]: { + clientId: string; + organizationId: string; + scope?: string; + consoleEndpoint?: string; + }; + }; + }; + }; +} diff --git a/plugins/auth-backend-module-vmware-csp-provider/dev/index.ts b/plugins/auth-backend-module-vmware-csp-provider/dev/index.ts new file mode 100644 index 0000000000..d3c18c1d48 --- /dev/null +++ b/plugins/auth-backend-module-vmware-csp-provider/dev/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); + +backend.add(import('@backstage/plugin-auth-backend')); +backend.add(import('../src')); + +backend.start(); diff --git a/plugins/auth-backend-module-vmware-csp-provider/package.json b/plugins/auth-backend-module-vmware-csp-provider/package.json new file mode 100644 index 0000000000..b52d62cfca --- /dev/null +++ b/plugins/auth-backend-module-vmware-csp-provider/package.json @@ -0,0 +1,44 @@ +{ + "name": "@backstage/plugin-auth-backend-module-vmware-csp-provider", + "description": "The vmware-csp-provider backend module for the auth plugin.", + "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" + }, + "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/catalog-model": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "jwt-decode": "^3.1.0", + "passport-oauth2": "^1.6.1" + }, + "devDependencies": { + "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-auth-backend": "workspace:^", + "@types/jwt-decode": "^3.1.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.ts b/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.ts new file mode 100644 index 0000000000..bd97111de6 --- /dev/null +++ b/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.ts @@ -0,0 +1,221 @@ +/* + * 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 { + createOAuthAuthenticator, + decodeOAuthState, + encodeOAuthState, + OAuthState, + PassportOAuthAuthenticatorHelper, + PassportOAuthDoneCallback, + PassportProfile, +} from '@backstage/plugin-auth-node'; +import jwtDecoder from 'jwt-decode'; +import { + Metadata, + StateStoreStoreCallback, + StateStoreVerifyCallback, + Strategy as OAuth2Strategy, +} from 'passport-oauth2'; + +export interface vmWareCSPAuthenticatorContext { + organizationId?: string; + providerStrategy: OAuth2Strategy; + helper: PassportOAuthAuthenticatorHelper; +} + +type CSPPassportProfile = PassportProfile & { + organizationId?: string; +}; + +/** + * VMWare CSP Authenticator to be used by `createOAuthProviderFactory` + */ +export const vmWareCSPAuthenticator = createOAuthAuthenticator< + vmWareCSPAuthenticatorContext, + CSPPassportProfile +>({ + defaultProfileTransform: async input => { + if (!input.session.idToken) { + throw new Error( + `Failed to parse id token and get profile info, missing token from session`, + ); + } + + const identity: Record = jwtDecoder(input.session.idToken); + const missingClaims = [ + 'email', + 'given_name', + 'family_name', + 'context_name', + ].filter(key => !(key in identity)); + + if (missingClaims.length > 0) { + throw new Error( + `ID token missing required claims: ${missingClaims.join(', ')}`, + ); + } + + if (identity.context_name !== input.fullProfile.organizationId) { + throw new Error(`ID token organizationId mismatch`); + } + + return { + profile: { + displayName: `${identity.given_name} ${identity.family_name}`, + email: identity.email, + }, + }; + }, + initialize({ callbackUrl, config }) { + const consoleEndpoint = + config.getOptionalString('consoleEndpoint') ?? + 'https://console.cloud.vmware.com'; + const organizationId = config.getString('organizationId'); + + const clientId = config.getString('clientId'); + const clientSecret = ''; + const authorizationUrl = `${consoleEndpoint}/csp/gateway/discovery`; + const tokenUrl = `${consoleEndpoint}/csp/gateway/am/api/auth/token`; + const scope = config.getOptionalString('scope') ?? 'openid offline_access'; + + const providerStrategy = new OAuth2Strategy( + { + clientID: clientId, + clientSecret: clientSecret, + callbackURL: callbackUrl, + authorizationURL: authorizationUrl, + tokenURL: tokenUrl, + passReqToCallback: false, + pkce: true, + state: true, + scope: scope, + customHeaders: { + Authorization: `Basic ${encodeClientCredentials( + clientId, + clientSecret, + )}`, + }, + }, + ( + accessToken: any, + refreshToken: any, + params: any, + fullProfile: PassportProfile, + done: PassportOAuthDoneCallback, + ) => { + done(undefined, { fullProfile, params, accessToken }, { refreshToken }); + }, + ); + + // Both VMWare & OAuth2Strategy fight over control of the state when PKCE is on, thus this hack + const pkceSessionStore = Object.create( + (providerStrategy as any)._stateStore, + ); + (providerStrategy as any)._stateStore = { + verify(req: Request, state: string, callback: StateStoreVerifyCallback) { + pkceSessionStore.verify( + req, + (decodeOAuthState(state) as any).handle, + callback, + ); + }, + store( + req: Request & { + scope: string; + state: OAuthState; + }, + verifier: string, + state: any, + meta: Metadata, + callback: StateStoreStoreCallback, + ) { + pkceSessionStore.store( + req, + verifier, + state, + meta, + (err: Error, handle: string) => { + callback( + err, + encodeOAuthState({ + handle, + ...state, + ...req.state, + } as OAuthState), + ); + }, + ); + }, + }; + + return { + organizationId, + providerStrategy, + helper: PassportOAuthAuthenticatorHelper.from(providerStrategy), + }; + }, + + async start(input, ctx) { + return new Promise((resolve, reject) => { + const strategy: OAuth2Strategy = Object.create(ctx.providerStrategy); + + strategy.redirect = (url: string, status?: number) => { + const parsed = new URL(url); + if (ctx.organizationId) { + parsed.searchParams.set('orgId', ctx.organizationId); + } + resolve({ url: parsed.toString(), status: status ?? undefined }); + }; + strategy.error = (error: Error) => { + reject(error); + }; + strategy.authenticate(input.req, { + scope: input.scope, + state: decodeOAuthState(input.state), + accessType: 'offline', + prompt: 'consent', + }); + }); + }, + + async authenticate(input, ctx) { + return ctx.helper.authenticate(input).then(result => ({ + ...result, + fullProfile: { + ...result.fullProfile, + organizationId: ctx.organizationId, + } as CSPPassportProfile, + })); + }, + + async refresh(input, ctx) { + return ctx.helper.refresh(input).then(result => ({ + ...result, + fullProfile: { + ...result.fullProfile, + organizationId: ctx.organizationId, + } as CSPPassportProfile, + })); + }, +}); + +/** @private */ +function encodeClientCredentials( + clientID: string, + clientSecret: string, +): string { + return Buffer.from(`${clientID}:${clientSecret}`).toString('base64'); +} diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/index.ts b/plugins/auth-backend-module-vmware-csp-provider/src/index.ts new file mode 100644 index 0000000000..3a0770836a --- /dev/null +++ b/plugins/auth-backend-module-vmware-csp-provider/src/index.ts @@ -0,0 +1,25 @@ +/* + * 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 vmware-csp-provider backend module for the auth plugin. + * + * @packageDocumentation + */ + +export { vmWareCSPAuthenticator } from './authenticator'; +export { authModuleVmwareCspProvider as default } from './module'; +export { vmwareCSPSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/module.ts b/plugins/auth-backend-module-vmware-csp-provider/src/module.ts new file mode 100644 index 0000000000..a0dc7ef566 --- /dev/null +++ b/plugins/auth-backend-module-vmware-csp-provider/src/module.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 { createBackendModule } from '@backstage/backend-plugin-api'; +import { + authProvidersExtensionPoint, + commonSignInResolvers, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; + +import { vmWareCSPAuthenticator } from './authenticator'; +import { vmwareCSPSignInResolvers } from './resolvers'; + +export const authModuleVmwareCspProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'vmware-csp-provider', + register(reg) { + reg.registerInit({ + deps: { providers: authProvidersExtensionPoint }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'oauth2', + factory: createOAuthProviderFactory({ + authenticator: vmWareCSPAuthenticator, + signInResolverFactories: { + ...vmwareCSPSignInResolvers, + ...commonSignInResolvers, + }, + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/resolvers.ts b/plugins/auth-backend-module-vmware-csp-provider/src/resolvers.ts new file mode 100644 index 0000000000..7cbd0967c3 --- /dev/null +++ b/plugins/auth-backend-module-vmware-csp-provider/src/resolvers.ts @@ -0,0 +1,75 @@ +/* + * 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 { stringifyEntityRef } from '@backstage/catalog-model'; +import { NotFoundError } from '@backstage/errors'; +import { + createSignInResolverFactory, + OAuthAuthenticatorResult, + PassportProfile, + SignInInfo, +} from '@backstage/plugin-auth-node'; + +/** + * Available sign-in resolvers for the VMWare CSP auth provider. + * + * @public + */ +export namespace vmwareCSPSignInResolvers { + /** + * Looks up the user by matching their profile email to the entity's profile email. + * If that fails, sign in the user without associating with a catalog user. + */ + export const usernameMatchingUserEntityName = createSignInResolverFactory({ + create() { + return async ( + info: SignInInfo>, + ctx, + ) => { + const email = info.profile.email; + + if (!email) { + throw new Error( + 'VMware login failed, user profile does not contain an email', + ); + } + + const userEntityRef = stringifyEntityRef({ + kind: 'User', + name: email, + }); + + try { + // we await here so that signInWithCatalogUser throws in the current `try` + return await ctx.signInWithCatalogUser({ + filter: { + 'spec.profile.email': email, + }, + }); + } catch (e) { + if (!(e instanceof NotFoundError)) { + throw e; + } + return ctx.issueToken({ + claims: { + sub: userEntityRef, + ent: [userEntityRef], + }, + }); + } + }; + }, + }); +} diff --git a/yarn.lock b/yarn.lock index c9013217c7..f8569c5dc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4840,6 +4840,25 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-auth-backend-module-vmware-csp-provider@workspace:plugins/auth-backend-module-vmware-csp-provider": + version: 0.0.0-use.local + resolution: "@backstage/plugin-auth-backend-module-vmware-csp-provider@workspace:plugins/auth-backend-module-vmware-csp-provider" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-auth-backend": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" + "@types/jwt-decode": ^3.1.0 + jwt-decode: ^3.1.0 + passport-oauth2: ^1.6.1 + languageName: unknown + linkType: soft + "@backstage/plugin-auth-backend@workspace:^, @backstage/plugin-auth-backend@workspace:plugins/auth-backend": version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend@workspace:plugins/auth-backend" From ed02c69a3c3b18f4e1c6fd4dc65ddb12ba7399a9 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Wed, 15 Nov 2023 18:57:14 -0500 Subject: [PATCH 02/19] chore: Add changeset Signed-off-by: Carlos Esteban Lopez --- .changeset/empty-fireants-study.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/empty-fireants-study.md diff --git a/.changeset/empty-fireants-study.md b/.changeset/empty-fireants-study.md new file mode 100644 index 0000000000..4316eaffdf --- /dev/null +++ b/.changeset/empty-fireants-study.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-vmware-csp-provider': patch +--- + +Add VMWare CSP (Cloud Service Provider) auth backend module provider From dc69df8d736f67114e431f7f58f7a6707ff7fb23 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Thu, 16 Nov 2023 14:29:17 -0500 Subject: [PATCH 03/19] test: Add resolver tests & rename resolver Signed-off-by: Carlos Esteban Lopez --- .../src/resolvers.test.ts | 90 +++++++++++++++++++ .../src/resolvers.ts | 75 ++++++++-------- 2 files changed, 128 insertions(+), 37 deletions(-) create mode 100644 plugins/auth-backend-module-vmware-csp-provider/src/resolvers.test.ts diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/resolvers.test.ts b/plugins/auth-backend-module-vmware-csp-provider/src/resolvers.test.ts new file mode 100644 index 0000000000..9a9abd7260 --- /dev/null +++ b/plugins/auth-backend-module-vmware-csp-provider/src/resolvers.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { NotFoundError } from '@backstage/errors'; +import { + AuthResolverContext, + OAuthAuthenticatorResult, + PassportProfile, + SignInInfo, + SignInResolver, +} from '@backstage/plugin-auth-node'; + +import { vmwareCSPSignInResolvers } from './resolvers'; + +describe('CSPResolver', () => { + let resolverContext: jest.Mocked; + let signInInfo: SignInInfo>; + let cspResolver: SignInResolver>; + + beforeEach(() => { + resolverContext = { + issueToken: jest.fn().mockResolvedValue({ + token: 'defaultBackstageToken', + }), + findCatalogUser: jest.fn(), + signInWithCatalogUser: jest.fn().mockResolvedValue({ + token: 'backstageToken', + }), + }; + + signInInfo = { + result: {} as any, // Resolver doesn't care about the result object + profile: { + displayName: 'TestName', + email: 'user@example.com', + }, + }; + + cspResolver = + vmwareCSPSignInResolvers.profileEmailMatchingUserEntityEmail(); + }); + + it('looks up backstage identity by email', async () => { + const backstageIdentity = await cspResolver(signInInfo, resolverContext); + + expect(backstageIdentity.token).toBe('backstageToken'); + expect(resolverContext.signInWithCatalogUser).toHaveBeenCalledWith({ + filter: { + 'spec.profile.email': 'user@example.com', + }, + }); + }); + + it('returns "fake" backstage identity when no entity matches', async () => { + resolverContext.signInWithCatalogUser.mockRejectedValue( + new NotFoundError('User not found'), + ); + + const backstageIdentity = await cspResolver(signInInfo, resolverContext); + + expect(backstageIdentity.token).toBe('defaultBackstageToken'); + expect(resolverContext.issueToken).toHaveBeenCalledWith({ + claims: { + sub: 'user:default/user@example.com', + ent: ['user:default/user@example.com'], + }, + }); + }); + + it('fails when resolver context throws other error', () => { + const error = new Error('bizarre'); + resolverContext.signInWithCatalogUser.mockRejectedValue(error); + + return expect(cspResolver(signInInfo, resolverContext)).rejects.toThrow( + error, + ); + }); +}); diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/resolvers.ts b/plugins/auth-backend-module-vmware-csp-provider/src/resolvers.ts index 7cbd0967c3..249f4f8abc 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-vmware-csp-provider/src/resolvers.ts @@ -32,44 +32,45 @@ export namespace vmwareCSPSignInResolvers { * Looks up the user by matching their profile email to the entity's profile email. * If that fails, sign in the user without associating with a catalog user. */ - export const usernameMatchingUserEntityName = createSignInResolverFactory({ - create() { - return async ( - info: SignInInfo>, - ctx, - ) => { - const email = info.profile.email; + export const profileEmailMatchingUserEntityEmail = + createSignInResolverFactory({ + create() { + return async ( + info: SignInInfo>, + ctx, + ) => { + const email = info.profile.email; - if (!email) { - throw new Error( - 'VMware login failed, user profile does not contain an email', - ); - } - - const userEntityRef = stringifyEntityRef({ - kind: 'User', - name: email, - }); - - try { - // we await here so that signInWithCatalogUser throws in the current `try` - return await ctx.signInWithCatalogUser({ - filter: { - 'spec.profile.email': email, - }, - }); - } catch (e) { - if (!(e instanceof NotFoundError)) { - throw e; + if (!email) { + throw new Error( + 'VMware login failed, user profile does not contain an email', + ); } - return ctx.issueToken({ - claims: { - sub: userEntityRef, - ent: [userEntityRef], - }, + + const userEntityRef = stringifyEntityRef({ + kind: 'User', + name: email, }); - } - }; - }, - }); + + try { + // we await here so that signInWithCatalogUser throws in the current `try` + return await ctx.signInWithCatalogUser({ + filter: { + 'spec.profile.email': email, + }, + }); + } catch (e) { + if (!(e instanceof NotFoundError)) { + throw e; + } + return ctx.issueToken({ + claims: { + sub: userEntityRef, + ent: [userEntityRef], + }, + }); + } + }; + }, + }); } From 28fe9841632f42c77e2accb00bdf3020b88dc9e6 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Fri, 17 Nov 2023 15:54:59 -0500 Subject: [PATCH 04/19] test: Add module test, fix providerId & remove jwt-decode dep Signed-off-by: Carlos Esteban Lopez --- .../package.json | 9 +- .../src/authenticator.test.ts | 473 ++++++++++++++++++ .../src/authenticator.ts | 6 +- .../src/module.test.ts | 243 +++++++++ .../src/module.ts | 2 +- yarn.lock | 140 +++++- 6 files changed, 864 insertions(+), 9 deletions(-) create mode 100644 plugins/auth-backend-module-vmware-csp-provider/src/authenticator.test.ts create mode 100644 plugins/auth-backend-module-vmware-csp-provider/src/module.test.ts diff --git a/plugins/auth-backend-module-vmware-csp-provider/package.json b/plugins/auth-backend-module-vmware-csp-provider/package.json index b52d62cfca..7d7b49fc72 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/package.json +++ b/plugins/auth-backend-module-vmware-csp-provider/package.json @@ -28,15 +28,20 @@ "@backstage/catalog-model": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", - "jwt-decode": "^3.1.0", + "jose": "^4.6.0", "passport-oauth2": "^1.6.1" }, "devDependencies": { + "@backstage/backend-app-api": "workspace:^", "@backstage/backend-defaults": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/config": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", - "@types/jwt-decode": "^3.1.0" + "express": "^4.18.2", + "express-session": "^1.17.3", + "msw": "^2.0.8", + "supertest": "^6.3.3" }, "files": [ "dist" diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.test.ts b/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.test.ts new file mode 100644 index 0000000000..8421c82db7 --- /dev/null +++ b/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.test.ts @@ -0,0 +1,473 @@ +/* + * 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 { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { + AuthResolverContext, + encodeOAuthState, + OAuthAuthenticatorAuthenticateInput, + OAuthAuthenticatorRefreshInput, + OAuthAuthenticatorStartInput, + OAuthState, +} from '@backstage/plugin-auth-node'; +import { SignJWT } from 'jose'; +import { rest } from 'msw'; +import { setupServer } from 'msw'; + +import { + vmWareCSPAuthenticator, + vmWareCSPAuthenticatorContext, +} from './authenticator'; + +jest.mock('uid2', () => jest.fn().mockReturnValue('sessionid')); + +describe('VMwareCloudServicesAuthenticator', () => { + const server = setupServer(); + setupRequestMockHandlers(server); + + let oAuthState: OAuthState = { + nonce: 'nonce', + env: 'env', + }; + + const signInInfo: Record = { + given_name: 'Givenname', + family_name: 'Familyname', + context_name: 'orgId', + email: 'user@example.com', + }; + + let idToken: string; + + let authResponse: { + access_token: string; + refresh_token: string; + id_token: typeof idToken; + }; + + let fakeSession: Record; + let authenticatorCtx: vmWareCSPAuthenticatorContext; + + beforeAll(async () => { + idToken = await new SignJWT(signInInfo) + .setProtectedHeader({ alg: 'HS256' }) + .sign(Buffer.from('signing key')); + + authResponse = { + access_token: 'accessToken', + refresh_token: 'refreshToken', + id_token: idToken, + }; + }); + + beforeEach(() => { + server.use( + rest.post( + 'https://console.cloud.vmware.com/csp/gateway/am/api/auth/token', + (req, res, ctx) => + res( + req.headers.get('Authorization') + ? ctx.json(authResponse) + : ctx.status(401), + ), + ), + ); + + authenticatorCtx = vmWareCSPAuthenticator.initialize({ + callbackUrl: 'http://callbackUrl', + config: new ConfigReader({ + clientId: 'placeholderClientId', + organizationId: 'orgId', + }), + }); + }); + + describe('#initialize', () => { + it('fails when organizationId is not configured', () => { + return expect(() => + vmWareCSPAuthenticator.initialize({ + callbackUrl: 'http://callbackUrl', + config: new ConfigReader({ + clientId: 'placeholderClientId', + }), + }), + ).toThrow(`Missing required config value at 'organizationId'`); + }); + }); + + describe('#start', () => { + let startRequest: OAuthAuthenticatorStartInput; + + beforeEach(() => { + fakeSession = {}; + startRequest = { + state: encodeOAuthState(oAuthState), + req: { + query: {}, + session: fakeSession, + }, + } as OAuthAuthenticatorStartInput; + }); + + it('redirects to the Cloud Services Console consent page', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + startRequest, + authenticatorCtx, + ); + const url = new URL(startResponse.url); + + expect(url.protocol).toBe('https:'); + expect(url.hostname).toBe('console.cloud.vmware.com'); + expect(url.pathname).toBe('/csp/gateway/discovery'); + }); + + it('passes client ID from config', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + startRequest, + authenticatorCtx, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('client_id')).toBe('placeholderClientId'); + }); + + it('passes organizationId from config', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + startRequest, + authenticatorCtx, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('orgId')).toBe('orgId'); + }); + + it('passes callback URL', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + startRequest, + authenticatorCtx, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('redirect_uri')).toBe('http://callbackUrl'); + }); + + it('requests scopes for ID and refresh token', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + startRequest, + authenticatorCtx, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('scope')).toBe('openid offline_access'); + }); + + it('generates PKCE challenge', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + startRequest, + authenticatorCtx, + ); + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('code_challenge_method')).toBe('S256'); + expect(searchParams.get('code_challenge')).not.toBeNull(); + }); + + it('stores PKCE verifier in session', async () => { + await vmWareCSPAuthenticator.start(startRequest, authenticatorCtx); + + expect( + fakeSession['oauth2:console.cloud.vmware.com'].state.code_verifier, + ).toBeDefined(); + }); + + it('fails when request has no session', () => { + return expect( + vmWareCSPAuthenticator.start( + { + state: encodeOAuthState(oAuthState), + req: { + query: {}, + }, + } as OAuthAuthenticatorStartInput, + authenticatorCtx, + ), + ).rejects.toThrow('requires session support'); + }); + + it('adds session ID handle to state param', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + startRequest, + authenticatorCtx, + ); + const stateParam = new URL(startResponse.url).searchParams.get('state'); + + const state = Object.fromEntries( + new URLSearchParams(Buffer.from(stateParam!, 'hex').toString('utf-8')), + ); + + const { handle } = fakeSession['oauth2:console.cloud.vmware.com'].state; + expect(state.handle).toBe(handle); + }); + }); + + describe('#authenticate', () => { + let resolverContext: jest.Mocked; + let authenticateRequest: OAuthAuthenticatorAuthenticateInput; + + beforeEach(() => { + resolverContext = { + issueToken: jest.fn().mockResolvedValue({ + token: 'defaultBackstageToken', + }), + findCatalogUser: jest.fn(), + signInWithCatalogUser: jest.fn().mockResolvedValue({ + token: 'backstageToken', + }), + }; + + oAuthState = { + code_verifier: 'foo', + handle: 'sessionid', + nonce: 'nonce', + env: 'development', + } as OAuthState; + + fakeSession = { + ['oauth2:console.cloud.vmware.com']: { + state: oAuthState, + }, + }; + + authenticateRequest = { + req: { + query: { + code: 'foo', + state: encodeOAuthState(oAuthState), + } as unknown, + session: fakeSession, + }, + } as OAuthAuthenticatorAuthenticateInput; + }); + + it('stores refresh token in cookie', async () => { + const { + session: { refreshToken }, + } = await vmWareCSPAuthenticator.authenticate( + authenticateRequest, + authenticatorCtx, + ); + + expect(refreshToken).toBe('refreshToken'); + }); + + it('responds with ID token', async () => { + const { session } = await vmWareCSPAuthenticator.authenticate( + authenticateRequest, + authenticatorCtx, + ); + + expect(session.idToken).toBe(idToken); + }); + + it('default transform decodes ID token', async () => { + const result = await vmWareCSPAuthenticator.authenticate( + authenticateRequest, + authenticatorCtx, + ); + + const { profile } = await vmWareCSPAuthenticator.defaultProfileTransform( + result, + resolverContext, + ); + + expect(profile).toStrictEqual({ + email: signInInfo.email, + displayName: `${signInInfo.given_name} ${signInInfo.family_name}`, + }); + }); + + it('default transform fails if claims are missing', async () => { + authenticatorCtx = vmWareCSPAuthenticator.initialize({ + callbackUrl: 'http://callbackUrl', + config: new ConfigReader({ + clientId: 'placeholderClientId', + organizationId: 'myOrgId', + }), + }); + + const result = await vmWareCSPAuthenticator.authenticate( + authenticateRequest, + authenticatorCtx, + ); + + return expect( + vmWareCSPAuthenticator.defaultProfileTransform(result, resolverContext), + ).rejects.toThrow('ID token organizationId mismatch'); + }); + + it('default transform fails if organizationId mismatch', async () => { + const inadequateIdToken: string = await new SignJWT({ sub: 'unusual' }) + .setProtectedHeader({ alg: 'HS256' }) + .sign(Buffer.from('signing key')); + + server.use( + rest.post( + 'https://console.cloud.vmware.com/csp/gateway/am/api/auth/token', + (_, res, ctx) => + res( + ctx.json({ + access_token: 'accessToken', + id_token: inadequateIdToken, + }), + ), + ), + ); + + const result = await vmWareCSPAuthenticator.authenticate( + authenticateRequest, + authenticatorCtx, + ); + + return expect( + vmWareCSPAuthenticator.defaultProfileTransform(result, resolverContext), + ).rejects.toThrow( + 'ID token missing required claims: email, given_name, family_name', + ); + }); + + it('fails when request has no session', () => { + return expect( + vmWareCSPAuthenticator.authenticate( + { + req: { + query: {}, + }, + } as OAuthAuthenticatorStartInput, + authenticatorCtx, + ), + ).rejects.toThrow('requires session support'); + }); + + it('fails when request has no authorization code', () => { + return expect( + vmWareCSPAuthenticator.authenticate( + { + req: { + query: {}, + session: fakeSession, + }, + } as OAuthAuthenticatorStartInput, + authenticatorCtx, + ), + ).rejects.toThrow('Unexpected redirect'); + }); + }); + + describe('integration between #start and #authenticate', () => { + beforeEach(() => { + fakeSession = { + ['oauth2:console.cloud.vmware.com']: { + state: oAuthState, + }, + }; + }); + + it('state param is compatible', async () => { + const startResponse = await vmWareCSPAuthenticator.start( + { + req: { + query: {}, + session: {}, + }, + state: encodeOAuthState(oAuthState), + } as OAuthAuthenticatorStartInput, + authenticatorCtx, + ); + const { searchParams } = new URL(startResponse.url); + const { session } = await vmWareCSPAuthenticator.authenticate( + { + req: { + query: { + code: 'authorization_code', + state: searchParams.get('state'), + } as unknown, + session: fakeSession, + }, + } as OAuthAuthenticatorAuthenticateInput, + authenticatorCtx, + ); + + expect(session).toBeDefined(); + expect(session.idToken).toBe(idToken); + }); + }); + + describe('#refresh', () => { + let refreshRequest: OAuthAuthenticatorRefreshInput; + let resolverContext: jest.Mocked; + + beforeEach(() => { + resolverContext = { + issueToken: jest.fn().mockResolvedValue({ + token: 'defaultBackstageToken', + }), + findCatalogUser: jest.fn(), + signInWithCatalogUser: jest.fn().mockResolvedValue({ + token: 'backstageToken', + }), + }; + + refreshRequest = { + req: { + query: { + code: 'foo', + state: 'sessionid', + } as unknown, + session: fakeSession, + }, + } as OAuthAuthenticatorRefreshInput; + }); + + it('gets new refresh token', async () => { + const { + session: { refreshToken }, + } = await vmWareCSPAuthenticator.refresh( + refreshRequest, + authenticatorCtx, + ); + + expect(refreshToken).toBe('refreshToken'); + }); + + it('default transform decodes ID token', async () => { + const result = await vmWareCSPAuthenticator.refresh( + refreshRequest, + authenticatorCtx, + ); + + const { profile } = await vmWareCSPAuthenticator.defaultProfileTransform( + result, + resolverContext, + ); + + expect(profile).toStrictEqual({ + email: signInInfo.email, + displayName: `${signInInfo.given_name} ${signInInfo.family_name}`, + }); + }); + }); +}); diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.ts b/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.ts index bd97111de6..c6c7d8a5c7 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.ts @@ -22,7 +22,7 @@ import { PassportOAuthDoneCallback, PassportProfile, } from '@backstage/plugin-auth-node'; -import jwtDecoder from 'jwt-decode'; +import { decodeJwt } from 'jose'; import { Metadata, StateStoreStoreCallback, @@ -54,7 +54,7 @@ export const vmWareCSPAuthenticator = createOAuthAuthenticator< ); } - const identity: Record = jwtDecoder(input.session.idToken); + const identity = decodeJwt(input.session.idToken); const missingClaims = [ 'email', 'given_name', @@ -75,7 +75,7 @@ export const vmWareCSPAuthenticator = createOAuthAuthenticator< return { profile: { displayName: `${identity.given_name} ${identity.family_name}`, - email: identity.email, + email: identity.email as string, }, }; }, diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/module.test.ts b/plugins/auth-backend-module-vmware-csp-provider/src/module.test.ts new file mode 100644 index 0000000000..3bcf7e4e56 --- /dev/null +++ b/plugins/auth-backend-module-vmware-csp-provider/src/module.test.ts @@ -0,0 +1,243 @@ +/* + * 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 { + createHttpServer, + createSpecializedBackend, + DefaultRootHttpRouter, + ExtendedHttpServer, + HostDiscovery, + MiddlewareFactory, +} from '@backstage/backend-app-api'; +import { + BackendFeature, + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { + mockServices, + TestBackend, + TestBackendOptions, +} from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { decodeOAuthState } from '@backstage/plugin-auth-node'; +import express from 'express'; +import session from 'express-session'; +import request from 'supertest'; + +import { authModuleVmwareCspProvider } from './module'; + +const secret = 'secret'; + +function isPromise(value: unknown | Promise): value is Promise { + return ( + typeof value === 'object' && + value !== null && + 'then' in value && + typeof value.then === 'function' + ); +} + +function unwrapFeature( + feature: BackendFeature | (() => BackendFeature), +): BackendFeature { + return typeof feature === 'function' ? feature() : feature; +} + +const defaultServiceFactories = [ + mockServices.cache.factory(), + mockServices.rootConfig.factory(), + mockServices.database.factory(), + mockServices.httpRouter.factory(), + mockServices.identity.factory(), + mockServices.lifecycle.factory(), + mockServices.logger.factory(), + mockServices.permissions.factory(), + mockServices.rootLifecycle.factory(), + mockServices.rootLogger.factory(), + mockServices.scheduler.factory(), + mockServices.tokenManager.factory(), + mockServices.urlReader.factory(), +]; + +async function createBackendWithSession( + options: TestBackendOptions, +): Promise { + const { extensionPoints, ...otherOptions } = options; + + // Unpack input into awaited plain BackendFeatures + const features: BackendFeature[] = await Promise.all( + options.features?.map(async val => { + if (isPromise(val)) { + const { default: feature } = await val; + return unwrapFeature(feature); + } + return unwrapFeature(val); + }) ?? [], + ); + + let server: ExtendedHttpServer; + + const rootHttpRouterFactory = createServiceFactory({ + service: coreServices.rootHttpRouter, + deps: { + config: coreServices.rootConfig, + lifecycle: coreServices.rootLifecycle, + rootLogger: coreServices.rootLogger, + }, + async factory({ config, lifecycle, rootLogger }) { + const router = DefaultRootHttpRouter.create(); + const logger = rootLogger.child({ service: 'rootHttpRouter' }); + + const app = express(); + + const middleware = MiddlewareFactory.create({ config, logger }); + + app.use( + session({ + secret, + resave: false, + saveUninitialized: false, + }), + ); + app.use(router.handler()); + app.use(middleware.notFound()); + app.use(middleware.error()); + + server = await createHttpServer( + app, + { listen: { host: '', port: 0 } }, + { logger }, + ); + + lifecycle.addShutdownHook(() => server.stop(), { logger }); + + await server.start(); + + return router; + }, + }); + + const discoveryFactory = createServiceFactory({ + service: coreServices.discovery, + deps: { + rootHttpRouter: coreServices.rootHttpRouter, + }, + async factory() { + if (!server) { + throw new Error('Test server not started yet'); + } + const port = server.port(); + const discovery = HostDiscovery.fromConfig( + new ConfigReader({ + backend: { baseUrl: `http://localhost:${port}`, listen: { port } }, + }), + ); + return discovery; + }, + }); + + const backend = createSpecializedBackend({ + ...otherOptions, + defaultServiceFactories: [ + ...defaultServiceFactories, + rootHttpRouterFactory, + discoveryFactory, + ], + }); + + for (const feature of features) { + backend.add(feature); + } + + await backend.start(); + + return Object.assign(backend, { + get server() { + if (!server) { + throw new Error('TestBackend server is not available'); + } + return server; + }, + }); +} + +describe('authModuleVmwareCspProvider', () => { + it('should start', async () => { + const backend = await createBackendWithSession({ + features: [ + import('@backstage/plugin-auth-backend'), + authModuleVmwareCspProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + vmwareCloudServices: { + development: { + clientId: 'placeholderClientId', + organizationId: 'orgId', + }, + }, + }, + }, + }, + }), + ], + }); + + const { server } = backend; + + const agent = request.agent(server); + + const res = await agent.get( + '/api/auth/vmwareCloudServices/start?env=development', + ); + + expect(res.status).toEqual(302); + + const nonceCookie = agent.jar.getCookie('vmwareCloudServices-nonce', { + domain: 'localhost', + path: '/api/auth/vmwareCloudServices/handler', + script: false, + secure: false, + }); + expect(nonceCookie).toBeDefined(); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://console.cloud.vmware.com'); + expect(startUrl.pathname).toBe('/csp/gateway/discovery'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + response_type: 'code', + client_id: 'placeholderClientId', + redirect_uri: `http://localhost:${server.port()}/api/auth/vmwareCloudServices/handler/frame`, + code_challenge: expect.any(String), + state: expect.any(String), + scope: 'openid offline_access', + orgId: 'orgId', + code_challenge_method: 'S256', + }); + + expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ + env: 'development', + handle: expect.any(String), + nonce: decodeURIComponent(nonceCookie.value), + }); + + backend.stop(); + }); +}); diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/module.ts b/plugins/auth-backend-module-vmware-csp-provider/src/module.ts index a0dc7ef566..9277d8e661 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/src/module.ts +++ b/plugins/auth-backend-module-vmware-csp-provider/src/module.ts @@ -31,7 +31,7 @@ export const authModuleVmwareCspProvider = createBackendModule({ deps: { providers: authProvidersExtensionPoint }, async init({ providers }) { providers.registerProvider({ - providerId: 'oauth2', + providerId: 'vmwareCloudServices', factory: createOAuthProviderFactory({ authenticator: vmWareCSPAuthenticator, signInResolverFactories: { diff --git a/yarn.lock b/yarn.lock index f8569c5dc6..242afeb841 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4844,18 +4844,23 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend-module-vmware-csp-provider@workspace:plugins/auth-backend-module-vmware-csp-provider" dependencies: + "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" - "@types/jwt-decode": ^3.1.0 - jwt-decode: ^3.1.0 + express: ^4.18.2 + express-session: ^1.17.3 + jose: ^4.6.0 + msw: ^2.0.8 passport-oauth2: ^1.6.1 + supertest: ^6.3.3 languageName: unknown linkType: soft @@ -10132,6 +10137,33 @@ __metadata: languageName: node linkType: hard +"@bundled-es-modules/cookie@npm:^2.0.0": + version: 2.0.0 + resolution: "@bundled-es-modules/cookie@npm:2.0.0" + dependencies: + cookie: ^0.5.0 + checksum: 53114eabbedda20ba6c63f45dcea35c568616d22adf5d1882cef9761f65ae636bf47e0c66325572cc8e3a335e0257caf5f76ff1287990d9e9265be7bc9767a87 + languageName: node + linkType: hard + +"@bundled-es-modules/js-levenshtein@npm:^2.0.1": + version: 2.0.1 + resolution: "@bundled-es-modules/js-levenshtein@npm:2.0.1" + dependencies: + js-levenshtein: ^1.1.6 + checksum: 13d0cbd2b00e563e09a797559dcff8c7e208c1f71e1787535a3d248f7e3d33ef3f0809b9f498d41788ab5fd399882dcca79917d70d97921b7dde94a282c1b7d8 + languageName: node + linkType: hard + +"@bundled-es-modules/statuses@npm:^1.0.1": + version: 1.0.1 + resolution: "@bundled-es-modules/statuses@npm:1.0.1" + dependencies: + statuses: ^2.0.1 + checksum: bcaa7de192e73056950b5fd20e75140d8d09074b1adc4437924b2051bb02b4dbf568c96e67d53b220fb7d735c3446e2ba746599cb1793ab2d23dd2ef230a8622 + languageName: node + linkType: hard + "@changesets/apply-release-plan@npm:^6.1.4": version: 6.1.4 resolution: "@changesets/apply-release-plan@npm:6.1.4" @@ -13104,6 +13136,13 @@ __metadata: languageName: node linkType: hard +"@mswjs/cookies@npm:^1.1.0": + version: 1.1.0 + resolution: "@mswjs/cookies@npm:1.1.0" + checksum: 1d9be44548907b92ff6acd46795292968661be19f1c04c43fdb2beb98bc7e58b8ffcef3be19d0f2cb58df07a36a6b53b4bbc0ea34e023b7366dbc28ffee90338 + languageName: node + linkType: hard + "@mswjs/interceptors@npm:^0.17.10": version: 0.17.10 resolution: "@mswjs/interceptors@npm:0.17.10" @@ -13120,6 +13159,20 @@ __metadata: languageName: node linkType: hard +"@mswjs/interceptors@npm:^0.25.11": + version: 0.25.12 + resolution: "@mswjs/interceptors@npm:0.25.12" + dependencies: + "@open-draft/deferred-promise": ^2.2.0 + "@open-draft/logger": ^0.3.0 + "@open-draft/until": ^2.0.0 + is-node-process: ^1.2.0 + outvariant: ^1.2.1 + strict-event-emitter: ^0.5.1 + checksum: 0676808700059f55536b51ffe38e9ea07e26b4d9c284fbbdf7a52b7282b7a93703f18d93b109c384205fe5f72b585506d5550cc8f3559267892b01a0f7561d3d + languageName: node + linkType: hard + "@mui/base@npm:5.0.0-beta.24": version: 5.0.0-beta.24 resolution: "@mui/base@npm:5.0.0-beta.24" @@ -14052,6 +14105,23 @@ __metadata: languageName: node linkType: hard +"@open-draft/deferred-promise@npm:^2.2.0": + version: 2.2.0 + resolution: "@open-draft/deferred-promise@npm:2.2.0" + checksum: 7f29d39725bb8ab5b62f89d88a4202ce2439ac740860979f9e3d0015dfe4bc3daddcfa5727fa4eed482fdbee770aa591b1136b98b0a0f0569a65294f35bdf56a + languageName: node + linkType: hard + +"@open-draft/logger@npm:^0.3.0": + version: 0.3.0 + resolution: "@open-draft/logger@npm:0.3.0" + dependencies: + is-node-process: ^1.2.0 + outvariant: ^1.4.0 + checksum: 7adfe3d0ed8ca32333ce2a77f9a93d561ebc89c989eaa9722f1dc8a2d2854f5de1bef6fa6894cdf58e16fa4dd9cfa99444ea1f5cac6eb1518e9247911ed042d5 + languageName: node + linkType: hard + "@open-draft/until@npm:^1.0.3": version: 1.0.3 resolution: "@open-draft/until@npm:1.0.3" @@ -14059,6 +14129,13 @@ __metadata: languageName: node linkType: hard +"@open-draft/until@npm:^2.0.0, @open-draft/until@npm:^2.1.0": + version: 2.1.0 + resolution: "@open-draft/until@npm:2.1.0" + checksum: 140ea3b16f4a3a6a729c1256050e20a93d408d7aa1e125648ce2665b3c526ed452510c6e4a6f4b15d95fb5e41203fb51510eb8fbc8812d5e5a91880293d66471 + languageName: node + linkType: hard + "@openapi-contrib/openapi-schema-to-json-schema@npm:~3.2.0": version: 3.2.0 resolution: "@openapi-contrib/openapi-schema-to-json-schema@npm:3.2.0" @@ -19081,6 +19158,13 @@ __metadata: languageName: node linkType: hard +"@types/statuses@npm:^2.0.1": + version: 2.0.4 + resolution: "@types/statuses@npm:2.0.4" + checksum: 3a806c3b96d1845e3e7441fbf0839037e95f717334760ddb7c29223c9a34a7206b68e2998631f89f1a1e3ef5b67b15652f6e8fa14987ebd7f6d38587c1bffd18 + languageName: node + linkType: hard + "@types/stoppable@npm:^1.1.0": version: 1.1.3 resolution: "@types/stoppable@npm:1.1.3" @@ -28915,6 +28999,13 @@ __metadata: languageName: node linkType: hard +"headers-polyfill@npm:^4.0.1": + version: 4.0.2 + resolution: "headers-polyfill@npm:4.0.2" + checksum: a95280ed58df429fc86c4f49b21596be3ea3f5f3d790e7d75238668df9b90b292f15a968c7c19ae1db88c0ae036dd1bf363a71b8e771199d82848e2d8b3c6c2e + languageName: node + linkType: hard + "helmet@npm:^6.0.0": version: 6.0.1 resolution: "helmet@npm:6.0.1" @@ -34689,6 +34780,42 @@ __metadata: languageName: node linkType: hard +"msw@npm:^2.0.8": + version: 2.0.8 + resolution: "msw@npm:2.0.8" + dependencies: + "@bundled-es-modules/cookie": ^2.0.0 + "@bundled-es-modules/js-levenshtein": ^2.0.1 + "@bundled-es-modules/statuses": ^1.0.1 + "@mswjs/cookies": ^1.1.0 + "@mswjs/interceptors": ^0.25.11 + "@open-draft/until": ^2.1.0 + "@types/cookie": ^0.4.1 + "@types/js-levenshtein": ^1.1.1 + "@types/statuses": ^2.0.1 + chalk: ^4.1.2 + chokidar: ^3.4.2 + graphql: ^16.8.1 + headers-polyfill: ^4.0.1 + inquirer: ^8.2.0 + is-node-process: ^1.2.0 + js-levenshtein: ^1.1.6 + outvariant: ^1.4.0 + path-to-regexp: ^6.2.0 + strict-event-emitter: ^0.5.0 + type-fest: ^2.19.0 + yargs: ^17.3.1 + peerDependencies: + typescript: ">= 4.7.x <= 5.2.x" + peerDependenciesMeta: + typescript: + optional: true + bin: + msw: cli/index.js + checksum: 8737dae4cf516d8c591ad8d3751cef2f8f07d10a1f995b8cd7d19d8e955a29352dd6fc092f04bc0e1d0025663ccc3c64ae7e56e29c8d4a4b13699e3ee4747c46 + languageName: node + linkType: hard + "multer@npm:^1.4.5-lts.1": version: 1.4.5-lts.1 resolution: "multer@npm:1.4.5-lts.1" @@ -41336,7 +41463,7 @@ __metadata: languageName: node linkType: hard -"statuses@npm:2.0.1": +"statuses@npm:2.0.1, statuses@npm:^2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" checksum: 18c7623fdb8f646fb213ca4051be4df7efb3484d4ab662937ca6fbef7ced9b9e12842709872eb3020cc3504b93bde88935c9f6417489627a7786f24f8031cbcb @@ -41473,6 +41600,13 @@ __metadata: languageName: node linkType: hard +"strict-event-emitter@npm:^0.5.0, strict-event-emitter@npm:^0.5.1": + version: 0.5.1 + resolution: "strict-event-emitter@npm:0.5.1" + checksum: 350480431bc1c28fdb601ef4976c2f8155fc364b4740f9692dd03e5bdd48aafc99a5e021fe655fbd986d0b803e9f3fc5c4b018b35cb838c4690d60f2a26f1cf3 + languageName: node + linkType: hard + "strict-uri-encode@npm:^2.0.0": version: 2.0.0 resolution: "strict-uri-encode@npm:2.0.0" From b70655f777d54522e4ce35a3c89bdc6e7200e357 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Fri, 17 Nov 2023 16:37:09 -0500 Subject: [PATCH 05/19] tests: Fix tests for msw 2.x Signed-off-by: Carlos Esteban Lopez --- .../src/authenticator.test.ts | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.test.ts b/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.test.ts index 8421c82db7..d2c276d257 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.test.ts @@ -25,8 +25,8 @@ import { OAuthState, } from '@backstage/plugin-auth-node'; import { SignJWT } from 'jose'; -import { rest } from 'msw'; -import { setupServer } from 'msw'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; import { vmWareCSPAuthenticator, @@ -76,14 +76,12 @@ describe('VMwareCloudServicesAuthenticator', () => { beforeEach(() => { server.use( - rest.post( + http.post( 'https://console.cloud.vmware.com/csp/gateway/am/api/auth/token', - (req, res, ctx) => - res( - req.headers.get('Authorization') - ? ctx.json(authResponse) - : ctx.status(401), - ), + ({ request }) => + request.headers.get('Authorization') + ? HttpResponse.json(authResponse) + : HttpResponse.json(null, { status: 500 }), ), ); @@ -325,15 +323,13 @@ describe('VMwareCloudServicesAuthenticator', () => { .sign(Buffer.from('signing key')); server.use( - rest.post( + http.post( 'https://console.cloud.vmware.com/csp/gateway/am/api/auth/token', - (_, res, ctx) => - res( - ctx.json({ - access_token: 'accessToken', - id_token: inadequateIdToken, - }), - ), + () => + HttpResponse.json({ + access_token: 'accessToken', + id_token: inadequateIdToken, + }), ), ); From 5cf2950f0b313d48294629dc1f280ee220d57d24 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Mon, 20 Nov 2023 12:10:15 -0500 Subject: [PATCH 06/19] refactor: Rename CSP to Cloud Signed-off-by: Carlos Esteban Lopez --- .../.eslintrc.js | 0 .../README.md | 7 ++ .../config.d.ts | 0 .../dev/index.ts | 0 .../package.json | 4 +- .../src/authenticator.test.ts | 80 ++++++++++--------- .../src/authenticator.ts | 18 ++--- .../src/index.ts | 8 +- .../src/module.test.ts | 9 +-- .../src/module.ts | 12 +-- .../src/resolvers.test.ts | 16 ++-- .../src/resolvers.ts | 4 +- .../README.md | 7 -- yarn.lock | 4 +- 14 files changed, 88 insertions(+), 81 deletions(-) rename plugins/{auth-backend-module-vmware-csp-provider => auth-backend-module-vmware-cloud-provider}/.eslintrc.js (100%) create mode 100644 plugins/auth-backend-module-vmware-cloud-provider/README.md rename plugins/{auth-backend-module-vmware-csp-provider => auth-backend-module-vmware-cloud-provider}/config.d.ts (100%) rename plugins/{auth-backend-module-vmware-csp-provider => auth-backend-module-vmware-cloud-provider}/dev/index.ts (100%) rename plugins/{auth-backend-module-vmware-csp-provider => auth-backend-module-vmware-cloud-provider}/package.json (89%) rename plugins/{auth-backend-module-vmware-csp-provider => auth-backend-module-vmware-cloud-provider}/src/authenticator.test.ts (84%) rename plugins/{auth-backend-module-vmware-csp-provider => auth-backend-module-vmware-cloud-provider}/src/authenticator.ts (93%) rename plugins/{auth-backend-module-vmware-csp-provider => auth-backend-module-vmware-cloud-provider}/src/index.ts (71%) rename plugins/{auth-backend-module-vmware-csp-provider => auth-backend-module-vmware-cloud-provider}/src/module.test.ts (97%) rename plugins/{auth-backend-module-vmware-csp-provider => auth-backend-module-vmware-cloud-provider}/src/module.ts (79%) rename plugins/{auth-backend-module-vmware-csp-provider => auth-backend-module-vmware-cloud-provider}/src/resolvers.test.ts (82%) rename plugins/{auth-backend-module-vmware-csp-provider => auth-backend-module-vmware-cloud-provider}/src/resolvers.ts (95%) delete mode 100644 plugins/auth-backend-module-vmware-csp-provider/README.md diff --git a/plugins/auth-backend-module-vmware-csp-provider/.eslintrc.js b/plugins/auth-backend-module-vmware-cloud-provider/.eslintrc.js similarity index 100% rename from plugins/auth-backend-module-vmware-csp-provider/.eslintrc.js rename to plugins/auth-backend-module-vmware-cloud-provider/.eslintrc.js diff --git a/plugins/auth-backend-module-vmware-cloud-provider/README.md b/plugins/auth-backend-module-vmware-cloud-provider/README.md new file mode 100644 index 0000000000..d06a0d84ab --- /dev/null +++ b/plugins/auth-backend-module-vmware-cloud-provider/README.md @@ -0,0 +1,7 @@ +# Auth Module: VMware Cloud Provider + +This module provides an VMware Cloud auth provider implementation for `@backstage/plugin-auth-backend`. + +## Links + +- [Backstage](https://backstage.io) diff --git a/plugins/auth-backend-module-vmware-csp-provider/config.d.ts b/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts similarity index 100% rename from plugins/auth-backend-module-vmware-csp-provider/config.d.ts rename to plugins/auth-backend-module-vmware-cloud-provider/config.d.ts diff --git a/plugins/auth-backend-module-vmware-csp-provider/dev/index.ts b/plugins/auth-backend-module-vmware-cloud-provider/dev/index.ts similarity index 100% rename from plugins/auth-backend-module-vmware-csp-provider/dev/index.ts rename to plugins/auth-backend-module-vmware-cloud-provider/dev/index.ts diff --git a/plugins/auth-backend-module-vmware-csp-provider/package.json b/plugins/auth-backend-module-vmware-cloud-provider/package.json similarity index 89% rename from plugins/auth-backend-module-vmware-csp-provider/package.json rename to plugins/auth-backend-module-vmware-cloud-provider/package.json index 7d7b49fc72..396f9bca10 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/package.json +++ b/plugins/auth-backend-module-vmware-cloud-provider/package.json @@ -1,6 +1,6 @@ { - "name": "@backstage/plugin-auth-backend-module-vmware-csp-provider", - "description": "The vmware-csp-provider backend module for the auth plugin.", + "name": "@backstage/plugin-auth-backend-module-vmware-cloud-provider", + "description": "The vmware-cloud-provider backend module for the auth plugin.", "version": "0.0.0", "main": "src/index.ts", "types": "src/index.ts", diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.test.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts similarity index 84% rename from plugins/auth-backend-module-vmware-csp-provider/src/authenticator.test.ts rename to plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts index d2c276d257..6e2c6aeb32 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts @@ -29,8 +29,8 @@ import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; import { - vmWareCSPAuthenticator, - vmWareCSPAuthenticatorContext, + vmwareCloudAuthenticator, + vmwareCloudAuthenticatorContext, } from './authenticator'; jest.mock('uid2', () => jest.fn().mockReturnValue('sessionid')); @@ -60,7 +60,7 @@ describe('VMwareCloudServicesAuthenticator', () => { }; let fakeSession: Record; - let authenticatorCtx: vmWareCSPAuthenticatorContext; + let authenticatorCtx: vmwareCloudAuthenticatorContext; beforeAll(async () => { idToken = await new SignJWT(signInInfo) @@ -85,7 +85,7 @@ describe('VMwareCloudServicesAuthenticator', () => { ), ); - authenticatorCtx = vmWareCSPAuthenticator.initialize({ + authenticatorCtx = vmwareCloudAuthenticator.initialize({ callbackUrl: 'http://callbackUrl', config: new ConfigReader({ clientId: 'placeholderClientId', @@ -97,7 +97,7 @@ describe('VMwareCloudServicesAuthenticator', () => { describe('#initialize', () => { it('fails when organizationId is not configured', () => { return expect(() => - vmWareCSPAuthenticator.initialize({ + vmwareCloudAuthenticator.initialize({ callbackUrl: 'http://callbackUrl', config: new ConfigReader({ clientId: 'placeholderClientId', @@ -122,7 +122,7 @@ describe('VMwareCloudServicesAuthenticator', () => { }); it('redirects to the Cloud Services Console consent page', async () => { - const startResponse = await vmWareCSPAuthenticator.start( + const startResponse = await vmwareCloudAuthenticator.start( startRequest, authenticatorCtx, ); @@ -134,7 +134,7 @@ describe('VMwareCloudServicesAuthenticator', () => { }); it('passes client ID from config', async () => { - const startResponse = await vmWareCSPAuthenticator.start( + const startResponse = await vmwareCloudAuthenticator.start( startRequest, authenticatorCtx, ); @@ -144,7 +144,7 @@ describe('VMwareCloudServicesAuthenticator', () => { }); it('passes organizationId from config', async () => { - const startResponse = await vmWareCSPAuthenticator.start( + const startResponse = await vmwareCloudAuthenticator.start( startRequest, authenticatorCtx, ); @@ -154,7 +154,7 @@ describe('VMwareCloudServicesAuthenticator', () => { }); it('passes callback URL', async () => { - const startResponse = await vmWareCSPAuthenticator.start( + const startResponse = await vmwareCloudAuthenticator.start( startRequest, authenticatorCtx, ); @@ -164,7 +164,7 @@ describe('VMwareCloudServicesAuthenticator', () => { }); it('requests scopes for ID and refresh token', async () => { - const startResponse = await vmWareCSPAuthenticator.start( + const startResponse = await vmwareCloudAuthenticator.start( startRequest, authenticatorCtx, ); @@ -174,7 +174,7 @@ describe('VMwareCloudServicesAuthenticator', () => { }); it('generates PKCE challenge', async () => { - const startResponse = await vmWareCSPAuthenticator.start( + const startResponse = await vmwareCloudAuthenticator.start( startRequest, authenticatorCtx, ); @@ -185,7 +185,7 @@ describe('VMwareCloudServicesAuthenticator', () => { }); it('stores PKCE verifier in session', async () => { - await vmWareCSPAuthenticator.start(startRequest, authenticatorCtx); + await vmwareCloudAuthenticator.start(startRequest, authenticatorCtx); expect( fakeSession['oauth2:console.cloud.vmware.com'].state.code_verifier, @@ -194,7 +194,7 @@ describe('VMwareCloudServicesAuthenticator', () => { it('fails when request has no session', () => { return expect( - vmWareCSPAuthenticator.start( + vmwareCloudAuthenticator.start( { state: encodeOAuthState(oAuthState), req: { @@ -207,7 +207,7 @@ describe('VMwareCloudServicesAuthenticator', () => { }); it('adds session ID handle to state param', async () => { - const startResponse = await vmWareCSPAuthenticator.start( + const startResponse = await vmwareCloudAuthenticator.start( startRequest, authenticatorCtx, ); @@ -264,7 +264,7 @@ describe('VMwareCloudServicesAuthenticator', () => { it('stores refresh token in cookie', async () => { const { session: { refreshToken }, - } = await vmWareCSPAuthenticator.authenticate( + } = await vmwareCloudAuthenticator.authenticate( authenticateRequest, authenticatorCtx, ); @@ -273,7 +273,7 @@ describe('VMwareCloudServicesAuthenticator', () => { }); it('responds with ID token', async () => { - const { session } = await vmWareCSPAuthenticator.authenticate( + const { session } = await vmwareCloudAuthenticator.authenticate( authenticateRequest, authenticatorCtx, ); @@ -282,15 +282,16 @@ describe('VMwareCloudServicesAuthenticator', () => { }); it('default transform decodes ID token', async () => { - const result = await vmWareCSPAuthenticator.authenticate( + const result = await vmwareCloudAuthenticator.authenticate( authenticateRequest, authenticatorCtx, ); - const { profile } = await vmWareCSPAuthenticator.defaultProfileTransform( - result, - resolverContext, - ); + const { profile } = + await vmwareCloudAuthenticator.defaultProfileTransform( + result, + resolverContext, + ); expect(profile).toStrictEqual({ email: signInInfo.email, @@ -299,7 +300,7 @@ describe('VMwareCloudServicesAuthenticator', () => { }); it('default transform fails if claims are missing', async () => { - authenticatorCtx = vmWareCSPAuthenticator.initialize({ + authenticatorCtx = vmwareCloudAuthenticator.initialize({ callbackUrl: 'http://callbackUrl', config: new ConfigReader({ clientId: 'placeholderClientId', @@ -307,13 +308,16 @@ describe('VMwareCloudServicesAuthenticator', () => { }), }); - const result = await vmWareCSPAuthenticator.authenticate( + const result = await vmwareCloudAuthenticator.authenticate( authenticateRequest, authenticatorCtx, ); return expect( - vmWareCSPAuthenticator.defaultProfileTransform(result, resolverContext), + vmwareCloudAuthenticator.defaultProfileTransform( + result, + resolverContext, + ), ).rejects.toThrow('ID token organizationId mismatch'); }); @@ -333,13 +337,16 @@ describe('VMwareCloudServicesAuthenticator', () => { ), ); - const result = await vmWareCSPAuthenticator.authenticate( + const result = await vmwareCloudAuthenticator.authenticate( authenticateRequest, authenticatorCtx, ); return expect( - vmWareCSPAuthenticator.defaultProfileTransform(result, resolverContext), + vmwareCloudAuthenticator.defaultProfileTransform( + result, + resolverContext, + ), ).rejects.toThrow( 'ID token missing required claims: email, given_name, family_name', ); @@ -347,7 +354,7 @@ describe('VMwareCloudServicesAuthenticator', () => { it('fails when request has no session', () => { return expect( - vmWareCSPAuthenticator.authenticate( + vmwareCloudAuthenticator.authenticate( { req: { query: {}, @@ -360,7 +367,7 @@ describe('VMwareCloudServicesAuthenticator', () => { it('fails when request has no authorization code', () => { return expect( - vmWareCSPAuthenticator.authenticate( + vmwareCloudAuthenticator.authenticate( { req: { query: {}, @@ -383,7 +390,7 @@ describe('VMwareCloudServicesAuthenticator', () => { }); it('state param is compatible', async () => { - const startResponse = await vmWareCSPAuthenticator.start( + const startResponse = await vmwareCloudAuthenticator.start( { req: { query: {}, @@ -394,7 +401,7 @@ describe('VMwareCloudServicesAuthenticator', () => { authenticatorCtx, ); const { searchParams } = new URL(startResponse.url); - const { session } = await vmWareCSPAuthenticator.authenticate( + const { session } = await vmwareCloudAuthenticator.authenticate( { req: { query: { @@ -441,7 +448,7 @@ describe('VMwareCloudServicesAuthenticator', () => { it('gets new refresh token', async () => { const { session: { refreshToken }, - } = await vmWareCSPAuthenticator.refresh( + } = await vmwareCloudAuthenticator.refresh( refreshRequest, authenticatorCtx, ); @@ -450,15 +457,16 @@ describe('VMwareCloudServicesAuthenticator', () => { }); it('default transform decodes ID token', async () => { - const result = await vmWareCSPAuthenticator.refresh( + const result = await vmwareCloudAuthenticator.refresh( refreshRequest, authenticatorCtx, ); - const { profile } = await vmWareCSPAuthenticator.defaultProfileTransform( - result, - resolverContext, - ); + const { profile } = + await vmwareCloudAuthenticator.defaultProfileTransform( + result, + resolverContext, + ); expect(profile).toStrictEqual({ email: signInInfo.email, diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts similarity index 93% rename from plugins/auth-backend-module-vmware-csp-provider/src/authenticator.ts rename to plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts index c6c7d8a5c7..671111bfd9 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts @@ -30,22 +30,22 @@ import { Strategy as OAuth2Strategy, } from 'passport-oauth2'; -export interface vmWareCSPAuthenticatorContext { +export interface vmwareCloudAuthenticatorContext { organizationId?: string; providerStrategy: OAuth2Strategy; helper: PassportOAuthAuthenticatorHelper; } -type CSPPassportProfile = PassportProfile & { +type vmwarePassportProfile = PassportProfile & { organizationId?: string; }; /** - * VMWare CSP Authenticator to be used by `createOAuthProviderFactory` + * VMware Cloud Authenticator to be used by `createOAuthProviderFactory` */ -export const vmWareCSPAuthenticator = createOAuthAuthenticator< - vmWareCSPAuthenticatorContext, - CSPPassportProfile +export const vmwareCloudAuthenticator = createOAuthAuthenticator< + vmwareCloudAuthenticatorContext, + vmwarePassportProfile >({ defaultProfileTransform: async input => { if (!input.session.idToken) { @@ -120,7 +120,7 @@ export const vmWareCSPAuthenticator = createOAuthAuthenticator< }, ); - // Both VMWare & OAuth2Strategy fight over control of the state when PKCE is on, thus this hack + // Both VMware & OAuth2Strategy fight over control of the state when PKCE is on, thus this hack const pkceSessionStore = Object.create( (providerStrategy as any)._stateStore, ); @@ -197,7 +197,7 @@ export const vmWareCSPAuthenticator = createOAuthAuthenticator< fullProfile: { ...result.fullProfile, organizationId: ctx.organizationId, - } as CSPPassportProfile, + } as vmwarePassportProfile, })); }, @@ -207,7 +207,7 @@ export const vmWareCSPAuthenticator = createOAuthAuthenticator< fullProfile: { ...result.fullProfile, organizationId: ctx.organizationId, - } as CSPPassportProfile, + } as vmwarePassportProfile, })); }, }); diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/index.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/index.ts similarity index 71% rename from plugins/auth-backend-module-vmware-csp-provider/src/index.ts rename to plugins/auth-backend-module-vmware-cloud-provider/src/index.ts index 3a0770836a..4ce11b4f3a 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/src/index.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/index.ts @@ -15,11 +15,11 @@ */ /** - * The vmware-csp-provider backend module for the auth plugin. + * The vmware-cloud-provider backend module for the auth plugin. * * @packageDocumentation */ -export { vmWareCSPAuthenticator } from './authenticator'; -export { authModuleVmwareCspProvider as default } from './module'; -export { vmwareCSPSignInResolvers } from './resolvers'; +export { vmwareCloudAuthenticator } from './authenticator'; +export { authModuleVmwareCloudProvider as default } from './module'; +export { vmwareCloudSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/module.test.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/module.test.ts similarity index 97% rename from plugins/auth-backend-module-vmware-csp-provider/src/module.test.ts rename to plugins/auth-backend-module-vmware-cloud-provider/src/module.test.ts index 3bcf7e4e56..3461ef79a5 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/src/module.test.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/module.test.ts @@ -37,9 +37,7 @@ import express from 'express'; import session from 'express-session'; import request from 'supertest'; -import { authModuleVmwareCspProvider } from './module'; - -const secret = 'secret'; +import { authModuleVmwareCloudProvider } from './module'; function isPromise(value: unknown | Promise): value is Promise { return ( @@ -104,6 +102,7 @@ async function createBackendWithSession( const app = express(); const middleware = MiddlewareFactory.create({ config, logger }); + const secret = 'secret'; app.use( session({ @@ -174,12 +173,12 @@ async function createBackendWithSession( }); } -describe('authModuleVmwareCspProvider', () => { +describe('authModuleVmwareCloudProvider', () => { it('should start', async () => { const backend = await createBackendWithSession({ features: [ import('@backstage/plugin-auth-backend'), - authModuleVmwareCspProvider, + authModuleVmwareCloudProvider, mockServices.rootConfig.factory({ data: { app: { diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/module.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/module.ts similarity index 79% rename from plugins/auth-backend-module-vmware-csp-provider/src/module.ts rename to plugins/auth-backend-module-vmware-cloud-provider/src/module.ts index 9277d8e661..fd53e01dce 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/src/module.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/module.ts @@ -20,12 +20,12 @@ import { createOAuthProviderFactory, } from '@backstage/plugin-auth-node'; -import { vmWareCSPAuthenticator } from './authenticator'; -import { vmwareCSPSignInResolvers } from './resolvers'; +import { vmwareCloudAuthenticator } from './authenticator'; +import { vmwareCloudSignInResolvers } from './resolvers'; -export const authModuleVmwareCspProvider = createBackendModule({ +export const authModuleVmwareCloudProvider = createBackendModule({ pluginId: 'auth', - moduleId: 'vmware-csp-provider', + moduleId: 'vmware-cloud-provider', register(reg) { reg.registerInit({ deps: { providers: authProvidersExtensionPoint }, @@ -33,9 +33,9 @@ export const authModuleVmwareCspProvider = createBackendModule({ providers.registerProvider({ providerId: 'vmwareCloudServices', factory: createOAuthProviderFactory({ - authenticator: vmWareCSPAuthenticator, + authenticator: vmwareCloudAuthenticator, signInResolverFactories: { - ...vmwareCSPSignInResolvers, + ...vmwareCloudSignInResolvers, ...commonSignInResolvers, }, }), diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/resolvers.test.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/resolvers.test.ts similarity index 82% rename from plugins/auth-backend-module-vmware-csp-provider/src/resolvers.test.ts rename to plugins/auth-backend-module-vmware-cloud-provider/src/resolvers.test.ts index 9a9abd7260..250fd430c7 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/src/resolvers.test.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/resolvers.test.ts @@ -22,12 +22,12 @@ import { SignInResolver, } from '@backstage/plugin-auth-node'; -import { vmwareCSPSignInResolvers } from './resolvers'; +import { vmwareCloudSignInResolvers } from './resolvers'; -describe('CSPResolver', () => { +describe('vmwareCloudResolver', () => { let resolverContext: jest.Mocked; let signInInfo: SignInInfo>; - let cspResolver: SignInResolver>; + let signInResolver: SignInResolver>; beforeEach(() => { resolverContext = { @@ -48,12 +48,12 @@ describe('CSPResolver', () => { }, }; - cspResolver = - vmwareCSPSignInResolvers.profileEmailMatchingUserEntityEmail(); + signInResolver = + vmwareCloudSignInResolvers.profileEmailMatchingUserEntityEmail(); }); it('looks up backstage identity by email', async () => { - const backstageIdentity = await cspResolver(signInInfo, resolverContext); + const backstageIdentity = await signInResolver(signInInfo, resolverContext); expect(backstageIdentity.token).toBe('backstageToken'); expect(resolverContext.signInWithCatalogUser).toHaveBeenCalledWith({ @@ -68,7 +68,7 @@ describe('CSPResolver', () => { new NotFoundError('User not found'), ); - const backstageIdentity = await cspResolver(signInInfo, resolverContext); + const backstageIdentity = await signInResolver(signInInfo, resolverContext); expect(backstageIdentity.token).toBe('defaultBackstageToken'); expect(resolverContext.issueToken).toHaveBeenCalledWith({ @@ -83,7 +83,7 @@ describe('CSPResolver', () => { const error = new Error('bizarre'); resolverContext.signInWithCatalogUser.mockRejectedValue(error); - return expect(cspResolver(signInInfo, resolverContext)).rejects.toThrow( + return expect(signInResolver(signInInfo, resolverContext)).rejects.toThrow( error, ); }); diff --git a/plugins/auth-backend-module-vmware-csp-provider/src/resolvers.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/resolvers.ts similarity index 95% rename from plugins/auth-backend-module-vmware-csp-provider/src/resolvers.ts rename to plugins/auth-backend-module-vmware-cloud-provider/src/resolvers.ts index 249f4f8abc..8863376ca6 100644 --- a/plugins/auth-backend-module-vmware-csp-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/resolvers.ts @@ -23,11 +23,11 @@ import { } from '@backstage/plugin-auth-node'; /** - * Available sign-in resolvers for the VMWare CSP auth provider. + * Available sign-in resolvers for the VMware Cloud auth provider. * * @public */ -export namespace vmwareCSPSignInResolvers { +export namespace vmwareCloudSignInResolvers { /** * Looks up the user by matching their profile email to the entity's profile email. * If that fails, sign in the user without associating with a catalog user. diff --git a/plugins/auth-backend-module-vmware-csp-provider/README.md b/plugins/auth-backend-module-vmware-csp-provider/README.md deleted file mode 100644 index 75ee770828..0000000000 --- a/plugins/auth-backend-module-vmware-csp-provider/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Auth Module: VMWare CSP Provider - -This module provides an VMWare CSP auth provider implementation for `@backstage/plugin-auth-backend`. - -## Links - -- [Backstage](https://backstage.io) diff --git a/yarn.lock b/yarn.lock index 242afeb841..ab1541dfa2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4840,9 +4840,9 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-auth-backend-module-vmware-csp-provider@workspace:plugins/auth-backend-module-vmware-csp-provider": +"@backstage/plugin-auth-backend-module-vmware-cloud-provider@workspace:plugins/auth-backend-module-vmware-cloud-provider": version: 0.0.0-use.local - resolution: "@backstage/plugin-auth-backend-module-vmware-csp-provider@workspace:plugins/auth-backend-module-vmware-csp-provider" + resolution: "@backstage/plugin-auth-backend-module-vmware-cloud-provider@workspace:plugins/auth-backend-module-vmware-cloud-provider" dependencies: "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" From 328b82e027c219d6afc512f4b3854a3bed47f6a8 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Mon, 20 Nov 2023 12:14:30 -0500 Subject: [PATCH 07/19] chore: Update changeset for refactor Signed-off-by: Carlos Esteban Lopez --- .changeset/empty-fireants-study.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/empty-fireants-study.md b/.changeset/empty-fireants-study.md index 4316eaffdf..0bca61fa6b 100644 --- a/.changeset/empty-fireants-study.md +++ b/.changeset/empty-fireants-study.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-auth-backend-module-vmware-csp-provider': patch +'@backstage/plugin-auth-backend-module-vmware-cloud-provider': patch --- -Add VMWare CSP (Cloud Service Provider) auth backend module provider +Add VMware CSP (Cloud Service Provider) auth backend module provider From f3d06de7d8636272f8053449f7bb2743e8b53752 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Mon, 20 Nov 2023 12:56:23 -0500 Subject: [PATCH 08/19] refactor: Rename vmwareCloudServices module id to vmwareCloud Signed-off-by: Carlos Esteban Lopez --- .../config.d.ts | 2 +- .../src/module.test.ts | 16 +++++++--------- .../src/module.ts | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts b/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts index 10b3566899..0026a7d2d0 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts @@ -18,7 +18,7 @@ export interface Config { auth?: { providers?: { /** @visibility frontend */ - vmwareCloudServices?: { + vmwareCloud?: { [authEnv: string]: { clientId: string; organizationId: string; diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/module.test.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/module.test.ts index 3461ef79a5..939e9a9a41 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/module.test.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/module.test.ts @@ -102,11 +102,11 @@ async function createBackendWithSession( const app = express(); const middleware = MiddlewareFactory.create({ config, logger }); - const secret = 'secret'; + const testSecret = 'secret'; app.use( session({ - secret, + secret: testSecret, resave: false, saveUninitialized: false, }), @@ -186,7 +186,7 @@ describe('authModuleVmwareCloudProvider', () => { }, auth: { providers: { - vmwareCloudServices: { + vmwareCloud: { development: { clientId: 'placeholderClientId', organizationId: 'orgId', @@ -203,15 +203,13 @@ describe('authModuleVmwareCloudProvider', () => { const agent = request.agent(server); - const res = await agent.get( - '/api/auth/vmwareCloudServices/start?env=development', - ); + const res = await agent.get('/api/auth/vmwareCloud/start?env=development'); expect(res.status).toEqual(302); - const nonceCookie = agent.jar.getCookie('vmwareCloudServices-nonce', { + const nonceCookie = agent.jar.getCookie('vmwareCloud-nonce', { domain: 'localhost', - path: '/api/auth/vmwareCloudServices/handler', + path: '/api/auth/vmwareCloud/handler', script: false, secure: false, }); @@ -223,7 +221,7 @@ describe('authModuleVmwareCloudProvider', () => { expect(Object.fromEntries(startUrl.searchParams)).toEqual({ response_type: 'code', client_id: 'placeholderClientId', - redirect_uri: `http://localhost:${server.port()}/api/auth/vmwareCloudServices/handler/frame`, + redirect_uri: `http://localhost:${server.port()}/api/auth/vmwareCloud/handler/frame`, code_challenge: expect.any(String), state: expect.any(String), scope: 'openid offline_access', diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/module.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/module.ts index fd53e01dce..d5312e6d73 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/module.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/module.ts @@ -31,7 +31,7 @@ export const authModuleVmwareCloudProvider = createBackendModule({ deps: { providers: authProvidersExtensionPoint }, async init({ providers }) { providers.registerProvider({ - providerId: 'vmwareCloudServices', + providerId: 'vmwareCloud', factory: createOAuthProviderFactory({ authenticator: vmwareCloudAuthenticator, signInResolverFactories: { From da273b695bcd3b8c64d0e0951dc60a139058d27f Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Mon, 20 Nov 2023 14:28:31 -0500 Subject: [PATCH 09/19] chore: Update api-reports Signed-off-by: Carlos Esteban Lopez --- .../api-report.md | 46 +++++++++++++++++++ .../config.d.ts | 2 +- .../src/authenticator.ts | 6 ++- .../src/index.ts | 6 ++- .../src/module.test.ts | 15 +++--- .../src/module.ts | 7 ++- 6 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 plugins/auth-backend-module-vmware-cloud-provider/api-report.md diff --git a/plugins/auth-backend-module-vmware-cloud-provider/api-report.md b/plugins/auth-backend-module-vmware-cloud-provider/api-report.md new file mode 100644 index 0000000000..2254b02491 --- /dev/null +++ b/plugins/auth-backend-module-vmware-cloud-provider/api-report.md @@ -0,0 +1,46 @@ +## API Report File for "@backstage/plugin-auth-backend-module-vmware-cloud-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 { OAuthAuthenticator } from '@backstage/plugin-auth-node'; +import { OAuthAuthenticatorResult } from '@backstage/plugin-auth-node'; +import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node'; +import { PassportProfile } from '@backstage/plugin-auth-node'; +import { SignInResolverFactory } from '@backstage/plugin-auth-node'; +import { Strategy } from 'passport-oauth2'; + +// @public +const authModuleVmwareCloudProvider: () => BackendFeature; +export default authModuleVmwareCloudProvider; + +// @public +export const vmwareCloudAuthenticator: OAuthAuthenticator< + vmwareCloudAuthenticatorContext, + vmwarePassportProfile +>; + +// @public (undocumented) +export interface vmwareCloudAuthenticatorContext { + // (undocumented) + helper: PassportOAuthAuthenticatorHelper; + // (undocumented) + organizationId?: string; + // (undocumented) + providerStrategy: Strategy; +} + +// @public +export namespace vmwareCloudSignInResolvers { + const profileEmailMatchingUserEntityEmail: SignInResolverFactory< + OAuthAuthenticatorResult, + unknown + >; +} + +// @public (undocumented) +export type vmwarePassportProfile = PassportProfile & { + organizationId?: string; +}; +``` diff --git a/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts b/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts index 0026a7d2d0..10b3566899 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts @@ -18,7 +18,7 @@ export interface Config { auth?: { providers?: { /** @visibility frontend */ - vmwareCloud?: { + vmwareCloudServices?: { [authEnv: string]: { clientId: string; organizationId: string; diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts index 671111bfd9..811a34dd58 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts @@ -30,18 +30,22 @@ import { Strategy as OAuth2Strategy, } from 'passport-oauth2'; +/** @public */ export interface vmwareCloudAuthenticatorContext { organizationId?: string; providerStrategy: OAuth2Strategy; helper: PassportOAuthAuthenticatorHelper; } -type vmwarePassportProfile = PassportProfile & { +/** @public */ +export type vmwarePassportProfile = PassportProfile & { organizationId?: string; }; /** * VMware Cloud Authenticator to be used by `createOAuthProviderFactory` + * + * @public */ export const vmwareCloudAuthenticator = createOAuthAuthenticator< vmwareCloudAuthenticatorContext, diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/index.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/index.ts index 4ce11b4f3a..13795e2d97 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/index.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/index.ts @@ -20,6 +20,10 @@ * @packageDocumentation */ -export { vmwareCloudAuthenticator } from './authenticator'; +export { + vmwareCloudAuthenticator, + type vmwareCloudAuthenticatorContext, + type vmwarePassportProfile, +} from './authenticator'; export { authModuleVmwareCloudProvider as default } from './module'; export { vmwareCloudSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/module.test.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/module.test.ts index 939e9a9a41..47c902391e 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/module.test.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/module.test.ts @@ -37,6 +37,7 @@ import express from 'express'; import session from 'express-session'; import request from 'supertest'; +import { Config } from '../config'; import { authModuleVmwareCloudProvider } from './module'; function isPromise(value: unknown | Promise): value is Promise { @@ -186,14 +187,14 @@ describe('authModuleVmwareCloudProvider', () => { }, auth: { providers: { - vmwareCloud: { + vmwareCloudServices: { development: { clientId: 'placeholderClientId', organizationId: 'orgId', }, }, }, - }, + } as Config['auth'], }, }), ], @@ -203,13 +204,15 @@ describe('authModuleVmwareCloudProvider', () => { const agent = request.agent(server); - const res = await agent.get('/api/auth/vmwareCloud/start?env=development'); + const res = await agent.get( + '/api/auth/vmwareCloudServices/start?env=development', + ); expect(res.status).toEqual(302); - const nonceCookie = agent.jar.getCookie('vmwareCloud-nonce', { + const nonceCookie = agent.jar.getCookie('vmwareCloudServices-nonce', { domain: 'localhost', - path: '/api/auth/vmwareCloud/handler', + path: '/api/auth/vmwareCloudServices/handler', script: false, secure: false, }); @@ -221,7 +224,7 @@ describe('authModuleVmwareCloudProvider', () => { expect(Object.fromEntries(startUrl.searchParams)).toEqual({ response_type: 'code', client_id: 'placeholderClientId', - redirect_uri: `http://localhost:${server.port()}/api/auth/vmwareCloud/handler/frame`, + redirect_uri: `http://localhost:${server.port()}/api/auth/vmwareCloudServices/handler/frame`, code_challenge: expect.any(String), state: expect.any(String), scope: 'openid offline_access', diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/module.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/module.ts index d5312e6d73..5ef79654a3 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/module.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/module.ts @@ -23,6 +23,11 @@ import { import { vmwareCloudAuthenticator } from './authenticator'; import { vmwareCloudSignInResolvers } from './resolvers'; +/** + * VMware Cloud Provider backend module for the auth plugin + * + * @public + */ export const authModuleVmwareCloudProvider = createBackendModule({ pluginId: 'auth', moduleId: 'vmware-cloud-provider', @@ -31,7 +36,7 @@ export const authModuleVmwareCloudProvider = createBackendModule({ deps: { providers: authProvidersExtensionPoint }, async init({ providers }) { providers.registerProvider({ - providerId: 'vmwareCloud', + providerId: 'vmwareCloudServices', factory: createOAuthProviderFactory({ authenticator: vmwareCloudAuthenticator, signInResolverFactories: { From 4c44c4af6653d9183ac879034c5a538005f1f244 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Mon, 20 Nov 2023 15:26:36 -0500 Subject: [PATCH 10/19] chore: Add catalog-info.yaml Signed-off-by: Carlos Esteban Lopez --- .../catalog-info.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 plugins/auth-backend-module-vmware-cloud-provider/catalog-info.yaml diff --git a/plugins/auth-backend-module-vmware-cloud-provider/catalog-info.yaml b/plugins/auth-backend-module-vmware-cloud-provider/catalog-info.yaml new file mode 100644 index 0000000000..d40b1e4308 --- /dev/null +++ b/plugins/auth-backend-module-vmware-cloud-provider/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-backend-module-vmware-cloud-provider + title: '@backstage/plugin-auth-backend-module-vmware-cloud-provider' + description: The vmware-cloud-provider backend module for the auth plugin. +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers From 343e1620cf9683f205b53754162d08cf2c2bc48b Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Tue, 21 Nov 2023 13:10:57 -0500 Subject: [PATCH 11/19] test: Refactor module test to remove unnecesary code Signed-off-by: Carlos Esteban Lopez --- .../package.json | 5 +- .../src/module.test.ts | 159 +----------------- yarn.lock | 13 +- 3 files changed, 9 insertions(+), 168 deletions(-) diff --git a/plugins/auth-backend-module-vmware-cloud-provider/package.json b/plugins/auth-backend-module-vmware-cloud-provider/package.json index 396f9bca10..137c1e8064 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/package.json +++ b/plugins/auth-backend-module-vmware-cloud-provider/package.json @@ -32,14 +32,11 @@ "passport-oauth2": "^1.6.1" }, "devDependencies": { - "@backstage/backend-app-api": "workspace:^", "@backstage/backend-defaults": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/config": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", - "express": "^4.18.2", - "express-session": "^1.17.3", + "@types/passport-oauth2": "^1.4.15", "msw": "^2.0.8", "supertest": "^6.3.3" }, diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/module.test.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/module.test.ts index 47c902391e..9bed931da1 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/module.test.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/module.test.ts @@ -13,170 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - createHttpServer, - createSpecializedBackend, - DefaultRootHttpRouter, - ExtendedHttpServer, - HostDiscovery, - MiddlewareFactory, -} from '@backstage/backend-app-api'; -import { - BackendFeature, - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - mockServices, - TestBackend, - TestBackendOptions, -} from '@backstage/backend-test-utils'; -import { ConfigReader } from '@backstage/config'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { decodeOAuthState } from '@backstage/plugin-auth-node'; -import express from 'express'; -import session from 'express-session'; import request from 'supertest'; import { Config } from '../config'; import { authModuleVmwareCloudProvider } from './module'; -function isPromise(value: unknown | Promise): value is Promise { - return ( - typeof value === 'object' && - value !== null && - 'then' in value && - typeof value.then === 'function' - ); -} - -function unwrapFeature( - feature: BackendFeature | (() => BackendFeature), -): BackendFeature { - return typeof feature === 'function' ? feature() : feature; -} - -const defaultServiceFactories = [ - mockServices.cache.factory(), - mockServices.rootConfig.factory(), - mockServices.database.factory(), - mockServices.httpRouter.factory(), - mockServices.identity.factory(), - mockServices.lifecycle.factory(), - mockServices.logger.factory(), - mockServices.permissions.factory(), - mockServices.rootLifecycle.factory(), - mockServices.rootLogger.factory(), - mockServices.scheduler.factory(), - mockServices.tokenManager.factory(), - mockServices.urlReader.factory(), -]; - -async function createBackendWithSession( - options: TestBackendOptions, -): Promise { - const { extensionPoints, ...otherOptions } = options; - - // Unpack input into awaited plain BackendFeatures - const features: BackendFeature[] = await Promise.all( - options.features?.map(async val => { - if (isPromise(val)) { - const { default: feature } = await val; - return unwrapFeature(feature); - } - return unwrapFeature(val); - }) ?? [], - ); - - let server: ExtendedHttpServer; - - const rootHttpRouterFactory = createServiceFactory({ - service: coreServices.rootHttpRouter, - deps: { - config: coreServices.rootConfig, - lifecycle: coreServices.rootLifecycle, - rootLogger: coreServices.rootLogger, - }, - async factory({ config, lifecycle, rootLogger }) { - const router = DefaultRootHttpRouter.create(); - const logger = rootLogger.child({ service: 'rootHttpRouter' }); - - const app = express(); - - const middleware = MiddlewareFactory.create({ config, logger }); - const testSecret = 'secret'; - - app.use( - session({ - secret: testSecret, - resave: false, - saveUninitialized: false, - }), - ); - app.use(router.handler()); - app.use(middleware.notFound()); - app.use(middleware.error()); - - server = await createHttpServer( - app, - { listen: { host: '', port: 0 } }, - { logger }, - ); - - lifecycle.addShutdownHook(() => server.stop(), { logger }); - - await server.start(); - - return router; - }, - }); - - const discoveryFactory = createServiceFactory({ - service: coreServices.discovery, - deps: { - rootHttpRouter: coreServices.rootHttpRouter, - }, - async factory() { - if (!server) { - throw new Error('Test server not started yet'); - } - const port = server.port(); - const discovery = HostDiscovery.fromConfig( - new ConfigReader({ - backend: { baseUrl: `http://localhost:${port}`, listen: { port } }, - }), - ); - return discovery; - }, - }); - - const backend = createSpecializedBackend({ - ...otherOptions, - defaultServiceFactories: [ - ...defaultServiceFactories, - rootHttpRouterFactory, - discoveryFactory, - ], - }); - - for (const feature of features) { - backend.add(feature); - } - - await backend.start(); - - return Object.assign(backend, { - get server() { - if (!server) { - throw new Error('TestBackend server is not available'); - } - return server; - }, - }); -} - describe('authModuleVmwareCloudProvider', () => { it('should start', async () => { - const backend = await createBackendWithSession({ + const backend = await startTestBackend({ features: [ import('@backstage/plugin-auth-backend'), authModuleVmwareCloudProvider, @@ -186,6 +32,7 @@ describe('authModuleVmwareCloudProvider', () => { baseUrl: 'http://localhost:3000', }, auth: { + session: { secret: 'test' }, providers: { vmwareCloudServices: { development: { diff --git a/yarn.lock b/yarn.lock index ab1541dfa2..59f75b23ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4844,19 +4844,16 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend-module-vmware-cloud-provider@workspace:plugins/auth-backend-module-vmware-cloud-provider" dependencies: - "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" - "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" - express: ^4.18.2 - express-session: ^1.17.3 + "@types/passport-oauth2": ^1.4.15 jose: ^4.6.0 msw: ^2.0.8 passport-oauth2: ^1.6.1 @@ -18689,14 +18686,14 @@ __metadata: languageName: node linkType: hard -"@types/passport-oauth2@npm:*, @types/passport-oauth2@npm:^1.4.11": - version: 1.4.11 - resolution: "@types/passport-oauth2@npm:1.4.11" +"@types/passport-oauth2@npm:*, @types/passport-oauth2@npm:^1.4.11, @types/passport-oauth2@npm:^1.4.15": + version: 1.4.15 + resolution: "@types/passport-oauth2@npm:1.4.15" dependencies: "@types/express": "*" "@types/oauth": "*" "@types/passport": "*" - checksum: 09d047a6c09a05c036f7db0cf8f8c09bf5878fdd15949bb1baa09a35f439929d471048fa6595f09ea4a4ea25396ce3918b362136579cf4a2b2ee29a92c0dd1ce + checksum: 352c4e2d09a86f8fc0dcf2c917c221f302a35a14e7467e2fa3a1653c0a9a1f842c910a4e52f799856536522e6359c20d1afc319579d9edf4b2ac7ea11b183b52 languageName: node linkType: hard From da363efb98cbbdee94d34b771367599d1e5400d2 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Tue, 21 Nov 2023 13:25:36 -0500 Subject: [PATCH 12/19] test: Add missing test dependency Signed-off-by: Carlos Esteban Lopez --- plugins/auth-backend-module-vmware-cloud-provider/package.json | 3 ++- yarn.lock | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/auth-backend-module-vmware-cloud-provider/package.json b/plugins/auth-backend-module-vmware-cloud-provider/package.json index 137c1e8064..0282029ec1 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/package.json +++ b/plugins/auth-backend-module-vmware-cloud-provider/package.json @@ -28,6 +28,7 @@ "@backstage/catalog-model": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", + "@types/passport-oauth2": "^1.4.15", "jose": "^4.6.0", "passport-oauth2": "^1.6.1" }, @@ -35,8 +36,8 @@ "@backstage/backend-defaults": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/config": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", - "@types/passport-oauth2": "^1.4.15", "msw": "^2.0.8", "supertest": "^6.3.3" }, diff --git a/yarn.lock b/yarn.lock index 59f75b23ad..7cc4d1d816 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4850,6 +4850,7 @@ __metadata: "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" From 2208aaea0de4a60b5e9db16cd7bf4b249f151ddc Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Jaramillo Date: Tue, 21 Nov 2023 18:15:17 -0500 Subject: [PATCH 13/19] Update .changeset/empty-fireants-study.md Co-authored-by: Jamie Klassen Signed-off-by: Carlos Esteban Lopez Jaramillo --- .changeset/empty-fireants-study.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/empty-fireants-study.md b/.changeset/empty-fireants-study.md index 0bca61fa6b..dc6b2d0a06 100644 --- a/.changeset/empty-fireants-study.md +++ b/.changeset/empty-fireants-study.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-auth-backend-module-vmware-cloud-provider': patch +'@backstage/plugin-auth-backend-module-vmware-cloud-provider': minor --- Add VMware CSP (Cloud Service Provider) auth backend module provider From 1d350f1e2cfabec9a414ac5163f365dfa27767dd Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Jaramillo Date: Tue, 21 Nov 2023 18:15:25 -0500 Subject: [PATCH 14/19] Update .changeset/empty-fireants-study.md Co-authored-by: Jamie Klassen Signed-off-by: Carlos Esteban Lopez Jaramillo --- .changeset/empty-fireants-study.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/empty-fireants-study.md b/.changeset/empty-fireants-study.md index dc6b2d0a06..4f5508ac9b 100644 --- a/.changeset/empty-fireants-study.md +++ b/.changeset/empty-fireants-study.md @@ -2,4 +2,4 @@ '@backstage/plugin-auth-backend-module-vmware-cloud-provider': minor --- -Add VMware CSP (Cloud Service Provider) auth backend module provider +Add VMware Cloud auth backend module provider From e06962c68cb8bbae0c5818f7d3427ca186af373c Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Jaramillo Date: Tue, 21 Nov 2023 19:36:05 -0500 Subject: [PATCH 15/19] Update plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts Co-authored-by: Jamie Klassen Signed-off-by: Carlos Esteban Lopez Jaramillo --- .../src/authenticator.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts index 6e2c6aeb32..c569cf1a9b 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts @@ -35,7 +35,7 @@ import { jest.mock('uid2', () => jest.fn().mockReturnValue('sessionid')); -describe('VMwareCloudServicesAuthenticator', () => { +describe('vmwareCloudAuthenticator', () => { const server = setupServer(); setupRequestMockHandlers(server); From ada059e149f884199af0dc7775c389130bba4663 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Wed, 22 Nov 2023 14:12:00 -0500 Subject: [PATCH 16/19] chore: Address PR comments Signed-off-by: Carlos Esteban Lopez --- .../api-report.md | 8 ++++---- .../src/authenticator.test.ts | 4 ++-- .../src/authenticator.ts | 12 ++++++------ .../src/index.ts | 4 ++-- .../src/resolvers.ts | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/plugins/auth-backend-module-vmware-cloud-provider/api-report.md b/plugins/auth-backend-module-vmware-cloud-provider/api-report.md index 2254b02491..ff9e82dd45 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/api-report.md +++ b/plugins/auth-backend-module-vmware-cloud-provider/api-report.md @@ -17,12 +17,12 @@ export default authModuleVmwareCloudProvider; // @public export const vmwareCloudAuthenticator: OAuthAuthenticator< - vmwareCloudAuthenticatorContext, - vmwarePassportProfile + VMwareCloudAuthenticatorContext, + VMwarePassportProfile >; // @public (undocumented) -export interface vmwareCloudAuthenticatorContext { +export interface VMwareCloudAuthenticatorContext { // (undocumented) helper: PassportOAuthAuthenticatorHelper; // (undocumented) @@ -40,7 +40,7 @@ export namespace vmwareCloudSignInResolvers { } // @public (undocumented) -export type vmwarePassportProfile = PassportProfile & { +export type VMwarePassportProfile = PassportProfile & { organizationId?: string; }; ``` diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts index c569cf1a9b..f38ceeb462 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts @@ -30,7 +30,7 @@ import { setupServer } from 'msw/node'; import { vmwareCloudAuthenticator, - vmwareCloudAuthenticatorContext, + VMwareCloudAuthenticatorContext, } from './authenticator'; jest.mock('uid2', () => jest.fn().mockReturnValue('sessionid')); @@ -60,7 +60,7 @@ describe('vmwareCloudAuthenticator', () => { }; let fakeSession: Record; - let authenticatorCtx: vmwareCloudAuthenticatorContext; + let authenticatorCtx: VMwareCloudAuthenticatorContext; beforeAll(async () => { idToken = await new SignJWT(signInInfo) diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts index 811a34dd58..abac9cf2b6 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts @@ -31,14 +31,14 @@ import { } from 'passport-oauth2'; /** @public */ -export interface vmwareCloudAuthenticatorContext { +export interface VMwareCloudAuthenticatorContext { organizationId?: string; providerStrategy: OAuth2Strategy; helper: PassportOAuthAuthenticatorHelper; } /** @public */ -export type vmwarePassportProfile = PassportProfile & { +export type VMwarePassportProfile = PassportProfile & { organizationId?: string; }; @@ -48,8 +48,8 @@ export type vmwarePassportProfile = PassportProfile & { * @public */ export const vmwareCloudAuthenticator = createOAuthAuthenticator< - vmwareCloudAuthenticatorContext, - vmwarePassportProfile + VMwareCloudAuthenticatorContext, + VMwarePassportProfile >({ defaultProfileTransform: async input => { if (!input.session.idToken) { @@ -201,7 +201,7 @@ export const vmwareCloudAuthenticator = createOAuthAuthenticator< fullProfile: { ...result.fullProfile, organizationId: ctx.organizationId, - } as vmwarePassportProfile, + } as VMwarePassportProfile, })); }, @@ -211,7 +211,7 @@ export const vmwareCloudAuthenticator = createOAuthAuthenticator< fullProfile: { ...result.fullProfile, organizationId: ctx.organizationId, - } as vmwarePassportProfile, + } as VMwarePassportProfile, })); }, }); diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/index.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/index.ts index 13795e2d97..1d7bb14b63 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/index.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/index.ts @@ -22,8 +22,8 @@ export { vmwareCloudAuthenticator, - type vmwareCloudAuthenticatorContext, - type vmwarePassportProfile, + type VMwareCloudAuthenticatorContext, + type VMwarePassportProfile, } from './authenticator'; export { authModuleVmwareCloudProvider as default } from './module'; export { vmwareCloudSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/resolvers.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/resolvers.ts index 8863376ca6..4c55b59f17 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/resolvers.ts @@ -60,7 +60,7 @@ export namespace vmwareCloudSignInResolvers { }, }); } catch (e) { - if (!(e instanceof NotFoundError)) { + if (e.name !== NotFoundError.name) { throw e; } return ctx.issueToken({ From 048038cfa7ecfbb509b2b7a44a398dee1ac5ffcb Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Wed, 22 Nov 2023 15:00:20 -0500 Subject: [PATCH 17/19] chore: Address PR comments 2 Signed-off-by: Carlos Esteban Lopez --- plugins/auth-backend-module-vmware-cloud-provider/package.json | 2 +- .../auth-backend-module-vmware-cloud-provider/src/resolvers.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/auth-backend-module-vmware-cloud-provider/package.json b/plugins/auth-backend-module-vmware-cloud-provider/package.json index 0282029ec1..f48e69154b 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/package.json +++ b/plugins/auth-backend-module-vmware-cloud-provider/package.json @@ -26,7 +26,6 @@ "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-model": "workspace:^", - "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@types/passport-oauth2": "^1.4.15", "jose": "^4.6.0", @@ -37,6 +36,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", "msw": "^2.0.8", "supertest": "^6.3.3" diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/resolvers.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/resolvers.ts index 4c55b59f17..2b3a78842b 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/resolvers.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/resolvers.ts @@ -14,7 +14,6 @@ * limitations under the License. */ import { stringifyEntityRef } from '@backstage/catalog-model'; -import { NotFoundError } from '@backstage/errors'; import { createSignInResolverFactory, OAuthAuthenticatorResult, @@ -60,7 +59,7 @@ export namespace vmwareCloudSignInResolvers { }, }); } catch (e) { - if (e.name !== NotFoundError.name) { + if (e.name !== 'NotFoundError') { throw e; } return ctx.issueToken({ From b3552bbfb5340dc8e13e889b13465d0c705d1aba Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Wed, 22 Nov 2023 16:41:46 -0500 Subject: [PATCH 18/19] chore: Address PR comments 3 Signed-off-by: Carlos Esteban Lopez --- .../src/authenticator.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts index abac9cf2b6..3a64c4ffb1 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts @@ -58,13 +58,10 @@ export const vmwareCloudAuthenticator = createOAuthAuthenticator< ); } + const vmwareClaims = ['email', 'given_name', 'family_name', 'context_name']; + const identity = decodeJwt(input.session.idToken); - const missingClaims = [ - 'email', - 'given_name', - 'family_name', - 'context_name', - ].filter(key => !(key in identity)); + const missingClaims = vmwareClaims.filter(key => !(key in identity)); if (missingClaims.length > 0) { throw new Error( @@ -72,6 +69,16 @@ export const vmwareCloudAuthenticator = createOAuthAuthenticator< ); } + const typeMismatchClaims = vmwareClaims.filter( + key => typeof identity[key] !== 'string', + ); + + if (typeMismatchClaims.length > 0) { + throw new Error( + `ID token claims type mismatch: ${typeMismatchClaims.join(', ')}`, + ); + } + if (identity.context_name !== input.fullProfile.organizationId) { throw new Error(`ID token organizationId mismatch`); } From 3bd14fc7dfada63fd8b00e19a22c3ea39263b363 Mon Sep 17 00:00:00 2001 From: Carlos Esteban Lopez Date: Wed, 22 Nov 2023 18:00:31 -0500 Subject: [PATCH 19/19] chore: Address PR comments 4 Signed-off-by: Carlos Esteban Lopez --- .../src/authenticator.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts index 3a64c4ffb1..4db4836f13 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.ts @@ -79,14 +79,20 @@ export const vmwareCloudAuthenticator = createOAuthAuthenticator< ); } - if (identity.context_name !== input.fullProfile.organizationId) { + // These claims were checked for presence & type + const { email, given_name, family_name, context_name } = identity as Record< + string, + string + >; + + if (context_name !== input.fullProfile.organizationId) { throw new Error(`ID token organizationId mismatch`); } return { profile: { - displayName: `${identity.given_name} ${identity.family_name}`, - email: identity.email as string, + displayName: `${given_name} ${family_name}`, + email, }, }; },