From 2fee9b8a7397656471efed1f4a6f8932397d75f3 Mon Sep 17 00:00:00 2001 From: David Brownman Date: Wed, 24 Apr 2019 16:34:08 -0400 Subject: [PATCH 1/2] add support for saml logins --- src/commands/login.js | 37 ++++++++++++++++++++++++------------- src/tests/utils/api.js | 17 +++++++++++++++++ src/utils/api.js | 14 ++++++++++++++ 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/commands/login.js b/src/commands/login.js index 75691f8..7ed7586 100644 --- a/src/commands/login.js +++ b/src/commands/login.js @@ -7,6 +7,7 @@ const QUESTION_USERNAME = 'What is your Zapier login email address? (Ctrl-C to cancel)'; const QUESTION_PASSWORD = 'What is your Zapier login password?'; const QUESTION_TOTP = 'What is your current 6-digit 2FA code?'; +const DEPLOY_KEY_DASH_URL = `${constants.BASE_ENDPOINT}/developer/dashboard`; // TODO: fix const login = async (context, firstTime = true) => { const checks = [ @@ -44,22 +45,32 @@ const login = async (context, firstTime = true) => { ) ); } - const username = await utils.getInput(QUESTION_USERNAME); - const password = await utils.getInput(QUESTION_PASSWORD, { secret: true }); + let deployKey; + const email = await utils.getInput(QUESTION_USERNAME); + const isSaml = await utils.isSamlEmail(email); - let goodResponse; - try { - goodResponse = await utils.createCredentials(username, password); - } catch ({ errText, json: { errors } }) { - if (errors[0].startsWith('missing totp_code')) { - const code = await utils.getInput(QUESTION_TOTP); - goodResponse = await utils.createCredentials(username, password, code); - } else { - throw new Error(errText); + if (isSaml) { + context.line( + `To generate a deploy key, go to ${DEPLOY_KEY_DASH_URL}, follow the instructions, and then paste the result below.` + ); + deployKey = await utils.getInput('Paste your deploy key:'); + } else { + const password = await utils.getInput(QUESTION_PASSWORD, { secret: true }); + + let goodResponse; + try { + goodResponse = await utils.createCredentials(email, password); + } catch ({ errText, json: { errors } }) { + if (errors[0].startsWith('missing totp_code')) { + const code = await utils.getInput(QUESTION_TOTP); + goodResponse = await utils.createCredentials(email, password, code); + } else { + throw new Error(errText); + } } - } - const deployKey = goodResponse.key; + deployKey = goodResponse.key; + } await utils.writeFile( constants.AUTH_LOCATION, diff --git a/src/tests/utils/api.js b/src/tests/utils/api.js index f804bf8..a3e4d9d 100644 --- a/src/tests/utils/api.js +++ b/src/tests/utils/api.js @@ -53,4 +53,21 @@ describe('api', () => { afterEach(mock.restore); }); + + describe('SAML checking', () => { + it('should throw for bad emails', async () => { + await api.isSamlEmail('asdf').should.be.rejected(); + }); + + it('should be false for non-saml emails', async () => { + const res = await api.isSamlEmail('bruce@wayneenterprises.com'); + res.should.be.false(); + }); + + // TODO: need an actual saml domain for this to work + it.skip('should work for saml emails', async () => { + const res = await api.isSamlEmail('bruce@wayneenterprises.com'); + res.should.be.true(); + }); + }); }); diff --git a/src/utils/api.js b/src/utils/api.js index 22dcdab..914b501 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -135,6 +135,19 @@ const createCredentials = (username, password, totpCode) => { ); }; +const isSamlEmail = async email => { + const rawResponse = await fetch( + `https://zapier.com/api/v4/idp-discovery/?email=${encodeURIComponent( + email + )}` + ); + const { results = [], errors = [] } = await rawResponse.json(); + if (errors.length) { + throw new Error(errors[0]); + } + return results.length > 0; +}; + // Reads the JSON file in the app directory. const getLinkedAppConfig = appDir => { appDir = appDir || '.'; @@ -314,6 +327,7 @@ module.exports = { getLinkedApp, getLinkedAppConfig, getVersionInfo, + isSamlEmail, listApps, listEndpoint, listEnv, From 617cfe976f438d3602af8661438acb3e0ae2c299 Mon Sep 17 00:00:00 2001 From: David Brownman Date: Thu, 25 Apr 2019 15:59:55 -0400 Subject: [PATCH 2/2] properly handle google SSO --- src/commands/login.js | 68 +++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/src/commands/login.js b/src/commands/login.js index 7ed7586..33b9bb2 100644 --- a/src/commands/login.js +++ b/src/commands/login.js @@ -6,8 +6,15 @@ const utils = require('../utils'); const QUESTION_USERNAME = 'What is your Zapier login email address? (Ctrl-C to cancel)'; const QUESTION_PASSWORD = 'What is your Zapier login password?'; +const QUESTION_SSO = 'Paste your deploy key here:'; const QUESTION_TOTP = 'What is your current 6-digit 2FA code?'; const DEPLOY_KEY_DASH_URL = `${constants.BASE_ENDPOINT}/developer/dashboard`; // TODO: fix +const SSO_INSTRUCTIONS = `To generate a deploy key, go to ${DEPLOY_KEY_DASH_URL}, create/copy a key, and then paste the result below.`; + +const getSsoKey = async context => { + context.line(SSO_INSTRUCTIONS); + return utils.getInput(QUESTION_SSO); +}; const login = async (context, firstTime = true) => { const checks = [ @@ -46,30 +53,37 @@ const login = async (context, firstTime = true) => { ); } let deployKey; - const email = await utils.getInput(QUESTION_USERNAME); - const isSaml = await utils.isSamlEmail(email); - if (isSaml) { - context.line( - `To generate a deploy key, go to ${DEPLOY_KEY_DASH_URL}, follow the instructions, and then paste the result below.` - ); - deployKey = await utils.getInput('Paste your deploy key:'); + if (context.argOpts.sso) { + deployKey = await getSsoKey(context); } else { - const password = await utils.getInput(QUESTION_PASSWORD, { secret: true }); - - let goodResponse; - try { - goodResponse = await utils.createCredentials(email, password); - } catch ({ errText, json: { errors } }) { - if (errors[0].startsWith('missing totp_code')) { - const code = await utils.getInput(QUESTION_TOTP); - goodResponse = await utils.createCredentials(email, password, code); - } else { - throw new Error(errText); + const email = await utils.getInput(QUESTION_USERNAME); + const isSaml = await utils.isSamlEmail(email); + + if (isSaml) { + deployKey = await getSsoKey(context); + } else { + context.line( + "\nNow you'll enter your Zapier password. If you log into Zapier via the Google button, you may not have a Zapier password. If that's the case, re-run this command with the `--sso` flag and follow the instructions.\n" + ); + const password = await utils.getInput(QUESTION_PASSWORD, { + secret: true + }); + + let goodResponse; + try { + goodResponse = await utils.createCredentials(email, password); + } catch ({ errText, json: { errors } }) { + if (errors[0].startsWith('missing totp_code')) { + const code = await utils.getInput(QUESTION_TOTP); + goodResponse = await utils.createCredentials(email, password, code); + } else { + throw new Error(errText); + } } - } - deployKey = goodResponse.key; + deployKey = goodResponse.key; + } } await utils.writeFile( @@ -90,7 +104,13 @@ const login = async (context, firstTime = true) => { } }; login.argsSpec = []; -login.argOptsSpec = {}; +login.argOptsSpec = { + sso: { + flag: true, + help: + "Use this flag if you log into Zapier using the Google SSO button and don't have a password to type" + } +}; login.help = `Configure your \`${ constants.AUTH_LOCATION_RAW }\` with a deploy key.`; @@ -102,8 +122,12 @@ This is an interactive prompt which will create, retrieve and store a deploy key constants.AUTH_LOCATION_RAW }\` (home directory identifies the deploy key & user). +**Arguments** + +${utils.argOptsFragment(login.argOptsSpec)} + ${'```'}bash -$ zapier login +# $ zapier login # ${QUESTION_USERNAME} # ${QUESTION_PASSWORD} #