Skip to content

Commit 1abae9d

Browse files
authored
Merge pull request #47 from raisely/update-for-optional-mfa
Update for optional mfa
2 parents 1a046c1 + f9b06a1 commit 1abae9d

7 files changed

Lines changed: 106 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11

2+
# 1.8.2
3+
- Updates CLI to support Raisely's expanded MFA options
4+
5+
# 1.8.1
6+
- Fixes bug with logging in using MFA
7+
28
# 1.7.0
39
- Now requires node v14+ LTS (esm no longer required)
410
- Added better command-line experience (more logs)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@raisely/cli",
3-
"version": "1.8.1",
3+
"version": "1.8.2",
44
"description": "Raisely CLI for local development",
55
"main": "./src/cli.js",
66
"type": "module",

src/actions/auth.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ export async function login(body, opts = {}) {
115115
});
116116
}
117117

118+
export async function logout() {
119+
return await api({
120+
path: '/logout',
121+
method: 'POST',
122+
});
123+
}
124+
118125
export async function getToken(program, opts, warnEarly) {
119126
if (opts.$tokenFromEnv) return;
120127
let isNewToken = false;

src/cli.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const start = actionBuilder(() => import('./start.js'));
2121
const create = actionBuilder(() => import('./create.js'));
2222
const deploy = actionBuilder(() => import('./deploy.js'));
2323
const login = actionBuilder(() => import('./login.js'));
24+
const logout = actionBuilder(() => import('./logout.js'));
2425
const local = actionBuilder(() => import('./local.js'));
2526

2627
export async function cli() {
@@ -63,6 +64,11 @@ export async function cli() {
6364
.description('Authenticate with the Raisely api')
6465
.action(login);
6566

67+
program
68+
.command('logout')
69+
.description('Logout from the Raisely api')
70+
.action(logout);
71+
6672
program
6773
.command('local')
6874
.description('Start local development server for a single campaign.')

src/helpers.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,20 @@ export function error(e, loader) {
130130

131131
export function requiresMfa(e) {
132132
return e.subcode && e.subcode.startsWith('MFA required');
133+
}
134+
135+
export function getMfaStrategy(e) {
136+
// Extract if it's authy or authenticator
137+
const subcodeArray = e.subcode.split(':');
138+
const authType = subcodeArray[1];
139+
140+
return {
141+
mfaType: authType,
142+
// if authenticator, we need to know whether to offer authy as alternative
143+
hasAuthy: Boolean(
144+
authType === 'AUTHY' ||
145+
(subcodeArray.length === 3 &&
146+
subcodeArray[2] === 'hasAuthy')
147+
)
148+
}
133149
}

src/login.js

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import ora from 'ora';
44
import { login } from './actions/auth.js';
55

66
import { updateConfig } from './config.js';
7-
import { log, error, informUpdate, requiresMfa } from './helpers.js';
7+
import { log, error, informUpdate, requiresMfa, getMfaStrategy } from './helpers.js';
88

99
export async function doLogin(message) {
1010
if (message) log(message, 'white');
@@ -38,16 +38,38 @@ export async function doLogin(message) {
3838
return loginSucceed(loginLoader, loginBody);
3939
} catch (e) {
4040
if (requiresMfa(e)) {
41-
return await loginWith2FA(loginLoader, credentials);
41+
const mfaStrategy = getMfaStrategy(e)
42+
return await loginWith2FA(loginLoader, credentials, mfaStrategy);
4243
} else {
4344
error(e, loginLoader);
4445
return false;
4546
}
4647
}
4748
}
4849

49-
async function loginWith2FA(loginLoader, credentials) {
50-
loginLoader.info('Your account requires 2 factor authentication.');
50+
async function loginWith2FA(loginLoader, credentials, mfaStrategy) {
51+
loginLoader.info(`Your account requires 2 factor authentication`);
52+
let mfaType = mfaStrategy.mfaType;
53+
if (mfaType === 'AUTHENTICATOR_APP' && mfaStrategy.hasAuthy) {
54+
const choiceMfa = await selectMfaType();
55+
mfaType = choiceMfa.mfaType;
56+
if (mfaType === 'AUTHY') {
57+
// trigger login again with mfaType to send the prompt
58+
try {
59+
await login({
60+
...credentials,
61+
mfaType,
62+
requestAdminToken: true,
63+
});
64+
} catch (e) {
65+
// don't throw error if just an error about missing MFA
66+
if (!requiresMfa(e)) {
67+
error(e, loginLoader);
68+
return false;
69+
}
70+
}
71+
}
72+
}
5173
try {
5274
const response = await inquirer.prompt([
5375
{
@@ -65,6 +87,7 @@ async function loginWith2FA(loginLoader, credentials) {
6587

6688
const loginBody = await login({
6789
...credentials,
90+
mfaType,
6891
otp: response.otp,
6992
requestAdminToken: true,
7093
});
@@ -75,6 +98,31 @@ async function loginWith2FA(loginLoader, credentials) {
7598
}
7699
}
77100

101+
async function selectMfaType() {
102+
const selectedMfa = await inquirer.prompt([
103+
{
104+
type: 'list',
105+
message: 'Select your preferred MFA',
106+
name: 'mfaType',
107+
choices: [
108+
{
109+
name: 'Authenticator App',
110+
value: 'AUTHENTICATOR_APP'
111+
},
112+
{
113+
name: 'SMS/Legacy',
114+
value: 'AUTHY'
115+
}
116+
],
117+
validate: (value) =>
118+
value.length
119+
? true
120+
: 'Please choose your preferred MFA',
121+
},
122+
]);
123+
return selectedMfa;
124+
}
125+
78126
async function loginSucceed(loginLoader, loginBody) {
79127
const { token, data: user } = loginBody;
80128
loginLoader.succeed();

src/logout.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import ora from 'ora';
2+
import { logout } from './actions/auth.js';
3+
import { updateConfig } from './config.js';
4+
5+
export default async function logoutAction() {
6+
let logoutLoader = ora('Logging you out...').start();
7+
try {
8+
await logout();
9+
logoutLoader.info(`You have been logged out`);
10+
} catch (e) {
11+
// don't throw error, continue
12+
} finally {
13+
await updateConfig({
14+
token: null,
15+
});
16+
logoutLoader.stop()
17+
}
18+
}

0 commit comments

Comments
 (0)