Skip to content

Commit

Permalink
Merge pull request #47 from raisely/update-for-optional-mfa
Browse files Browse the repository at this point in the history
Update for optional mfa
  • Loading branch information
rubicola authored Nov 6, 2023
2 parents 1a046c1 + f9b06a1 commit 1abae9d
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 5 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
7 changes: 7 additions & 0 deletions src/actions/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('./#.js'));
const logout = actionBuilder(() => import('./logout.js'));
const local = actionBuilder(() => import('./local.js'));

export async function cli() {
Expand Down Expand Up @@ -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.')
Expand Down
16 changes: 16 additions & 0 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,20 @@ 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 {
mfaType: authType,
// if authenticator, we need to know whether to offer authy as alternative
hasAuthy: Boolean(
authType === 'AUTHY' ||
(subcodeArray.length === 3 &&
subcodeArray[2] === 'hasAuthy')
)
}
}
56 changes: 52 additions & 4 deletions src/#.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -38,16 +38,38 @@ export async function doLogin(message) {
return loginSucceed(loginLoader, loginBody);
} catch (e) {
if (requiresMfa(e)) {
return await loginWith2FA(loginLoader, credentials);
const mfaStrategy = getMfaStrategy(e)
return await loginWith2FA(loginLoader, credentials, mfaStrategy);
} else {
error(e, loginLoader);
return false;
}
}
}

async function loginWith2FA(loginLoader, credentials) {
loginLoader.info('Your account requires 2 factor authentication.');
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;
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([
{
Expand All @@ -65,6 +87,7 @@ async function loginWith2FA(loginLoader, credentials) {

const loginBody = await login({
...credentials,
mfaType,
otp: response.otp,
requestAdminToken: true,
});
Expand All @@ -75,6 +98,31 @@ async function loginWith2FA(loginLoader, credentials) {
}
}

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();
Expand Down
18 changes: 18 additions & 0 deletions src/logout.js
Original file line number Diff line number Diff line change
@@ -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()
}
}

0 comments on commit 1abae9d

Please # to comment.