@rockcarver/frodo-lib 0.17.0 → 0.17.2-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,65 +1,81 @@
1
+ /**
2
+ * To record and update snapshots, you must perform 3 steps in order:
3
+ *
4
+ * 1. Record API responses & update ESM snapshots
5
+ *
6
+ * To record and update ESM snapshots, you must call the test:record
7
+ * script and override all the connection state variables required
8
+ * to connect to the env to record from:
9
+ *
10
+ * FRODO_DEBUG=1 FRODO_HOST=volker-dev npm run test:record VariablesApi
11
+ *
12
+ * The above command assumes that you have a connection profile for
13
+ * 'volker-dev' on your development machine.
14
+ *
15
+ * 2. Update CJS snapshots
16
+ *
17
+ * After recording, the ESM snapshots will already be updated as that happens
18
+ * in one go, but you musty manually update the CJS snapshots by running:
19
+ *
20
+ * FRODO_DEBUG=1 npm run test:update VariablesApi
21
+ *
22
+ * 3. Test your changes
23
+ *
24
+ * If 1 and 2 didn't produce any errors, you are ready to run the tests in
25
+ * replay mode and make sure they all succeed as well:
26
+ *
27
+ * npm run test VariablesApi
28
+ *
29
+ * Note: FRODO_DEBUG=1 is optional and enables debug logging for some output
30
+ * in case things don't function as expected
31
+ */
1
32
  import { VariablesRaw } from '../index';
