Skip to content

Commit

Permalink
Merge pull request #441 from particle-iot/ch19349/support-mfa-in-part…
Browse files Browse the repository at this point in the history
…icle-login

Support MFA in login
  • Loading branch information
monkbroc authored Aug 3, 2018
2 parents 3e64025 + d22f293 commit e4e15af
Show file tree
Hide file tree
Showing 14 changed files with 337 additions and 337 deletions.
12 changes: 7 additions & 5 deletions src/cli/cloud.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,7 @@ export default ({ commandProcessor, root }) => {
examples: {
'$0 $command': 'prompt for credentials and log in',
'$0 $command --username user@example.com --password test': 'log in with credentials provided on the command line',
'$0 $command --token <my-api-token>': 'log in with an access token provided on the command line',
'$0 $command --token <my-api-token> --username user@example.com': 'log in with an access token provided on the command line and set your username'
'$0 $command --token <my-api-token>': 'log in with an access token provided on the command line'
},
options: {
u: {
Expand All @@ -123,15 +122,18 @@ export default ({ commandProcessor, root }) => {
nargs: 1
},
t: {
description: '',
description: 'an existing Particle access token to use',
alias: 'token',
nargs: 1
},
otp: {
description: 'the login code if two-step authentication is enabled',
nargs: 1
}
},
handler: (args) => {
const CloudCommands = require('../cmd/cloud');
const { username, password, token } = args;
return new CloudCommands().login(username, password, token);
return new CloudCommands().login(args);
}
});

Expand Down
55 changes: 49 additions & 6 deletions src/cmd/cloud.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ class CloudCommand {
});
}

