Repository: elastic/synthetics
Pull request #975 (2024-10-25 17:35:29)

feat: introduce totp command for OTP token
+ Part of https://github.com/elastic/synthetics-dev/issues/367
+ Goes together with https://github.com/elastic/synthetics/pull/957 and makes the onboarding to the MFA based auth easier. 
+ Introduces a `totp` command that lets users generate a OTP token based on the provided secret that can be used to onboard the Synthetics TOTP generator to other providers. 

```ts
npx @elastic/synthetics totp secret --issuer <issuer> --label <label>

// prints
OTP Token: 123456

```

Documentation will be covered as part of https://github.com/elastic/observability-docs/issues/4334


Patch:
diff --git a/src/cli.ts b/src/cli.ts
index 23d6bec..0d03194 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -27,6 +27,8 @@
 
 import { program, Option } from 'commander';
 import { cwd } from 'process';
+import { bold } from 'kleur/colors';
+import { resolve } from 'path';
 import { CliArgs, PushOptions } from './common_types';
 import { reporters } from './reporters';
 import {
@@ -45,13 +47,13 @@ import {
   renderLocations,
   LocationCmdOptions,
 } from './locations';
-import { resolve } from 'path';
 import { Generator } from './generator';
-import { error } from './helpers';
+import { error, write } from './helpers';
 import { LocationsMap } from './locations/public-locations';
 import { createLightweightMonitors } from './push/monitor';
 import { getVersion } from './push/kibana_api';
 import { installTransform } from './core/transform';
+import { totp, TOTPCmdOptions } from './core/mfa';
 
 /* eslint-disable-next-line @typescript-eslint/no-var-requires */
 const { name, version } = require('../package.json');
@@ -192,7 +194,6 @@ program
     'the target Kibana spaces for the pushed monitors — spaces help you organise pushed monitors.'
   )
   .option('-y, --yes', 'skip all questions and run non-interactively')
-
   .addOption(pattern)
   .addOption(tags)
   .addOption(fields)
@@ -274,4 +275,20 @@ program
     }
   });
 
+// TOTP command
+program
+  .command('totp <secret>')
+  .description('Generate a Time-based One-Time token using the provided secret.')
+  .option('--issuer <issuer>', 'Provider or Service the secret is associated with.')
+  .option('--label <label>', 'Account Identifier (default: SyntheticsTOTP)')
+  .action((secret, cmdOpts: TOTPCmdOptions) => {
+    try {
+      const token = totp(secret, cmdOpts)
+      write(bold(`OTP Token: ${token}`));
+    } catch (e) {
+      error(e);
+      process.exit(1);
+    }
+  });
+
 program.parse(process.argv);
diff --git a/src/core/mfa.ts b/src/core/mfa.ts
index a23f872..28a9096 100644
--- a/src/core/mfa.ts
+++ b/src/core/mfa.ts
@@ -60,6 +60,11 @@ type TOTPOptions = {
   period?: number
 };
 
+export type TOTPCmdOptions = {
+  issuer?: string
+  label?: string
+};
+
 export function totp(secret?: string, options: TOTPOptions = {}) {
   return new TOTP({ label: "SyntheticsTOTP", secret, ...options }).generate();
 }


Test patch:
diff --git a/__tests__/cli.test.ts b/__tests__/cli.test.ts
index ddb69fc..cc6a4a7 100644
--- a/__tests__/cli.test.ts
+++ b/__tests__/cli.test.ts
@@ -93,7 +93,7 @@ describe('CLI', () => {
     });
 
     it('generate mfa totp token', async () => {
-      const cli = new CLIMock(true)
+      const cli = new CLIMock()
         .stdin(
           `step('gen mfa totp', async () => {
           const token = mfa.totp('FLIIOLP3IR3W');
@@ -491,4 +491,24 @@ describe('CLI', () => {
       });
     });
   });
+
+  describe('TOTP token', () => {
+    async function runTotp(args) {
+      const cli = new CLIMock()
+        .args(['totp', ...args])
+        .run({});
+      await cli.exitCode;
+      return cli.stderr();
+    }
+
+    it('error when secret is not provided', async () => {
+      const output = await runTotp('');
+      expect(output).toContain(`error: missing required argument 'secret'`);
+    });
+
+    it('generates otp token', async () => {
+      const output = await runTotp('FLIIOLP3IR3W');
+      expect(output).toContain('OTP Token: ');
+    });
+  });
 });