2
- import autoSetupPolly from '../utils/AutoSetupPolly';
3
- const pollyContext = autoSetupPolly();
33
+ import { autoSetupPolly } from '../utils/AutoSetupPolly';
34
+ autoSetupPolly();
4
35
  describe('VariablesApi', () => {
5
- describe('VariablesApi - getVariables()', () => {
6
- test('getVariables() 0: Method is implemented', async () => {
36
+ describe('getVariables()', () => {
37
+ test('0: Method is implemented', async () => {
7
38
  expect(VariablesRaw.getVariables).toBeDefined();
8
39
  });
9
- test('getVariables() 1: Get all variables - success', async () => {
40
+ test('1: Get all variables - success', async () => {
10
41
  const response = await VariablesRaw.getVariables();
11
42
  expect(response).toMatchSnapshot();
12
43
  });
13
- test('getVariables() 2: Get all variables - error', async () => {
14
- try {
15
- await VariablesRaw.getVariables();
16
- } catch (error) {
17
- expect(error.response.data).toMatchSnapshot();
18
- }
19
- });
20
44
  });
21
- describe('VariablesApi - getVariable()', () => {
22
- test('getVariable() 0: Method is implemented', async () => {
45
+ describe('getVariable()', () => {
46
+ test('0: Method is implemented', async () => {
23
47
  expect(VariablesRaw.getVariable).toBeDefined();
24
48
  });
25
- test('getVariable() 1: Get existing variable: esv-volkerstestvariable1', async () => {
49
+ test('1: Get existing variable: esv-volkerstestvariable1', async () => {
26
50
  const response = await VariablesRaw.getVariable('esv-volkerstestvariable1');
27
51
  expect(response).toMatchSnapshot();
28
52
  });
29
- test('getVariable() 2: Get non-existing variable: esv-does-not-exist', async () => {
53
+ test('2: Get non-existing variable: esv-does-not-exist', async () => {
30
54
  try {
31
55
  await VariablesRaw.getVariable('esv-does-not-exist');
32
56
  } catch (error) {
33
- // expect(error.response.status).toBe(404);
34
57
  expect(error.response.data).toMatchSnapshot();
35
58
  }
36
59
  });
37
60
  });
38
- describe('VariablesApi - putVariable()', () => {
39
- test('putVariable() 0: Method is implemented', async () => {
61
+ describe('putVariable()', () => {
62
+ test('0: Method is implemented', async () => {
40
63
  expect(VariablesRaw.putVariable).toBeDefined();
41
64
  });
42
- test('putVariable() 1: Create variable: esv-volkerstestvariable2 - success', async () => {
65
+ test('1: Create variable: esv-volkerstestvariable2 - success', async () => {
43
66
  const response = await VariablesRaw.putVariable('esv-volkerstestvariable2', "Volker's Test Variable Value", "Volker's Test Variable Description");
44
67
  expect(response).toMatchSnapshot();
45
68
  });
46
- test('putVariable() 2: Create variable: esv-volkerstestvariable2 - error', async () => {
47
- try {
48
- await VariablesRaw.putVariable('esv-volkerstestvariable2', "Volker's Test Variable Value", "Volker's Test Variable Description");
49
- } catch (error) {
50
- expect(error.response.data).toMatchSnapshot();
51
- }
52
- });
53
69
  });
54
- describe('VariablesApi - setVariableDescription()', () => {
55
- test('setVariableDescription() 0: Method is implemented', async () => {
70
+ describe('setVariableDescription()', () => {
71
+ test('0: Method is implemented', async () => {
56
72
  expect(VariablesRaw.setVariableDescription).toBeDefined();
57
73
  });
58
- test('setVariableDescription() 1: Set variable description: esv-volkerstestvariable2 - success', async () => {
74
+ test('1: Set variable description: esv-volkerstestvariable2 - success', async () => {
59
75
  const response = await VariablesRaw.setVariableDescription('esv-volkerstestvariable2', "Volker's Updated Test Secret Description");
60
76
  expect(response).toMatchSnapshot();
61
77
  });
62
- test('setVariableDescription() 2: Set variable description: esv-volkerstestvariable3 - error', async () => {
78
+ test('2: Set variable description: esv-volkerstestvariable3 - error', async () => {
63
79
  try {
64
80
  await VariablesRaw.setVariableDescription('esv-volkerstestvariable3', "Volker's Updated Test Secret Description");
65
81
  } catch (error) {
@@ -67,15 +83,15 @@ describe('VariablesApi', () => {
67
83
  }
68
84
  });
69
85
  });
70
- describe('VariablesApi - deleteVariable()', () => {
71
- test('deleteVariable() 0: Method is implemented', async () => {
86
+ describe('deleteVariable()', () => {
87
+ test('0: Method is implemented', async () => {
72
88
  expect(VariablesRaw.deleteVariable).toBeDefined();
73
89
  });
74
- test('deleteVariable() 1: Delete variable: esv-volkerstestvariable2 - success', async () => {
90
+ test('1: Delete variable: esv-volkerstestvariable2 - success', async () => {
75
91
  const response = await VariablesRaw.deleteVariable('esv-volkerstestvariable2');
76
92
  expect(response).toMatchSnapshot();
77
93
  });
78
- test('deleteVariable() 2: Delete variable: esv-volkerstestvariable3 - error', async () => {
94
+ test('2: Delete variable: esv-volkerstestvariable3 - error', async () => {
79
95
  try {
80
96
  await VariablesRaw.deleteVariable('esv-volkerstestvariable3');
81
97
  } catch (error) {
@@ -87,7 +87,7 @@ function checkAndHandle2FA(payload) {
87
87
  * @param {string} deploymentType deployment type
88
88
  */
89
89
  function determineDefaultRealm(deploymentType) {
90
- if (state.getRealm() === globalConfig.DEFAULT_REALM_KEY) {
90
+ if (!state.getRealm() || state.getRealm() === globalConfig.DEFAULT_REALM_KEY) {
91
91
  state.setRealm(globalConfig.DEPLOYMENT_TYPE_REALM_MAP[deploymentType]);
92
92
  }
93
93
  }
@@ -238,6 +238,7 @@ async function getAuthCode(redirectURL, codeChallenge, codeChallengeMethod) {
238
238
  * @returns {Promise<string | null>} access token or null
239
239
  */
240
240
  async function getAccessTokenForUser() {
241
+ debugMessage(`AuthenticateOps.getAccessTokenForUser: start`);
241
242
  try {
242
243
  const verifier = encodeBase64Url(randomBytes(32));
243
244
  const challenge = encodeBase64Url(createHash('sha256').update(verifier).digest());
@@ -263,6 +264,7 @@ async function getAccessTokenForUser() {
263
264
  response = await accessToken(bodyFormData);
264
265
  }
265
266
  if ('access_token' in response.data) {
267
+ debugMessage(`AuthenticateOps.getAccessTokenForUser: end with token`);
266
268
  return response.data.access_token;
267
269
  }
268
270
  printMessage('No access token in response.', 'error');
@@ -271,6 +273,7 @@ async function getAccessTokenForUser() {
271
273
  debugMessage(`Error getting access token for user: ${error}`);
272
274
  debugMessage((_error$response2 = error.response) === null || _error$response2 === void 0 ? void 0 : _error$response2.data);
273
275
  }
276
+ debugMessage(`AuthenticateOps.getAccessTokenForUser: end without token`);
274
277
  return null;
275
278
  }
276
279
  function createPayload(serviceAccountId) {
@@ -328,6 +331,7 @@ async function determineDeploymentTypeAndDefaultRealmAndVersion() {
328
331
  state.setDeploymentType(await determineDeploymentType());
329
332
  }
330
333
  determineDefaultRealm(state.getDeploymentType());
334
+ debugMessage(`AuthenticateOps.determineDeploymentTypeAndDefaultRealmAndVersion: realm=${state.getRealm()}, type=${state.getDeploymentType()}`);
331
335
  const versionInfo = (await getServerVersionInfo()).data;
332
336
 
333
337
  // https://github.com/rockcarver/frodo-cli/issues/109
@@ -351,6 +355,7 @@ async function getLoggedInSubject() {
351
355
  * @returns {Promise<boolean>} true if tokens were successfully obtained, false otherwise
352
356
  */
353
357
  export async function getTokens() {
358
+ debugMessage(`AuthenticateOps.getTokens: start`);
354
359
  if (!state.getHost()) {
355
360
  printMessage(`No host specified and FRODO_HOST env variable not set!`, 'error');
356
361
  return false;
@@ -393,10 +398,17 @@ export async function getTokens() {
393
398
  const token = await authenticate(state.getUsername(), state.getPassword());
394
399
  if (token) state.setCookieValue(token);
395
400
  await determineDeploymentTypeAndDefaultRealmAndVersion();
401
+ debugMessage(`AuthenticateOps.getTokens: before bearer`);
402
+ debugMessage(`AuthenticateOps.getTokens: cookie=${state.getCookieValue()}`);
403
+ debugMessage(`AuthenticateOps.getTokens: bearer=${state.getBearerToken()}`);
404
+ debugMessage(`AuthenticateOps.getTokens: type=${state.getDeploymentType()}`);
405
+ debugMessage(`AuthenticateOps.getTokens: condition=${state.getCookieValue() && !state.getBearerToken() && (state.getDeploymentType() === globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY || state.getDeploymentType() === globalConfig.FORGEOPS_DEPLOYMENT_TYPE_KEY)}`);
396
406
  if (state.getCookieValue() && !state.getBearerToken() && (state.getDeploymentType() === globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY || state.getDeploymentType() === globalConfig.FORGEOPS_DEPLOYMENT_TYPE_KEY)) {
407
+ debugMessage(`AuthenticateOps.getTokens: in bearer`);
397
408
  const accessToken = await getAccessTokenForUser();
398
409
  if (accessToken) state.setBearerToken(accessToken);
399
410
  }
411
+ debugMessage(`AuthenticateOps.getTokens: after bearer`);
400
412
  }
401
413
  // incomplete or no credentials
402
414
  else {
@@ -406,6 +418,7 @@ export async function getTokens() {
406
418
  if (state.getCookieValue() || state.getUseBearerTokenForAmApis() && state.getBearerToken()) {
407
419
  // https://github.com/rockcarver/frodo-cli/issues/102
408
420
  printMessage(`Connected to ${state.getHost()} [${state.getRealm() ? state.getRealm() : 'root'}] as ${await getLoggedInSubject()}`, 'info');
421
+ debugMessage(`AuthenticateOps.getTokens: end with tokens`);
409
422
  return true;
410
423
  }
411
424
  } catch (error) {
@@ -421,6 +434,7 @@ export async function getTokens() {
421
434
  // stack trace
422
435
  debugMessage(error.stack || new Error().stack);
423
436
  }
437
+ debugMessage(`AuthenticateOps.getTokens: end without tokens`);
424
438
  return false;
425
439
  }
426
440
  //# sourceMappingURL=AuthenticateOps.js.map
@@ -1,16 +1,67 @@
1
+ /**
2
+ * To record and update snapshots, you must perform 3 steps in order:
3
+ *
4
+ * 1. Record API responses & update ESM snapshots
5
+ *
6
+ * To record and update ESM snapshots, you must call the test:record_noauth
7
+ * script and override all the connection state variables supplied to the
8
+ * getTokens() function by the test to connect to the env to record from:
9
+ *
10
+ * FRODO_DEBUG=1 FRODO_HOST=https://openam-volker-dev.forgeblocks.com/am \
11
+ * FRODO_USERNAME=volker.scheuber@forgerock.com FRODO_PASSWORD='S3cr3!S@uc3' \
12
+ * npm run test:record_noauth AuthenticateOps
13
+ *
14
+ * 2. Update CJS snapshots
15
+ *
16
+ * After recording, the ESM snapshots will already be updated as that happens
17
+ * in one go, but you musty manually update the CJS snapshots by running:
18
+ *
19
+ * FRODO_DEBUG=1 npm run test:update AuthenticateOps
20
+ *
21
+ * 3. Test your changes
22
+ *
23
+ * If 1 and 2 didn't produce any errors, you are ready to run the tests in
24
+ * replay mode and make sure they all succeed as well:
25
+ *
26
+ * npm run test AuthenticateOps
27
+ *
28
+ * Note: FRODO_DEBUG=1 is optional and enables debug logging for some output
29
+ * in case things don't function as expected
30
+ */
31
+ import { jest } from '@jest/globals';
1
32
  import { Authenticate, state } from '../index';
2
- describe('AuthenticationOps', () => {
3
- test('getTokens() 1: ', async () => {
4
- state.setHost(process.env.FRODO_HOST || 'frodo-dev');
5
- state.setRealm('alpha');
6
- if (process.env.FRODO_HOST && process.env.FRODO_USER && process.env.FRODO_PASSWORD) {
7
- state.setUsername(process.env.FRODO_USER);
8
- state.setPassword(process.env.FRODO_PASSWORD);
9
- }
10
- await Authenticate.getTokens();
11
- expect(state.getCookieName()).toBeTruthy();
12
- expect(state.getCookieValue()).toBeTruthy();
13
- expect(state.getBearerToken()).toBeTruthy();
33
+ import { autoSetupPolly, defaultMatchRequestsBy } from '../utils/AutoSetupPolly';
34
+
35
+ // need to modify the default matching rules to allow the mocking to work for an authentication flow.
36
+ const matchConfig = defaultMatchRequestsBy();
37
+ matchConfig.body = false; // oauth flows are tricky because of the PKCE challenge, which is different for each request
38
+ matchConfig.order = true; // since we instruct Polly not to match the body, we need to enable ordering of the requests
39
+
40
+ autoSetupPolly(matchConfig);
41
+
42
+ // Increase timeout for this test as pipeline keeps failing with error:
43
+ // Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.
44
+ jest.setTimeout(30000);
45
+ describe('AuthenticateOps', () => {
46
+ describe('getTokens()', () => {
47
+ test('0: Method is implemented', async () => {
48
+ expect(Authenticate.getTokens).toBeDefined();
49
+ });
50
+ test('1: Authenticate successfully as user', async () => {
51
+ state.setHost(process.env.FRODO_HOST || 'https://openam-frodo-dev.forgeblocks.com/am');
52
+ state.setRealm(process.env.FRODO_REALM || 'alpha');
53
+ state.setUsername(process.env.FRODO_USERNAME || 'mockUser');
54
+ state.setPassword(process.env.FRODO_PASSWORD || 'mockPassword');
55
+ const result = await Authenticate.getTokens();
56
+ expect(result).toBe(true);
57
+ expect(state.getDeploymentType()).toEqual('cloud');
58
+ expect(state.getCookieName()).toBeTruthy();
59
+ expect(state.getCookieValue()).toBeTruthy();
60
+ expect(state.getBearerToken()).toBeTruthy();
61
+ expect(state.getCookieName()).toMatchSnapshot();
62
+ expect(state.getCookieValue()).toMatchSnapshot();
63
+ expect(state.getBearerToken()).toMatchSnapshot();
64
+ });
14
65
  });
15
66
  });
16
67
  //# sourceMappingURL=AuthenticateOps.test.js.map
@@ -1,3 +1,4 @@
1
+ import { jest } from '@jest/globals';
1
2
  import axios from 'axios';
2
3
  import MockAdapter from 'axios-mock-adapter';
3
4
  import { state } from '../index';
@@ -7,6 +8,10 @@ import * as ServiceAccount from './ServiceAccountOps';
7
8
  import { mockCreateManagedObject } from '../test/mocks/ForgeRockApiMockEngine';
8
9
  import { isEqualJson } from './utils/OpsUtils';
9
10
  const mock = new MockAdapter(axios);
11
+
12
+ // Increase timeout for this test as pipeline keeps failing with error:
13
+ // Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.
14
+ jest.setTimeout(30000);
10
15
  const outputHandler = message => {
11
16
  console.log(message);
12
17
  };
@@ -4,7 +4,47 @@ import { fileURLToPath } from 'url';
4
4
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
5
  const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../package.json'), 'utf8'));
6
6
  const _state = {
7
- authenticationHeaderOverrides: {}
7
+ authenticationHeaderOverrides: {},
8
+ printHandler: message => {
9
+ if (!message) return;
10
+ if (typeof message === 'object') {
11
+ console.dir(message, {
12
+ depth: 3
13
+ });
14
+ } else {
15
+ console.log(message);
16
+ }
17
+ },
18
+ verboseHandler: message => {
19
+ if (!message) return;
20
+ if (getVerbose()) {
21
+ if (typeof message === 'object') {
22
+ console.dir(message, {
23
+ depth: 3
24
+ });
25
+ } else {
26
+ console.log(message);
27
+ }
28
+ }
29
+ },
30
+ debugHandler: message => {
31
+ if (!message) return;
32
+ if (getDebug()) {
33
+ if (typeof message === 'object') {
34
+ console.dir(message, {
35
+ depth: 6
36
+ });
37
+ } else {
38
+ console.log(message);
39
+ }
40
+ }
41
+ },
42
+ curlirizeHandler: message => {
43
+ if (!message) return;
44
+ if (getDebug()) {
45
+ console.log(message);
46
+ }
47
+ }
8
48
  };
9
49
  export const setHost = host => _state.host = host;
10
50
  export const getHost = () => _state.host || process.env.FRODO_HOST;
@@ -1,6 +1,4 @@
1
1
  {
2
- "_id": "FrodoTest",
3
- "_rev": "189750709",
4
2
  "identityResource": "managed/alpha_user",
5
3
  "uiConfig": {
6
4
  "categories": "[]"
@@ -13,30 +13,57 @@ const {
13
13
  Polly.register(NodeHttpAdapter);
14
14
  Polly.register(FSPersister);
15
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
- state.setHost('https://openam-frodo-dev.forgeblocks.com/am');
17
- state.setRealm('alpha');
18
- let recordIfMissing = true;
16
+ let recordIfMissing = false;
19
17
  let mode = MODES.REPLAY;
20
18
 
21
19
  // resolve "/home/sandeepc/work/ForgeRock/sources/frodo-lib/esm/api" to
22
20
  // "/home/sandeepc/work/ForgeRock/sources/frodo-lib/src/test/recordings"
23
21
  const recordingsDir = __dirname.replace(/^(.*\/frodo-\w{3})(.*)$/gi, '$1/src/test/mock-recordings');
24
22
  switch (process.env.FRODO_POLLY_MODE) {
23
+ // record mock responses from a real env: `npm run test:record`
25
24
  case 'record':
25
+ {
26
+ state.setHost(process.env.FRODO_HOST || 'https://openam-frodo-dev.forgeblocks.com/am');
27
+ state.setRealm(process.env.FRODO_REALM || 'alpha');
28
+ if (!(await getTokens())) throw new Error(`Unable to record mock responses from '${state.getHost()}'`);
29
+ mode = MODES.RECORD;
30
+ recordIfMissing = true;
31
+ break;
32
+ }
33
+ // record mock responses from authentication APIs (don't authenticate): `npm run test:record_noauth`
34
+ case 'record_noauth':
26
35
  mode = MODES.RECORD;
27
- await getTokens();
28
- break;
29
- case 'replay':
30
- mode = MODES.REPLAY;
31
- state.default.session.setCookieName('cookieName');
32
- state.default.session.setCookieValue('cookieValue');
36
+ recordIfMissing = true;
33
37
  break;
34
- case 'offline':
35
- mode = MODES.REPLAY;
36
- recordIfMissing = false;
38
+ // replay mock responses: `npm test`
39
+ default:
40
+ state.setHost(process.env.FRODO_HOST || 'https://openam-frodo-dev.forgeblocks.com/am');
41
+ state.setRealm(process.env.FRODO_REALM || 'alpha');
42
+ state.setCookieName('cookieName');
43
+ state.setCookieValue('cookieValue');
37
44
  break;
38
45
  }
39
- export default function autoSetupPolly() {
46
+ export function defaultMatchRequestsBy() {
47
+ return JSON.parse(JSON.stringify({
48
+ method: true,
49
+ headers: false,
50
+ // do not match headers, because "Authorization" header is sent only at recording time
51
+ body: true,
52
+ order: false,
53
+ url: {
54
+ protocol: true,
55
+ username: false,
56
+ password: false,
57
+ hostname: false,
58
+ // we will record from different envs but run tests always against `frodo-dev`
59
+ port: false,
60
+ pathname: true,
61
+ query: true,
62
+ hash: true
63
+ }
64
+ }));
65
+ }
66
+ export function autoSetupPolly(matchRequestsBy = defaultMatchRequestsBy()) {
40
67
  return setupPolly({
41
68
  adapters: ['node-http'],
42
69
  mode,
@@ -50,23 +77,7 @@ export default function autoSetupPolly() {
50
77
  recordingsDir
51
78
  }
52
79
  },
53
- matchRequestsBy: {
54
- method: true,
55
- headers: false,
56
- // do not match headers, because "Authorization" header is sent only at recording time
57
- body: true,
58
- order: false,
59
- url: {
60
- protocol: true,
61
- username: false,
62
- password: false,
63
- hostname: true,
64
- port: false,
65
- pathname: true,
66
- query: true,
67
- hash: true
68
- }
69
- }
80
+ matchRequestsBy
70
81
  });
71
82
  }
72
83
  //# sourceMappingURL=AutoSetupPolly.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rockcarver/frodo-lib",
3
- "version": "0.17.0",
3
+ "version": "0.17.2-0",
4
4
  "type": "commonjs",
5
5
  "main": "./cjs/index.js",
6
6
  "module": "./esm/index.mjs",
@@ -15,10 +15,10 @@
15
15
  "scripts": {
16
16
  "test": "npx gulp && node --no-warnings --experimental-vm-modules --experimental-specifier-resolution=node node_modules/jest/bin/jest.js --silent",
17
17
  "test:only": "node --no-warnings --experimental-vm-modules --experimental-specifier-resolution=node node_modules/jest/bin/jest.js --silent",
18
- "test:record": "npm run test:record:esm && npm run test:update:cjs",
19
- "test:record:esm": "FRODO_POLLY_MODE=record node --no-warnings --experimental-vm-modules --experimental-specifier-resolution=node node_modules/jest/bin/jest.js --silent esm -u",
20
- "test:update:cjs": "node --no-warnings --experimental-vm-modules --experimental-specifier-resolution=node node_modules/jest/bin/jest.js --silent cjs -u",
21
18
  "test:debug": "node --no-warnings --experimental-vm-modules --experimental-specifier-resolution=node node_modules/jest/bin/jest.js --verbose=true --silent=false",
19
+ "test:record": "FRODO_POLLY_MODE=record node --no-warnings --experimental-vm-modules --experimental-specifier-resolution=node node_modules/jest/bin/jest.js --verbose=true --silent=false --updateSnapshot --testPathIgnorePatterns cjs --testPathPattern",
20
+ "test:record_noauth": "FRODO_POLLY_MODE=record_noauth node --no-warnings --experimental-vm-modules --experimental-specifier-resolution=node node_modules/jest/bin/jest.js --verbose=true --silent=false --updateSnapshot --testPathIgnorePatterns cjs --testPathPattern",
21
+ "test:update": "node --no-warnings --experimental-vm-modules --experimental-specifier-resolution=node node_modules/jest/bin/jest.js --verbose=true --silent=false --updateSnapshot --testPathIgnorePatterns esm --testPathPattern",
22
22
  "lint": "npx eslint --ext .ts --ignore-path .gitignore .",
23
23
  "build": "npx gulp",
24
24
  "watch": "npx gulp watch"
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ops/AuthenticateOps.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,MAAM,EAAwB,MAAM,WAAW,CAAC;AA+SzD;;;;;GAKG;AACH,wBAAsB,+BAA+B,CACnD,gBAAgB,EAAE,MAAM,EACxB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAqBxB;AAkCD;;;;GAIG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CA6GlD","file":"AuthenticateOps.d.ts","sourcesContent":["import url from 'url';\nimport { createHash, randomBytes } from 'crypto';\nimport readlineSync from 'readline-sync';\nimport { encodeBase64Url } from '../api/utils/Base64';\nimport * as state from '../shared/State';\nimport * as globalConfig from '../storage/StaticStorage';\nimport { debugMessage, printMessage, verboseMessage } from './utils/Console';\nimport { getServerInfo, getServerVersionInfo } from '../api/ServerInfoApi';\nimport { step } from '../api/AuthenticateApi';\nimport { accessToken, authorize } from '../api/OAuth2OIDCApi';\nimport { getConnectionProfile } from './ConnectionProfileOps';\nimport { v4 } from 'uuid';\nimport { parseUrl } from '../api/utils/ApiUtils';\nimport { JwkRsa, createSignedJwtToken } from './JoseOps';\nimport { getManagedObject } from '../api/ManagedObjectApi';\n\nconst adminClientPassword = 'doesnotmatter';\nconst redirectUrlTemplate = '/platform/appAuthHelperRedirect.html';\n\nconst idmAdminScopes = 'fr:idm:* openid';\nconst serviceAccountScopes = 'fr:am:* fr:idm:* fr:idc:esv:*';\n\nlet adminClientId = 'idmAdminClient';\n\n/**\n * Helper function to get cookie name\n * @returns {String} cookie name\n */\nasync function determineCookieName() {\n try {\n const { data } = await getServerInfo();\n debugMessage(\n `AuthenticateOps.getCookieName: cookieName=${data.cookieName}`\n );\n return data.cookieName;\n } catch (error) {\n printMessage(`Error getting cookie name: ${error}`, 'error');\n debugMessage(error.stack);\n return null;\n }\n}\n\n/**\n * Helper function to determine if this is a setup mfa prompt in the ID Cloud tenant admin login journey\n * @param {Object} payload response from the previous authentication journey step\n * @returns {Object} an object indicating if 2fa is required and the original payload\n */\nfunction checkAndHandle2FA(payload) {\n // let skippable = false;\n if ('callbacks' in payload) {\n for (const element of payload.callbacks) {\n if (element.type === 'HiddenValueCallback') {\n if (element.input[0].value.includes('skip')) {\n // skippable = true;\n element.input[0].value = 'Skip';\n return {\n need2fa: true,\n payload,\n };\n }\n }\n if (element.type === 'NameCallback') {\n if (element.output[0].value.includes('code')) {\n // skippable = false;\n printMessage('2FA is enabled and required for this user...');\n const code = readlineSync.question(`${element.output[0].value}: `);\n element.input[0].value = code;\n return {\n need2fa: true,\n payload,\n };\n }\n }\n }\n // console.info(\"NO2FA\");\n return {\n need2fa: false,\n payload,\n };\n }\n // console.info(\"NO2FA\");\n return {\n need2fa: false,\n payload,\n };\n}\n\n/**\n * Helper function to set the default realm by deployment type\n * @param {string} deploymentType deployment type\n */\nfunction determineDefaultRealm(deploymentType: string) {\n if (state.getRealm() === globalConfig.DEFAULT_REALM_KEY) {\n state.setRealm(globalConfig.DEPLOYMENT_TYPE_REALM_MAP[deploymentType]);\n }\n}\n\n/**\n * Helper function to determine the deployment type\n * @returns {Promise<string>} deployment type\n */\nasync function determineDeploymentType(): Promise<string> {\n const cookieValue = state.getCookieValue();\n // https://bugster.forgerock.org/jira/browse/FRAAS-13018\n // There is a chance that this will be blocked due to security concerns and thus is probably best not to keep active\n // if (!cookieValue && getUseBearerTokenForAmApis()) {\n // const token = await getTokenInfo();\n // cookieValue = token.sessionToken;\n // setCookieValue(cookieValue);\n // }\n\n // if we are using a service account, we know it's cloud\n if (state.getUseBearerTokenForAmApis())\n return globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY;\n\n const fidcClientId = 'idmAdminClient';\n const forgeopsClientId = 'idm-admin-ui';\n\n const verifier = encodeBase64Url(randomBytes(32));\n const challenge = encodeBase64Url(\n createHash('sha256').update(verifier).digest()\n );\n const challengeMethod = 'S256';\n const redirectURL = url.resolve(state.getHost(), redirectUrlTemplate);\n\n const config = {\n maxRedirects: 0,\n headers: {\n [state.getCookieName()]: state.getCookieValue(),\n },\n };\n let bodyFormData = `redirect_uri=${redirectURL}&scope=${idmAdminScopes}&response_type=code&client_id=${fidcClientId}&csrf=${cookieValue}&decision=allow&code_challenge=${challenge}&code_challenge_method=${challengeMethod}`;\n\n let deploymentType = globalConfig.CLASSIC_DEPLOYMENT_TYPE_KEY;\n try {\n await authorize(bodyFormData, config);\n } catch (e) {\n // debugMessage(e.response);\n if (\n e.response?.status === 302 &&\n e.response.headers?.location?.indexOf('code=') > -1\n ) {\n verboseMessage(`ForgeRock Identity Cloud`['brightCyan'] + ` detected.`);\n deploymentType = globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY;\n } else {\n try {\n bodyFormData = `redirect_uri=${redirectURL}&scope=${idmAdminScopes}&response_type=code&client_id=${forgeopsClientId}&csrf=${state.getCookieValue()}&decision=allow&code_challenge=${challenge}&code_challenge_method=${challengeMethod}`;\n await authorize(bodyFormData, config);\n } catch (ex) {\n if (\n ex.response?.status === 302 &&\n ex.response.headers?.location?.indexOf('code=') > -1\n ) {\n adminClientId = forgeopsClientId;\n verboseMessage(`ForgeOps deployment`['brightCyan'] + ` detected.`);\n deploymentType = globalConfig.FORGEOPS_DEPLOYMENT_TYPE_KEY;\n } else {\n verboseMessage(`Classic deployment`['brightCyan'] + ` detected.`);\n }\n }\n }\n }\n return deploymentType;\n}\n\n/**\n * Helper function to extract the semantic version string from a version info object\n * @param {Object} versionInfo version info object\n * @returns {String} semantic version\n */\nasync function getSemanticVersion(versionInfo) {\n if ('version' in versionInfo) {\n const versionString = versionInfo.version;\n const rx = /([\\d]\\.[\\d]\\.[\\d](\\.[\\d])*)/g;\n const version = versionString.match(rx);\n return version[0];\n }\n throw new Error('Cannot extract semantic version from version info object.');\n}\n\n/**\n * Helper function to authenticate and obtain and store session cookie\n * @returns {string} Session token or null\n */\nasync function authenticate(\n username: string,\n password: string\n): Promise<string> {\n const config = {\n headers: {\n 'X-OpenAM-Username': username,\n 'X-OpenAM-Password': password,\n },\n };\n const response1 = await step({}, config);\n const skip2FA = checkAndHandle2FA(response1);\n let response2 = {};\n if (skip2FA.need2fa) {\n response2 = await step(skip2FA.payload);\n } else {\n response2 = skip2FA.payload;\n }\n if ('tokenId' in response2) {\n return response2['tokenId'] as string;\n }\n return null;\n}\n\n/**\n * Helper function to obtain an oauth2 authorization code\n * @param {string} redirectURL oauth2 redirect uri\n * @param {string} codeChallenge PKCE code challenge\n * @param {string} codeChallengeMethod PKCE code challenge method\n * @returns {string} oauth2 authorization code or null\n */\nasync function getAuthCode(redirectURL, codeChallenge, codeChallengeMethod) {\n try {\n const bodyFormData = `redirect_uri=${redirectURL}&scope=${idmAdminScopes}&response_type=code&client_id=${adminClientId}&csrf=${state.getCookieValue()}&decision=allow&code_challenge=${codeChallenge}&code_challenge_method=${codeChallengeMethod}`;\n const config = {\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n maxRedirects: 0,\n };\n let response = undefined;\n try {\n response = await authorize(bodyFormData, config);\n } catch (error) {\n response = error.response;\n }\n if (response.status < 200 || response.status > 399) {\n printMessage('error getting auth code', 'error');\n printMessage(\n 'likely cause: mismatched parameters with OAuth client config',\n 'error'\n );\n return null;\n }\n const redirectLocationURL = response.headers?.location;\n const queryObject = url.parse(redirectLocationURL, true).query;\n if ('code' in queryObject) {\n return queryObject.code;\n }\n printMessage('auth code not found', 'error');\n return null;\n } catch (error) {\n printMessage(`error getting auth code - ${error.message}`, 'error');\n printMessage(error.response?.data, 'error');\n debugMessage(error.stack);\n return null;\n }\n}\n\n/**\n * Helper function to obtain oauth2 access token\n * @returns {Promise<string | null>} access token or null\n */\nasync function getAccessTokenForUser(): Promise<string | null> {\n try {\n const verifier = encodeBase64Url(randomBytes(32));\n const challenge = encodeBase64Url(\n createHash('sha256').update(verifier).digest()\n );\n const challengeMethod = 'S256';\n const redirectURL = url.resolve(state.getHost(), redirectUrlTemplate);\n const authCode = await getAuthCode(redirectURL, challenge, challengeMethod);\n if (authCode == null) {\n printMessage('error getting auth code', 'error');\n return null;\n }\n let response = null;\n if (state.getDeploymentType() === globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY) {\n const config = {\n auth: {\n username: adminClientId,\n password: adminClientPassword,\n },\n };\n const bodyFormData = `redirect_uri=${redirectURL}&grant_type=authorization_code&code=${authCode}&code_verifier=${verifier}`;\n response = await accessToken(bodyFormData, config);\n } else {\n const bodyFormData = `client_id=${adminClientId}&redirect_uri=${redirectURL}&grant_type=authorization_code&code=${authCode}&code_verifier=${verifier}`;\n response = await accessToken(bodyFormData);\n }\n if ('access_token' in response.data) {\n return response.data.access_token;\n }\n printMessage('No access token in response.', 'error');\n } catch (error) {\n debugMessage(`Error getting access token for user: ${error}`);\n debugMessage(error.response?.data);\n }\n return null;\n}\n\nfunction createPayload(serviceAccountId: string) {\n const u = parseUrl(state.getHost());\n const aud = `${u.origin}:${\n u.port ? u.port : u.protocol === 'https' ? '443' : '80'\n }${u.pathname}/oauth2/access_token`;\n\n // Cross platform way of setting JWT expiry time 3 minutes in the future, expressed as number of seconds since EPOCH\n const exp = Math.floor(new Date().getTime() / 1000 + 180);\n\n // A unique ID for the JWT which is required when requesting the openid scope\n const jti = v4();\n\n const iss = serviceAccountId;\n const sub = serviceAccountId;\n\n // Create the payload for our bearer token\n const payload = { iss, sub, aud, exp, jti };\n\n return payload;\n}\n\n/**\n * Get access token for service account\n * @param {string} serviceAccountId UUID of service account\n * @param {JwkRsa} jwk Java Wek Key\n * @returns {string | null} Access token or null\n */\nexport async function getAccessTokenForServiceAccount(\n serviceAccountId: string,\n jwk: JwkRsa\n): Promise<string | null> {\n debugMessage(`AuthenticateOps.getAccessTokenForServiceAccount: start`);\n const payload = createPayload(serviceAccountId);\n debugMessage(`AuthenticateOps.getAccessTokenForServiceAccount: payload:`);\n debugMessage(payload);\n const jwt = await createSignedJwtToken(payload, jwk);\n debugMessage(`AuthenticateOps.getAccessTokenForServiceAccount: jwt:`);\n debugMessage(jwt);\n const bodyFormData = `assertion=${jwt}&client_id=service-account&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&scope=${serviceAccountScopes}`;\n const response = await accessToken(bodyFormData);\n if ('access_token' in response.data) {\n debugMessage(`AuthenticateOps.getAccessTokenForServiceAccount: token:`);\n debugMessage(response.data.access_token);\n debugMessage(`AuthenticateOps.getAccessTokenForServiceAccount: end`);\n return response.data.access_token;\n }\n debugMessage(\n `AuthenticateOps.getAccessTokenForServiceAccount: No access token in response.`\n );\n debugMessage(`AuthenticateOps.getAccessTokenForServiceAccount: end`);\n return null;\n}\n\nasync function determineDeploymentTypeAndDefaultRealmAndVersion() {\n debugMessage(\n `AuthenticateOps.determineDeploymentTypeAndDefaultRealmAndVersion: start`\n );\n if (!state.getDeploymentType()) {\n state.setDeploymentType(await determineDeploymentType());\n }\n determineDefaultRealm(state.getDeploymentType());\n\n const versionInfo = (await getServerVersionInfo()).data;\n\n // https://github.com/rockcarver/frodo-cli/issues/109\n debugMessage(`Full version: ${versionInfo.fullVersion}`);\n\n const version = await getSemanticVersion(versionInfo);\n state.setAmVersion(version);\n debugMessage(\n `AuthenticateOps.determineDeploymentTypeAndDefaultRealmAndVersion: end`\n );\n}\n\nasync function getLoggedInSubject(): Promise<string> {\n let subjectString = `user ${state.getUsername()}`;\n if (state.getUseBearerTokenForAmApis()) {\n const name = (\n await getManagedObject('svcacct', state.getServiceAccountId(), ['name'])\n ).data.name;\n subjectString = `service account ${name} [${state.getServiceAccountId()}]`;\n }\n return subjectString;\n}\n\n/**\n * Get tokens\n * @param {boolean} save true to save a connection profile upon successful authentication, false otherwise\n * @returns {Promise<boolean>} true if tokens were successfully obtained, false otherwise\n */\nexport async function getTokens(): Promise<boolean> {\n if (!state.getHost()) {\n printMessage(\n `No host specified and FRODO_HOST env variable not set!`,\n 'error'\n );\n return false;\n }\n try {\n // if username/password on cli are empty, try to read from connections.json\n if (\n state.getUsername() == null &&\n state.getPassword() == null &&\n !state.getServiceAccountId() &&\n !state.getServiceAccountJwk()\n ) {\n const conn = await getConnectionProfile();\n if (conn) {\n state.setHost(conn.tenant);\n state.setUsername(conn.username);\n state.setPassword(conn.password);\n state.setAuthenticationService(conn.authenticationService);\n state.setAuthenticationHeaderOverrides(\n conn.authenticationHeaderOverrides\n );\n state.setServiceAccountId(conn.svcacctId);\n state.setServiceAccountJwk(conn.svcacctJwk);\n } else {\n return false;\n }\n }\n // now that we have the full tenant URL we can lookup the cookie name\n state.setCookieName(await determineCookieName());\n\n // use service account to login?\n if (state.getServiceAccountId() && state.getServiceAccountJwk()) {\n debugMessage(\n `AuthenticateOps.getTokens: Authenticating with service account ${state.getServiceAccountId()}`\n );\n try {\n const token = await getAccessTokenForServiceAccount(\n state.getServiceAccountId(),\n state.getServiceAccountJwk()\n );\n state.setBearerToken(token);\n state.setUseBearerTokenForAmApis(true);\n await determineDeploymentTypeAndDefaultRealmAndVersion();\n } catch (saErr) {\n throw new Error(\n `Service account login error: ${\n saErr.response?.data?.error_description ||\n saErr.response?.data?.message\n }`\n );\n }\n }\n // use user account to login\n else if (state.getUsername() && state.getPassword()) {\n debugMessage(\n `AuthenticateOps.getTokens: Authenticating with user account ${state.getUsername()}`\n );\n const token = await authenticate(\n state.getUsername(),\n state.getPassword()\n );\n if (token) state.setCookieValue(token);\n await determineDeploymentTypeAndDefaultRealmAndVersion();\n if (\n state.getCookieValue() &&\n !state.getBearerToken() &&\n (state.getDeploymentType() === globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY ||\n state.getDeploymentType() ===\n globalConfig.FORGEOPS_DEPLOYMENT_TYPE_KEY)\n ) {\n const accessToken = await getAccessTokenForUser();\n if (accessToken) state.setBearerToken(accessToken);\n }\n }\n // incomplete or no credentials\n else {\n printMessage(`Incomplete or no credentials!`, 'error');\n return false;\n }\n if (\n state.getCookieValue() ||\n (state.getUseBearerTokenForAmApis() && state.getBearerToken())\n ) {\n // https://github.com/rockcarver/frodo-cli/issues/102\n printMessage(\n `Connected to ${state.getHost()} [${\n state.getRealm() ? state.getRealm() : 'root'\n }] as ${await getLoggedInSubject()}`,\n 'info'\n );\n return true;\n }\n } catch (error) {\n // regular error\n printMessage(error.message, 'error');\n // axios error am api\n printMessage(error.response?.data?.message, 'error');\n // axios error am oauth2 api\n printMessage(error.response?.data?.error_description, 'error');\n // axios error data\n debugMessage(error.response?.data);\n // stack trace\n debugMessage(error.stack || new Error().stack);\n }\n return false;\n}\n"]}
1
+ {"version":3,"sources":["../src/ops/AuthenticateOps.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,MAAM,EAAwB,MAAM,WAAW,CAAC;AAqTzD;;;;;GAKG;AACH,wBAAsB,+BAA+B,CACnD,gBAAgB,EAAE,MAAM,EACxB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAqBxB;AAqCD;;;;GAIG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAsIlD","file":"AuthenticateOps.d.ts","sourcesContent":["import url from 'url';\nimport { createHash, randomBytes } from 'crypto';\nimport readlineSync from 'readline-sync';\nimport { encodeBase64Url } from '../api/utils/Base64';\nimport * as state from '../shared/State';\nimport * as globalConfig from '../storage/StaticStorage';\nimport { debugMessage, printMessage, verboseMessage } from './utils/Console';\nimport { getServerInfo, getServerVersionInfo } from '../api/ServerInfoApi';\nimport { step } from '../api/AuthenticateApi';\nimport { accessToken, authorize } from '../api/OAuth2OIDCApi';\nimport { getConnectionProfile } from './ConnectionProfileOps';\nimport { v4 } from 'uuid';\nimport { parseUrl } from '../api/utils/ApiUtils';\nimport { JwkRsa, createSignedJwtToken } from './JoseOps';\nimport { getManagedObject } from '../api/ManagedObjectApi';\n\nconst adminClientPassword = 'doesnotmatter';\nconst redirectUrlTemplate = '/platform/appAuthHelperRedirect.html';\n\nconst idmAdminScopes = 'fr:idm:* openid';\nconst serviceAccountScopes = 'fr:am:* fr:idm:* fr:idc:esv:*';\n\nlet adminClientId = 'idmAdminClient';\n\n/**\n * Helper function to get cookie name\n * @returns {String} cookie name\n */\nasync function determineCookieName() {\n try {\n const { data } = await getServerInfo();\n debugMessage(\n `AuthenticateOps.getCookieName: cookieName=${data.cookieName}`\n );\n return data.cookieName;\n } catch (error) {\n printMessage(`Error getting cookie name: ${error}`, 'error');\n debugMessage(error.stack);\n return null;\n }\n}\n\n/**\n * Helper function to determine if this is a setup mfa prompt in the ID Cloud tenant admin login journey\n * @param {Object} payload response from the previous authentication journey step\n * @returns {Object} an object indicating if 2fa is required and the original payload\n */\nfunction checkAndHandle2FA(payload) {\n // let skippable = false;\n if ('callbacks' in payload) {\n for (const element of payload.callbacks) {\n if (element.type === 'HiddenValueCallback') {\n if (element.input[0].value.includes('skip')) {\n // skippable = true;\n element.input[0].value = 'Skip';\n return {\n need2fa: true,\n payload,\n };\n }\n }\n if (element.type === 'NameCallback') {\n if (element.output[0].value.includes('code')) {\n // skippable = false;\n printMessage('2FA is enabled and required for this user...');\n const code = readlineSync.question(`${element.output[0].value}: `);\n element.input[0].value = code;\n return {\n need2fa: true,\n payload,\n };\n }\n }\n }\n // console.info(\"NO2FA\");\n return {\n need2fa: false,\n payload,\n };\n }\n // console.info(\"NO2FA\");\n return {\n need2fa: false,\n payload,\n };\n}\n\n/**\n * Helper function to set the default realm by deployment type\n * @param {string} deploymentType deployment type\n */\nfunction determineDefaultRealm(deploymentType: string) {\n if (\n !state.getRealm() ||\n state.getRealm() === globalConfig.DEFAULT_REALM_KEY\n ) {\n state.setRealm(globalConfig.DEPLOYMENT_TYPE_REALM_MAP[deploymentType]);\n }\n}\n\n/**\n * Helper function to determine the deployment type\n * @returns {Promise<string>} deployment type\n */\nasync function determineDeploymentType(): Promise<string> {\n const cookieValue = state.getCookieValue();\n // https://bugster.forgerock.org/jira/browse/FRAAS-13018\n // There is a chance that this will be blocked due to security concerns and thus is probably best not to keep active\n // if (!cookieValue && getUseBearerTokenForAmApis()) {\n // const token = await getTokenInfo();\n // cookieValue = token.sessionToken;\n // setCookieValue(cookieValue);\n // }\n\n // if we are using a service account, we know it's cloud\n if (state.getUseBearerTokenForAmApis())\n return globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY;\n\n const fidcClientId = 'idmAdminClient';\n const forgeopsClientId = 'idm-admin-ui';\n\n const verifier = encodeBase64Url(randomBytes(32));\n const challenge = encodeBase64Url(\n createHash('sha256').update(verifier).digest()\n );\n const challengeMethod = 'S256';\n const redirectURL = url.resolve(state.getHost(), redirectUrlTemplate);\n\n const config = {\n maxRedirects: 0,\n headers: {\n [state.getCookieName()]: state.getCookieValue(),\n },\n };\n let bodyFormData = `redirect_uri=${redirectURL}&scope=${idmAdminScopes}&response_type=code&client_id=${fidcClientId}&csrf=${cookieValue}&decision=allow&code_challenge=${challenge}&code_challenge_method=${challengeMethod}`;\n\n let deploymentType = globalConfig.CLASSIC_DEPLOYMENT_TYPE_KEY;\n try {\n await authorize(bodyFormData, config);\n } catch (e) {\n // debugMessage(e.response);\n if (\n e.response?.status === 302 &&\n e.response.headers?.location?.indexOf('code=') > -1\n ) {\n verboseMessage(`ForgeRock Identity Cloud`['brightCyan'] + ` detected.`);\n deploymentType = globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY;\n } else {\n try {\n bodyFormData = `redirect_uri=${redirectURL}&scope=${idmAdminScopes}&response_type=code&client_id=${forgeopsClientId}&csrf=${state.getCookieValue()}&decision=allow&code_challenge=${challenge}&code_challenge_method=${challengeMethod}`;\n await authorize(bodyFormData, config);\n } catch (ex) {\n if (\n ex.response?.status === 302 &&\n ex.response.headers?.location?.indexOf('code=') > -1\n ) {\n adminClientId = forgeopsClientId;\n verboseMessage(`ForgeOps deployment`['brightCyan'] + ` detected.`);\n deploymentType = globalConfig.FORGEOPS_DEPLOYMENT_TYPE_KEY;\n } else {\n verboseMessage(`Classic deployment`['brightCyan'] + ` detected.`);\n }\n }\n }\n }\n return deploymentType;\n}\n\n/**\n * Helper function to extract the semantic version string from a version info object\n * @param {Object} versionInfo version info object\n * @returns {String} semantic version\n */\nasync function getSemanticVersion(versionInfo) {\n if ('version' in versionInfo) {\n const versionString = versionInfo.version;\n const rx = /([\\d]\\.[\\d]\\.[\\d](\\.[\\d])*)/g;\n const version = versionString.match(rx);\n return version[0];\n }\n throw new Error('Cannot extract semantic version from version info object.');\n}\n\n/**\n * Helper function to authenticate and obtain and store session cookie\n * @returns {string} Session token or null\n */\nasync function authenticate(\n username: string,\n password: string\n): Promise<string> {\n const config = {\n headers: {\n 'X-OpenAM-Username': username,\n 'X-OpenAM-Password': password,\n },\n };\n const response1 = await step({}, config);\n const skip2FA = checkAndHandle2FA(response1);\n let response2 = {};\n if (skip2FA.need2fa) {\n response2 = await step(skip2FA.payload);\n } else {\n response2 = skip2FA.payload;\n }\n if ('tokenId' in response2) {\n return response2['tokenId'] as string;\n }\n return null;\n}\n\n/**\n * Helper function to obtain an oauth2 authorization code\n * @param {string} redirectURL oauth2 redirect uri\n * @param {string} codeChallenge PKCE code challenge\n * @param {string} codeChallengeMethod PKCE code challenge method\n * @returns {string} oauth2 authorization code or null\n */\nasync function getAuthCode(redirectURL, codeChallenge, codeChallengeMethod) {\n try {\n const bodyFormData = `redirect_uri=${redirectURL}&scope=${idmAdminScopes}&response_type=code&client_id=${adminClientId}&csrf=${state.getCookieValue()}&decision=allow&code_challenge=${codeChallenge}&code_challenge_method=${codeChallengeMethod}`;\n const config = {\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n maxRedirects: 0,\n };\n let response = undefined;\n try {\n response = await authorize(bodyFormData, config);\n } catch (error) {\n response = error.response;\n }\n if (response.status < 200 || response.status > 399) {\n printMessage('error getting auth code', 'error');\n printMessage(\n 'likely cause: mismatched parameters with OAuth client config',\n 'error'\n );\n return null;\n }\n const redirectLocationURL = response.headers?.location;\n const queryObject = url.parse(redirectLocationURL, true).query;\n if ('code' in queryObject) {\n return queryObject.code;\n }\n printMessage('auth code not found', 'error');\n return null;\n } catch (error) {\n printMessage(`error getting auth code - ${error.message}`, 'error');\n printMessage(error.response?.data, 'error');\n debugMessage(error.stack);\n return null;\n }\n}\n\n/**\n * Helper function to obtain oauth2 access token\n * @returns {Promise<string | null>} access token or null\n */\nasync function getAccessTokenForUser(): Promise<string | null> {\n debugMessage(`AuthenticateOps.getAccessTokenForUser: start`);\n try {\n const verifier = encodeBase64Url(randomBytes(32));\n const challenge = encodeBase64Url(\n createHash('sha256').update(verifier).digest()\n );\n const challengeMethod = 'S256';\n const redirectURL = url.resolve(state.getHost(), redirectUrlTemplate);\n const authCode = await getAuthCode(redirectURL, challenge, challengeMethod);\n if (authCode == null) {\n printMessage('error getting auth code', 'error');\n return null;\n }\n let response = null;\n if (state.getDeploymentType() === globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY) {\n const config = {\n auth: {\n username: adminClientId,\n password: adminClientPassword,\n },\n };\n const bodyFormData = `redirect_uri=${redirectURL}&grant_type=authorization_code&code=${authCode}&code_verifier=${verifier}`;\n response = await accessToken(bodyFormData, config);\n } else {\n const bodyFormData = `client_id=${adminClientId}&redirect_uri=${redirectURL}&grant_type=authorization_code&code=${authCode}&code_verifier=${verifier}`;\n response = await accessToken(bodyFormData);\n }\n if ('access_token' in response.data) {\n debugMessage(`AuthenticateOps.getAccessTokenForUser: end with token`);\n return response.data.access_token;\n }\n printMessage('No access token in response.', 'error');\n } catch (error) {\n debugMessage(`Error getting access token for user: ${error}`);\n debugMessage(error.response?.data);\n }\n debugMessage(`AuthenticateOps.getAccessTokenForUser: end without token`);\n return null;\n}\n\nfunction createPayload(serviceAccountId: string) {\n const u = parseUrl(state.getHost());\n const aud = `${u.origin}:${\n u.port ? u.port : u.protocol === 'https' ? '443' : '80'\n }${u.pathname}/oauth2/access_token`;\n\n // Cross platform way of setting JWT expiry time 3 minutes in the future, expressed as number of seconds since EPOCH\n const exp = Math.floor(new Date().getTime() / 1000 + 180);\n\n // A unique ID for the JWT which is required when requesting the openid scope\n const jti = v4();\n\n const iss = serviceAccountId;\n const sub = serviceAccountId;\n\n // Create the payload for our bearer token\n const payload = { iss, sub, aud, exp, jti };\n\n return payload;\n}\n\n/**\n * Get access token for service account\n * @param {string} serviceAccountId UUID of service account\n * @param {JwkRsa} jwk Java Wek Key\n * @returns {string | null} Access token or null\n */\nexport async function getAccessTokenForServiceAccount(\n serviceAccountId: string,\n jwk: JwkRsa\n): Promise<string | null> {\n debugMessage(`AuthenticateOps.getAccessTokenForServiceAccount: start`);\n const payload = createPayload(serviceAccountId);\n debugMessage(`AuthenticateOps.getAccessTokenForServiceAccount: payload:`);\n debugMessage(payload);\n const jwt = await createSignedJwtToken(payload, jwk);\n debugMessage(`AuthenticateOps.getAccessTokenForServiceAccount: jwt:`);\n debugMessage(jwt);\n const bodyFormData = `assertion=${jwt}&client_id=service-account&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&scope=${serviceAccountScopes}`;\n const response = await accessToken(bodyFormData);\n if ('access_token' in response.data) {\n debugMessage(`AuthenticateOps.getAccessTokenForServiceAccount: token:`);\n debugMessage(response.data.access_token);\n debugMessage(`AuthenticateOps.getAccessTokenForServiceAccount: end`);\n return response.data.access_token;\n }\n debugMessage(\n `AuthenticateOps.getAccessTokenForServiceAccount: No access token in response.`\n );\n debugMessage(`AuthenticateOps.getAccessTokenForServiceAccount: end`);\n return null;\n}\n\nasync function determineDeploymentTypeAndDefaultRealmAndVersion() {\n debugMessage(\n `AuthenticateOps.determineDeploymentTypeAndDefaultRealmAndVersion: start`\n );\n if (!state.getDeploymentType()) {\n state.setDeploymentType(await determineDeploymentType());\n }\n determineDefaultRealm(state.getDeploymentType());\n debugMessage(\n `AuthenticateOps.determineDeploymentTypeAndDefaultRealmAndVersion: realm=${state.getRealm()}, type=${state.getDeploymentType()}`\n );\n\n const versionInfo = (await getServerVersionInfo()).data;\n\n // https://github.com/rockcarver/frodo-cli/issues/109\n debugMessage(`Full version: ${versionInfo.fullVersion}`);\n\n const version = await getSemanticVersion(versionInfo);\n state.setAmVersion(version);\n debugMessage(\n `AuthenticateOps.determineDeploymentTypeAndDefaultRealmAndVersion: end`\n );\n}\n\nasync function getLoggedInSubject(): Promise<string> {\n let subjectString = `user ${state.getUsername()}`;\n if (state.getUseBearerTokenForAmApis()) {\n const name = (\n await getManagedObject('svcacct', state.getServiceAccountId(), ['name'])\n ).data.name;\n subjectString = `service account ${name} [${state.getServiceAccountId()}]`;\n }\n return subjectString;\n}\n\n/**\n * Get tokens\n * @param {boolean} save true to save a connection profile upon successful authentication, false otherwise\n * @returns {Promise<boolean>} true if tokens were successfully obtained, false otherwise\n */\nexport async function getTokens(): Promise<boolean> {\n debugMessage(`AuthenticateOps.getTokens: start`);\n if (!state.getHost()) {\n printMessage(\n `No host specified and FRODO_HOST env variable not set!`,\n 'error'\n );\n return false;\n }\n try {\n // if username/password on cli are empty, try to read from connections.json\n if (\n state.getUsername() == null &&\n state.getPassword() == null &&\n !state.getServiceAccountId() &&\n !state.getServiceAccountJwk()\n ) {\n const conn = await getConnectionProfile();\n if (conn) {\n state.setHost(conn.tenant);\n state.setUsername(conn.username);\n state.setPassword(conn.password);\n state.setAuthenticationService(conn.authenticationService);\n state.setAuthenticationHeaderOverrides(\n conn.authenticationHeaderOverrides\n );\n state.setServiceAccountId(conn.svcacctId);\n state.setServiceAccountJwk(conn.svcacctJwk);\n } else {\n return false;\n }\n }\n // now that we have the full tenant URL we can lookup the cookie name\n state.setCookieName(await determineCookieName());\n\n // use service account to login?\n if (state.getServiceAccountId() && state.getServiceAccountJwk()) {\n debugMessage(\n `AuthenticateOps.getTokens: Authenticating with service account ${state.getServiceAccountId()}`\n );\n try {\n const token = await getAccessTokenForServiceAccount(\n state.getServiceAccountId(),\n state.getServiceAccountJwk()\n );\n state.setBearerToken(token);\n state.setUseBearerTokenForAmApis(true);\n await determineDeploymentTypeAndDefaultRealmAndVersion();\n } catch (saErr) {\n throw new Error(\n `Service account login error: ${\n saErr.response?.data?.error_description ||\n saErr.response?.data?.message\n }`\n );\n }\n }\n // use user account to login\n else if (state.getUsername() && state.getPassword()) {\n debugMessage(\n `AuthenticateOps.getTokens: Authenticating with user account ${state.getUsername()}`\n );\n const token = await authenticate(\n state.getUsername(),\n state.getPassword()\n );\n if (token) state.setCookieValue(token);\n await determineDeploymentTypeAndDefaultRealmAndVersion();\n debugMessage(`AuthenticateOps.getTokens: before bearer`);\n debugMessage(\n `AuthenticateOps.getTokens: cookie=${state.getCookieValue()}`\n );\n debugMessage(\n `AuthenticateOps.getTokens: bearer=${state.getBearerToken()}`\n );\n debugMessage(\n `AuthenticateOps.getTokens: type=${state.getDeploymentType()}`\n );\n debugMessage(\n `AuthenticateOps.getTokens: condition=${\n state.getCookieValue() &&\n !state.getBearerToken() &&\n (state.getDeploymentType() ===\n globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY ||\n state.getDeploymentType() ===\n globalConfig.FORGEOPS_DEPLOYMENT_TYPE_KEY)\n }`\n );\n if (\n state.getCookieValue() &&\n !state.getBearerToken() &&\n (state.getDeploymentType() === globalConfig.CLOUD_DEPLOYMENT_TYPE_KEY ||\n state.getDeploymentType() ===\n globalConfig.FORGEOPS_DEPLOYMENT_TYPE_KEY)\n ) {\n debugMessage(`AuthenticateOps.getTokens: in bearer`);\n const accessToken = await getAccessTokenForUser();\n if (accessToken) state.setBearerToken(accessToken);\n }\n debugMessage(`AuthenticateOps.getTokens: after bearer`);\n }\n // incomplete or no credentials\n else {\n printMessage(`Incomplete or no credentials!`, 'error');\n return false;\n }\n if (\n state.getCookieValue() ||\n (state.getUseBearerTokenForAmApis() && state.getBearerToken())\n ) {\n // https://github.com/rockcarver/frodo-cli/issues/102\n printMessage(\n `Connected to ${state.getHost()} [${\n state.getRealm() ? state.getRealm() : 'root'\n }] as ${await getLoggedInSubject()}`,\n 'info'\n );\n debugMessage(`AuthenticateOps.getTokens: end with tokens`);\n return true;\n }\n } catch (error) {\n // regular error\n printMessage(error.message, 'error');\n // axios error am api\n printMessage(error.response?.data?.message, 'error');\n // axios error am oauth2 api\n printMessage(error.response?.data?.error_description, 'error');\n // axios error data\n debugMessage(error.response?.data);\n // stack trace\n debugMessage(error.stack || new Error().stack);\n }\n debugMessage(`AuthenticateOps.getTokens: end without tokens`);\n return false;\n}\n"]}