From 18446152d208a1e80cb0854b55953d8b11e41183 Mon Sep 17 00:00:00 2001 From: Joni Gear <25245618+rubicola@users.noreply.github.com> Date: Fri, 3 Nov 2023 10:29:04 +1100 Subject: [PATCH 1/5] Set mfa type on login request --- src/helpers.js | 8 ++++++++ src/login.js | 10 ++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/helpers.js b/src/helpers.js index 109f8d6..2b1ba07 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -130,4 +130,12 @@ export function error(e, loader) { export function requiresMfa(e) { return e.subcode && e.subcode.startsWith('MFA required'); +} + +export function getMfaStrategy(e) { + // Extract if it's authy or authenticator + const subcodeArray = e.subcode.split(':'); + const authType = subcodeArray[1]; + + return authType; } \ No newline at end of file diff --git a/src/login.js b/src/login.js index cbadc53..64e2b7a 100644 --- a/src/login.js +++ b/src/login.js @@ -4,7 +4,7 @@ import ora from 'ora'; import { login } from './actions/auth.js'; import { updateConfig } from './config.js'; -import { log, error, informUpdate, requiresMfa } from './helpers.js'; +import { log, error, informUpdate, requiresMfa, getMfaStrategy } from './helpers.js'; export async function doLogin(message) { if (message) log(message, 'white'); @@ -38,7 +38,8 @@ export async function doLogin(message) { return loginSucceed(loginLoader, loginBody); } catch (e) { if (requiresMfa(e)) { - return await loginWith2FA(loginLoader, credentials); + const mfaType = getMfaStrategy(e) + return await loginWith2FA(loginLoader, credentials, mfaType); } else { error(e, loginLoader); return false; @@ -46,8 +47,8 @@ export async function doLogin(message) { } } -async function loginWith2FA(loginLoader, credentials) { - loginLoader.info('Your account requires 2 factor authentication.'); +async function loginWith2FA(loginLoader, credentials, mfaType) { + loginLoader.info(`Your account requires 2 factor authentication ${mfaType}`); try { const response = await inquirer.prompt([ { @@ -65,6 +66,7 @@ async function loginWith2FA(loginLoader, credentials) { const loginBody = await login({ ...credentials, + mfaType, otp: response.otp, requestAdminToken: true, }); From c087dbcd5661ad47ce025462aaf9d38112a80679 Mon Sep 17 00:00:00 2001 From: Joni Gear <25245618+rubicola@users.noreply.github.com> Date: Fri, 3 Nov 2023 11:08:59 +1100 Subject: [PATCH 2/5] Logout option --- src/actions/auth.js | 7 +++++++ src/cli.js | 6 ++++++ src/logout.js | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 src/logout.js diff --git a/src/actions/auth.js b/src/actions/auth.js index dceaf0a..be5089e 100644 --- a/src/actions/auth.js +++ b/src/actions/auth.js @@ -115,6 +115,13 @@ export async function login(body, opts = {}) { }); } +export async function logout() { + return await api({ + path: '/logout', + method: 'POST', + }); +} + export async function getToken(program, opts, warnEarly) { if (opts.$tokenFromEnv) return; let isNewToken = false; diff --git a/src/cli.js b/src/cli.js index e06b57d..721054a 100644 --- a/src/cli.js +++ b/src/cli.js @@ -21,6 +21,7 @@ const start = actionBuilder(() => import('./start.js')); const create = actionBuilder(() => import('./create.js')); const deploy = actionBuilder(() => import('./deploy.js')); const login = actionBuilder(() => import('./login.js')); +const logout = actionBuilder(() => import('./logout.js')); const local = actionBuilder(() => import('./local.js')); export async function cli() { @@ -63,6 +64,11 @@ export async function cli() { .description('Authenticate with the Raisely api') .action(login); + program + .command('logout') + .description('Logout from the Raisely api') + .action(logout); + program .command('local') .description('Start local development server for a single campaign.') diff --git a/src/logout.js b/src/logout.js new file mode 100644 index 0000000..7eee2c9 --- /dev/null +++ b/src/logout.js @@ -0,0 +1,18 @@ +import ora from 'ora'; +import { logout } from './actions/auth.js'; +import { updateConfig } from './config.js'; + +export default async function logoutAction() { + let logoutLoader = ora('Logging you out...').start(); + try { + await logout(); + logoutLoader.info(`You have been logged out`); + } catch (e) { + // don't throw error, continue + } finally { + await updateConfig({ + token: null, + }); + logoutLoader.stop() + } +} From 9f784765682f65849b958fe052c1a3a3cbe84b72 Mon Sep 17 00:00:00 2001 From: Joni Gear <25245618+rubicola@users.noreply.github.com> Date: Fri, 3 Nov 2023 11:10:06 +1100 Subject: [PATCH 3/5] Offer multiple MFA if available --- src/helpers.js | 10 +++++++++- src/login.js | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/helpers.js b/src/helpers.js index 2b1ba07..1b41ab4 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -137,5 +137,13 @@ export function getMfaStrategy(e) { const subcodeArray = e.subcode.split(':'); const authType = subcodeArray[1]; - return authType; + return { + mfaType: authType, + // if authenticator, we need to know whether to offer authy as alternative + hasAuthy: Boolean( + authType === 'AUTHY' || + (subcodeArray.length === 3 && + subcodeArray[2] === 'hasAuthy') + ) + } } \ No newline at end of file diff --git a/src/login.js b/src/login.js index 64e2b7a..3ebbf55 100644 --- a/src/login.js +++ b/src/login.js @@ -38,8 +38,8 @@ export async function doLogin(message) { return loginSucceed(loginLoader, loginBody); } catch (e) { if (requiresMfa(e)) { - const mfaType = getMfaStrategy(e) - return await loginWith2FA(loginLoader, credentials, mfaType); + const mfaStrategy = getMfaStrategy(e) + return await loginWith2FA(loginLoader, credentials, mfaStrategy); } else { error(e, loginLoader); return false; @@ -47,8 +47,13 @@ export async function doLogin(message) { } } -async function loginWith2FA(loginLoader, credentials, mfaType) { - loginLoader.info(`Your account requires 2 factor authentication ${mfaType}`); +async function loginWith2FA(loginLoader, credentials, mfaStrategy) { + loginLoader.info(`Your account requires 2 factor authentication`); + let mfaType = mfaStrategy.mfaType; + if (mfaType === 'AUTHENTICATOR_APP' && mfaStrategy.hasAuthy) { + const choiceMfa = await selectMfaType(); + mfaType = choiceMfa.mfaType; + } try { const response = await inquirer.prompt([ { @@ -77,6 +82,31 @@ async function loginWith2FA(loginLoader, credentials, mfaType) { } } +async function selectMfaType() { + const selectedMfa = await inquirer.prompt([ + { + type: 'list', + message: 'Select your preferred MFA', + name: 'mfaType', + choices: [ + { + name: 'Authenticator App', + value: 'AUTHENTICATOR_APP' + }, + { + name: 'SMS/Legacy', + value: 'AUTHY' + } + ], + validate: (value) => + value.length + ? true + : 'Please choose your preferred MFA', + }, + ]); + return selectedMfa; +} + async function loginSucceed(loginLoader, loginBody) { const { token, data: user } = loginBody; loginLoader.succeed(); From cfdc4480191ffda2b70e671caf2cf3fc4cb4963f Mon Sep 17 00:00:00 2001 From: Joni Gear <25245618+rubicola@users.noreply.github.com> Date: Fri, 3 Nov 2023 11:40:43 +1100 Subject: [PATCH 4/5] If multiple MFA and Authy selected, make login request again with mfaType to trigger Authy prompt --- src/login.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/login.js b/src/login.js index 3ebbf55..d4a0ec7 100644 --- a/src/login.js +++ b/src/login.js @@ -53,6 +53,22 @@ async function loginWith2FA(loginLoader, credentials, mfaStrategy) { if (mfaType === 'AUTHENTICATOR_APP' && mfaStrategy.hasAuthy) { const choiceMfa = await selectMfaType(); mfaType = choiceMfa.mfaType; + if (mfaType === 'AUTHY') { + // trigger login again with mfaType to send the prompt + try { + await login({ + ...credentials, + mfaType, + requestAdminToken: true, + }); + } catch (e) { + // don't throw error if just an error about missing MFA + if (!requiresMfa(e)) { + error(e, loginLoader); + return false; + } + } + } } try { const response = await inquirer.prompt([ From f9b06a13375c931eec65c5622f8b32980ad19859 Mon Sep 17 00:00:00 2001 From: Joni Gear <25245618+rubicola@users.noreply.github.com> Date: Fri, 3 Nov 2023 12:12:11 +1100 Subject: [PATCH 5/5] Bump minor version; update changelog --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b86d7d3..c1a3f1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ +# 1.8.2 +- Updates CLI to support Raisely's expanded MFA options + +# 1.8.1 +- Fixes bug with logging in using MFA + # 1.7.0 - Now requires node v14+ LTS (esm no longer required) - Added better command-line experience (more logs) diff --git a/package.json b/package.json index 1162821..2a75775 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@raisely/cli", - "version": "1.8.1", + "version": "1.8.2", "description": "Raisely CLI for local development", "main": "./src/cli.js", "type": "module",