Merge pull request #21337 from luchillo17/feat/BCKSTG-189-open-source-vmware-cloud-serv
feat: Add VMware Cloud auth backend module provider
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-vmware-cloud-provider': minor
|
||||
---
|
||||
|
||||
Add VMware Cloud auth backend module provider
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -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)
|
||||
@@ -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<PassportProfile>,
|
||||
unknown
|
||||
>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type VMwarePassportProfile = PassportProfile & {
|
||||
organizationId?: string;
|
||||
};
|
||||
```
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"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",
|
||||
"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/plugin-auth-node": "workspace:^",
|
||||
"@types/passport-oauth2": "^1.4.15",
|
||||
"jose": "^4.6.0",
|
||||
"passport-oauth2": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-defaults": "workspace:^",
|
||||
"@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"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
/*
|
||||
* 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 { http, HttpResponse } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
|
||||
import {
|
||||
vmwareCloudAuthenticator,
|
||||
VMwareCloudAuthenticatorContext,
|
||||
} from './authenticator';
|
||||
|
||||
jest.mock('uid2', () => jest.fn().mockReturnValue('sessionid'));
|
||||
|
||||
describe('vmwareCloudAuthenticator', () => {
|
||||
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: VMwareCloudAuthenticatorContext;
|
||||
|
||||
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(
|
||||
http.post(
|
||||
'https://console.cloud.vmware.com/csp/gateway/am/api/auth/token',
|
||||
({ request }) =>
|
||||
request.headers.get('Authorization')
|
||||
? HttpResponse.json(authResponse)
|
||||
: HttpResponse.json(null, { status: 500 }),
|
||||
),
|
||||
);
|
||||
|
||||
authenticatorCtx = vmwareCloudAuthenticator.initialize({
|
||||
callbackUrl: 'http://callbackUrl',
|
||||
config: new ConfigReader({
|
||||
clientId: 'placeholderClientId',
|
||||
organizationId: 'orgId',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
describe('#initialize', () => {
|
||||
it('fails when organizationId is not configured', () => {
|
||||
return expect(() =>
|
||||
vmwareCloudAuthenticator.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 vmwareCloudAuthenticator.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 vmwareCloudAuthenticator.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 vmwareCloudAuthenticator.start(
|
||||
startRequest,
|
||||
authenticatorCtx,
|
||||
);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
|
||||
expect(searchParams.get('orgId')).toBe('orgId');
|
||||
});
|
||||
|
||||
it('passes callback URL', async () => {
|
||||
const startResponse = await vmwareCloudAuthenticator.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 vmwareCloudAuthenticator.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 vmwareCloudAuthenticator.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 vmwareCloudAuthenticator.start(startRequest, authenticatorCtx);
|
||||
|
||||
expect(
|
||||
fakeSession['oauth2:console.cloud.vmware.com'].state.code_verifier,
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('fails when request has no session', () => {
|
||||
return expect(
|
||||
vmwareCloudAuthenticator.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 vmwareCloudAuthenticator.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 vmwareCloudAuthenticator.authenticate(
|
||||
authenticateRequest,
|
||||
authenticatorCtx,
|
||||
);
|
||||
|
||||
expect(refreshToken).toBe('refreshToken');
|
||||
});
|
||||
|
||||
it('responds with ID token', async () => {
|
||||
const { session } = await vmwareCloudAuthenticator.authenticate(
|
||||
authenticateRequest,
|
||||
authenticatorCtx,
|
||||
);
|
||||
|
||||
expect(session.idToken).toBe(idToken);
|
||||
});
|
||||
|
||||
it('default transform decodes ID token', async () => {
|
||||
const result = await vmwareCloudAuthenticator.authenticate(
|
||||
authenticateRequest,
|
||||
authenticatorCtx,
|
||||
);
|
||||
|
||||
const { profile } =
|
||||
await vmwareCloudAuthenticator.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 = vmwareCloudAuthenticator.initialize({
|
||||
callbackUrl: 'http://callbackUrl',
|
||||
config: new ConfigReader({
|
||||
clientId: 'placeholderClientId',
|
||||
organizationId: 'myOrgId',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await vmwareCloudAuthenticator.authenticate(
|
||||
authenticateRequest,
|
||||
authenticatorCtx,
|
||||
);
|
||||
|
||||
return expect(
|
||||
vmwareCloudAuthenticator.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(
|
||||
http.post(
|
||||
'https://console.cloud.vmware.com/csp/gateway/am/api/auth/token',
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
access_token: 'accessToken',
|
||||
id_token: inadequateIdToken,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const result = await vmwareCloudAuthenticator.authenticate(
|
||||
authenticateRequest,
|
||||
authenticatorCtx,
|
||||
);
|
||||
|
||||
return expect(
|
||||
vmwareCloudAuthenticator.defaultProfileTransform(
|
||||
result,
|
||||
resolverContext,
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'ID token missing required claims: email, given_name, family_name',
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when request has no session', () => {
|
||||
return expect(
|
||||
vmwareCloudAuthenticator.authenticate(
|
||||
{
|
||||
req: {
|
||||
query: {},
|
||||
},
|
||||
} as OAuthAuthenticatorStartInput,
|
||||
authenticatorCtx,
|
||||
),
|
||||
).rejects.toThrow('requires session support');
|
||||
});
|
||||
|
||||
it('fails when request has no authorization code', () => {
|
||||
return expect(
|
||||
vmwareCloudAuthenticator.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 vmwareCloudAuthenticator.start(
|
||||
{
|
||||
req: {
|
||||
query: {},
|
||||
session: {},
|
||||
},
|
||||
state: encodeOAuthState(oAuthState),
|
||||
} as OAuthAuthenticatorStartInput,
|
||||
authenticatorCtx,
|
||||
);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
const { session } = await vmwareCloudAuthenticator.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 vmwareCloudAuthenticator.refresh(
|
||||
refreshRequest,
|
||||
authenticatorCtx,
|
||||
);
|
||||
|
||||
expect(refreshToken).toBe('refreshToken');
|
||||
});
|
||||
|
||||
it('default transform decodes ID token', async () => {
|
||||
const result = await vmwareCloudAuthenticator.refresh(
|
||||
refreshRequest,
|
||||
authenticatorCtx,
|
||||
);
|
||||
|
||||
const { profile } =
|
||||
await vmwareCloudAuthenticator.defaultProfileTransform(
|
||||
result,
|
||||
resolverContext,
|
||||
);
|
||||
|
||||
expect(profile).toStrictEqual({
|
||||
email: signInInfo.email,
|
||||
displayName: `${signInInfo.given_name} ${signInInfo.family_name}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* 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 { decodeJwt } from 'jose';
|
||||
import {
|
||||
Metadata,
|
||||
StateStoreStoreCallback,
|
||||
StateStoreVerifyCallback,
|
||||
Strategy as OAuth2Strategy,
|
||||
} from 'passport-oauth2';
|
||||
|
||||
/** @public */
|
||||
export interface VMwareCloudAuthenticatorContext {
|
||||
organizationId?: string;
|
||||
providerStrategy: OAuth2Strategy;
|
||||
helper: PassportOAuthAuthenticatorHelper;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type VMwarePassportProfile = PassportProfile & {
|
||||
organizationId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* VMware Cloud Authenticator to be used by `createOAuthProviderFactory`
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const vmwareCloudAuthenticator = createOAuthAuthenticator<
|
||||
VMwareCloudAuthenticatorContext,
|
||||
VMwarePassportProfile
|
||||
>({
|
||||
defaultProfileTransform: async input => {
|
||||
if (!input.session.idToken) {
|
||||
throw new Error(
|
||||
`Failed to parse id token and get profile info, missing token from session`,
|
||||
);
|
||||
}
|
||||
|
||||
const vmwareClaims = ['email', 'given_name', 'family_name', 'context_name'];
|
||||
|
||||
const identity = decodeJwt(input.session.idToken);
|
||||
const missingClaims = vmwareClaims.filter(key => !(key in identity));
|
||||
|
||||
if (missingClaims.length > 0) {
|
||||
throw new Error(
|
||||
`ID token missing required claims: ${missingClaims.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const typeMismatchClaims = vmwareClaims.filter(
|
||||
key => typeof identity[key] !== 'string',
|
||||
);
|
||||
|
||||
if (typeMismatchClaims.length > 0) {
|
||||
throw new Error(
|
||||
`ID token claims type mismatch: ${typeMismatchClaims.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 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: `${given_name} ${family_name}`,
|
||||
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 VMwarePassportProfile,
|
||||
}));
|
||||
},
|
||||
|
||||
async refresh(input, ctx) {
|
||||
return ctx.helper.refresh(input).then(result => ({
|
||||
...result,
|
||||
fullProfile: {
|
||||
...result.fullProfile,
|
||||
organizationId: ctx.organizationId,
|
||||
} as VMwarePassportProfile,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
/** @private */
|
||||
function encodeClientCredentials(
|
||||
clientID: string,
|
||||
clientSecret: string,
|
||||
): string {
|
||||
return Buffer.from(`${clientID}:${clientSecret}`).toString('base64');
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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-cloud-provider backend module for the auth plugin.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export {
|
||||
vmwareCloudAuthenticator,
|
||||
type VMwareCloudAuthenticatorContext,
|
||||
type VMwarePassportProfile,
|
||||
} from './authenticator';
|
||||
export { authModuleVmwareCloudProvider as default } from './module';
|
||||
export { vmwareCloudSignInResolvers } from './resolvers';
|
||||
@@ -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 { mockServices, startTestBackend } from '@backstage/backend-test-utils';
|
||||
import { decodeOAuthState } from '@backstage/plugin-auth-node';
|
||||
import request from 'supertest';
|
||||
|
||||
import { Config } from '../config';
|
||||
import { authModuleVmwareCloudProvider } from './module';
|
||||
|
||||
describe('authModuleVmwareCloudProvider', () => {
|
||||
it('should start', async () => {
|
||||
const backend = await startTestBackend({
|
||||
features: [
|
||||
import('@backstage/plugin-auth-backend'),
|
||||
authModuleVmwareCloudProvider,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'http://localhost:3000',
|
||||
},
|
||||
auth: {
|
||||
session: { secret: 'test' },
|
||||
providers: {
|
||||
vmwareCloudServices: {
|
||||
development: {
|
||||
clientId: 'placeholderClientId',
|
||||
organizationId: 'orgId',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Config['auth'],
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 { 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',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: { providers: authProvidersExtensionPoint },
|
||||
async init({ providers }) {
|
||||
providers.registerProvider({
|
||||
providerId: 'vmwareCloudServices',
|
||||
factory: createOAuthProviderFactory({
|
||||
authenticator: vmwareCloudAuthenticator,
|
||||
signInResolverFactories: {
|
||||
...vmwareCloudSignInResolvers,
|
||||
...commonSignInResolvers,
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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 { vmwareCloudSignInResolvers } from './resolvers';
|
||||
|
||||
describe('vmwareCloudResolver', () => {
|
||||
let resolverContext: jest.Mocked<AuthResolverContext>;
|
||||
let signInInfo: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>;
|
||||
let signInResolver: SignInResolver<OAuthAuthenticatorResult<PassportProfile>>;
|
||||
|
||||
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',
|
||||
},
|
||||
};
|
||||
|
||||
signInResolver =
|
||||
vmwareCloudSignInResolvers.profileEmailMatchingUserEntityEmail();
|
||||
});
|
||||
|
||||
it('looks up backstage identity by email', async () => {
|
||||
const backstageIdentity = await signInResolver(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 signInResolver(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(signInResolver(signInInfo, resolverContext)).rejects.toThrow(
|
||||
error,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
createSignInResolverFactory,
|
||||
OAuthAuthenticatorResult,
|
||||
PassportProfile,
|
||||
SignInInfo,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
|
||||
/**
|
||||
* Available sign-in resolvers for the VMware Cloud auth provider.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
export const profileEmailMatchingUserEntityEmail =
|
||||
createSignInResolverFactory({
|
||||
create() {
|
||||
return async (
|
||||
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
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.name !== 'NotFoundError') {
|
||||
throw e;
|
||||
}
|
||||
return ctx.issueToken({
|
||||
claims: {
|
||||
sub: userEntityRef,
|
||||
ent: [userEntityRef],
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -4917,6 +4917,28 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@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-cloud-provider@workspace:plugins/auth-backend-module-vmware-cloud-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/config": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/plugin-auth-backend": "workspace:^"
|
||||
"@backstage/plugin-auth-node": "workspace:^"
|
||||
"@types/passport-oauth2": ^1.4.15
|
||||
jose: ^4.6.0
|
||||
msw: ^2.0.8
|
||||
passport-oauth2: ^1.6.1
|
||||
supertest: ^6.3.3
|
||||
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"
|
||||
@@ -10191,6 +10213,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"
|
||||
@@ -13144,6 +13193,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"
|
||||
@@ -13160,6 +13216,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"
|
||||
@@ -14092,6 +14162,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"
|
||||
@@ -14099,6 +14186,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"
|
||||
@@ -18635,14 +18729,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
|
||||
|
||||
@@ -19094,6 +19188,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"
|
||||
@@ -28893,6 +28994,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"
|
||||
@@ -34617,6 +34725,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"
|
||||
@@ -41203,7 +41347,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
|
||||
@@ -41340,6 +41484,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