login(username, password, token) {
login({ username, password, token, otp } = {}) {
const shouldRetry = !((username && password) || token && !this.tries);

return Promise.resolve()
Expand All @@ -356,11 +356,24 @@ class CloudCommand {
this.newSpin('Sending login details...').start();
this._usernameProvided = username;

if (token){
return api.getUser(token).then(() => ({ token, username, password }));
if (token) {
return api.getUser(token).then((response) => {
return {
token,
username: response.username
};
});
}
return api.login(settings.clientId, username, password)
.then(token => ({ token, username, password }));
.catch((error) => {
if (error.error === 'mfa_required') {
this.stopSpin();

this.tries = 0;
return this.enterOtp({ otp, mfaToken: error.mfa_token, shouldRetry });
}
throw error;
}).then(token => ({ token, username }));
})
.then(credentials => {
const { token, username } = credentials;
Expand All @@ -382,16 +395,46 @@ class CloudCommand {
.catch(error => {
this.stopSpin();
console.log(alert, `There was an error logging you in! ${shouldRetry ? "Let's try again." : ''}`);
console.error(alert, error);
console.error(alert, error.message || error.error_description);
this.tries = (this.tries || 0) + 1;

if (shouldRetry && this.tries < 3){
return this.login(this._usernameProvided);
return this.login({ username: this._usernameProvided });
}
throw new VError("It seems we're having trouble with logging in.");
});
}

enterOtp({ otp, mfaToken, shouldRetry }) {
return Promise.resolve().then(() => {
if (!this.tries) {
console.log('Use your authenticator app on your mobile device to get a login code.');
console.log('Lost access to your phone? Visit https://login.particle.io/account-info');
}

if (otp) {
return otp;
}
return prompts.getOtp();
}).then(_otp => {
otp = _otp;
this.newSpin('Sending login code...').start();

const api = new ApiClient();
return api.sendOtp(settings.clientId, mfaToken, otp);
}).catch(error => {
this.stopSpin();
console.log(alert, `This login code didn't work. ${shouldRetry ? "Let's try again." : ''}`);
console.error(alert, error.message || error.error_description);
this.tries = (this.tries || 0) + 1;

if (shouldRetry && this.tries < 3){
return this.enterOtp({ mfaToken, shouldRetry });
}
throw new VError('Recover your account at https://login.particle.io/account-info');
});
}

doLogout(keep, password) {
const api = new ApiClient();

Expand Down
27 changes: 18 additions & 9 deletions src/cmd/serial.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ try {
}
const wifiScan = require('node-wifiscanner2').scan;
const specs = require('../lib/deviceSpecs');
const ApiClient = require('../lib/ApiClient2');
const OldApiClient = require('../lib/ApiClient');
const ApiClient = require('../lib/ApiClient');
const settings = require('../../settings');
const DescribeParser = require('binary-version-reader').HalDescribeParser;
const YModem = require('../lib/ymodem');
Expand Down Expand Up @@ -619,15 +618,23 @@ class SerialCommand {

function getClaim() {
self.newSpin('Obtaining magical secure claim code from the cloud...').start();
api.getClaimCode(undefined, afterClaim);
api.getClaimCode().then((response) => {
afterClaim(null, response);
}, (error) => {
afterClaim(error);
});
}

function revived() {
self.stopSpin();
self.newSpin("Attempting to verify the Photon's connection to the cloud...").start();

setTimeout(() => {
api.listDevices(checkDevices);
api.listDevices({ silent: true }).then((body) => {
checkDevices(null, body);
}, (error) => {
checkDevices(error);
});
}, 6000);
}

Expand All @@ -646,7 +653,7 @@ class SerialCommand {
self.exit();
}

// self.__deviceID -> _deviceID
// self.deviceID -> _deviceID
const onlinePhoton = _.find(dat, (device) => {
return (device.id.toUpperCase() === _deviceID.toUpperCase()) && device.connected === true;
});
Expand Down Expand Up @@ -674,7 +681,11 @@ class SerialCommand {

function recheck(ans) {
if (ans.recheck === 'recheck') {
api.listDevices(checkDevices);
api.listDevices({ silent: true }).then((body) => {
checkDevices(null, body);
}, (error) => {
checkDevices(error);
});
} else {
self._promptForListeningMode();
self.setup(device);
Expand All @@ -683,8 +694,6 @@ class SerialCommand {
}

function namePhoton(deviceId) {
const __oldapi = new OldApiClient();

prompt([
{
type: 'input',
Expand All @@ -695,7 +704,7 @@ class SerialCommand {
// todo - retrieve existing name of the device?
const deviceName = ans.deviceName;
if (deviceName) {
__oldapi.renameDevice(deviceId, deviceName).then(() => {
api.renameDevice(deviceId, deviceName).then(() => {
console.log();
console.log(arrow, 'Your Photon has been given the name', chalk.bold.cyan(deviceName));
console.log(arrow, "Congratulations! You've just won the internet!");
Expand Down
67 changes: 25 additions & 42 deletions src/cmd/setup.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
const chalk = require('chalk');
const prompt = require('inquirer').prompt;
const ApiClient2 = require('../../dist/lib/ApiClient2');

const settings = require('../../settings.js');
const ApiClient = require('../../dist/lib/ApiClient.js');
const utilities = require('../../dist/lib/utilities.js');
const settings = require('../../settings');
const ApiClient = require('../lib/ApiClient');
const utilities = require('../lib/utilities');

const when = require('when');
const sequence = require('when/sequence');
Expand Down Expand Up @@ -35,9 +34,7 @@ class SetupCommand {

constructor() {
spinnerMixin(this);
this.__wasLoggedIn;
this.__api = new ApiClient2();
this.__oldapi = new ApiClient();
this.api = new ApiClient();
}

command(name, options = { params: {} }) {
Expand All @@ -58,7 +55,7 @@ class SetupCommand {
loginCheck();

function loginCheck() {
self.__wasLoggedIn = !!settings.username;
self.wasLoggedIn = !!settings.username;

if (settings.access_token) {
return promptSwitch();
Expand Down Expand Up @@ -90,8 +87,7 @@ class SetupCommand {
// user wants to logout
if (!ans.switch) {
return self.command('cloud').logout(true).then(() => {
self.__api.clearToken();
self.__oldapi.clearToken();
self.api.clearToken();
accountStatus(false);
});
} else {
Expand All @@ -106,7 +102,7 @@ class SetupCommand {

if (!alreadyLoggedIn) {
// New user or a fresh environment!
if (!self.__wasLoggedIn) {
if (!self.wasLoggedIn) {
self.prompt([
{
type: 'list',
Expand Down Expand Up @@ -147,7 +143,7 @@ class SetupCommand {
);
}
const self = this;
const signupUsername = this.__signupUsername || undefined;
const signupUsername = this.signupUsername || undefined;
console.log(arrow, "Let's create your new account!");

self.prompt([{
Expand Down Expand Up @@ -194,7 +190,7 @@ class SetupCommand {

// try to remember username to save them some frustration
if (ans.username) {
self.__signupUsername = ans.username;
self.signupUsername = ans.username;
}
console.log(
arrow,
Expand All @@ -203,30 +199,19 @@ class SetupCommand {
return self.signup(cb, ++tries);
}

self.__api.createUser(ans.username, ans.password, (signupErr) => {
if (signupErr) {
console.error(signupErr);
console.error(alert, "Oops, that didn't seem to work. Let's try that again");
return self.signup(cb, ++tries);
}

self.api.createUser(ans.username, ans.password).then(() => {
// Login the new user automatically
self.__api.login(settings.clientId, ans.username, ans.password, (loginErr, body) => {
// if just the login fails, reset to the login part of the setup flow
if (loginErr) {
console.error(loginErr);
console.error(alert, 'We had a problem logging you in :(');
return self.login(cb);
}

self.__oldapi.updateToken(body.access_token);

settings.override(null, 'username', ans.username);
console.log(arrow, strings.signupSuccess);
cb(null);
});
return self.api.login(settings.clientId, ans.username, ans.password);
}).then((token) => {
settings.override(null, 'access_token', token);
settings.override(null, 'username', ans.username);
console.log(arrow, strings.signupSuccess);
cb(null);
}).catch((signupErr) => {
console.error(signupErr);
console.error(alert, "Oops, that didn't seem to work. Let's try that again");
return self.signup(cb, ++tries);
});

}
}

Expand All @@ -236,14 +221,12 @@ class SetupCommand {
console.log(arrow, "Let's get you logged in!");

this.command('cloud').login().then((accessToken) => {
self.__api.updateToken(accessToken);
self.__oldapi.updateToken(accessToken);
self.api.updateToken(accessToken);
cb();
}).catch(() => {});
}

findDevice() {

const self = this;
const serial = this.command('serial');
const wireless = this.command('wireless');
Expand Down Expand Up @@ -459,12 +442,12 @@ class SetupCommand {
() => {
self.newSpin('Claiming the core to your account').start();
return utilities.retryDeferred(() => {
return self.__oldapi.claimDevice(deviceId);
return self.api.claimDevice(deviceId);
}, 3, promptForCyan);
},
() => {
self.stopSpin();
return self.__oldapi.signalDevice(deviceId, true);
return self.api.signalDevice(deviceId, true);
},
() => {
const rainbow = when.defer();
Expand All @@ -491,10 +474,10 @@ class SetupCommand {
deviceName = ans.coreName;
sequence([
() => {
return self.__oldapi.signalDevice(deviceId, false);
return self.api.signalDevice(deviceId, false);
},
() => {
return self.__oldapi.renameDevice(deviceId, deviceName);
return self.api.renameDevice(deviceId, deviceName);
}
]).then(naming.resolve, naming.reject);
});
Expand Down
1 change: 1 addition & 0 deletions src/cmd/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class AccessTokenCommands {
return this.getCredentials();
}).then(creds => {
return api.createAccessToken(clientName, creds.username, creds.password);
// TODO: add support for MFA here
}).then(result => {
const nowUnix = Date.now();
const expiresUnix = nowUnix + (result.expires_in * 1000);
Expand Down
Loading

0 comments on commit e4e15af

Please sign in to comment.