test: Add module test, fix providerId & remove jwt-decode dep
Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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<string, any>;
|
||||
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<AuthResolverContext>;
|
||||
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<AuthResolverContext>;
|
||||
|
||||
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}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<string, string> = 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,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -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<T>(value: unknown | Promise<T>): value is Promise<T> {
|
||||
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<TExtensionPoints extends any[]>(
|
||||
options: TestBackendOptions<TExtensionPoints>,
|
||||
): Promise<TestBackend> {
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user