diff --git a/octorun/src/authentication.js b/octorun/src/authentication.js index e2d082100..d724fca8f 100644 --- a/octorun/src/authentication.js +++ b/octorun/src/authentication.js @@ -2,13 +2,13 @@ var endOfLine = require('os').EOL; var config = require("./configuration"); var octokitWrapper = require("./octokit"); -var scopes = ["user", "repo", "gist", "write:public_key"]; - var lockedRegex = new RegExp("number of login attempts exceeded", "gi"); var twoFactorRegex = new RegExp("must specify two-factor authentication otp code", "gi"); var badCredentialsRegex = new RegExp("bad credentials", "gi"); -var handleBasicAuthentication = function (username, password, onSuccess, onRequiresTwoFa, onFailure) { +var scopes = ["user", "repo", "gist", "write:public_key"]; + +var handleAuthentication = function (username, password, onSuccess, onFailure, twoFactor) { if (!config.clientId || !config.clientSecret) { throw "clientId and/or clientSecret missing"; } @@ -25,64 +25,34 @@ var handleBasicAuthentication = function (username, password, onSuccess, onRequi password: password }); - octokit.authorization.create({ - scopes: scopes, - note: config.appName, - client_id: config.clientId, - client_secret: config.clientSecret - }, function (err, res) { - if (err) { - if (twoFactorRegex.test(err.message)) { - onRequiresTwoFa(); - } - else if (lockedRegex.test(err.message)) { - onFailure("Account locked.") - } - else if (badCredentialsRegex.test(err.message)) { - onFailure("Bad credentials.") - } - else { - onFailure(err) - } - } - else { - onSuccess(res.data.token); - } - }); -} - -var handleTwoFactorAuthentication = function (username, password, twoFactor, onSuccess, onFailure) { - if (!config.clientId || !config.clientSecret) { - throw "clientId and/or clientSecret missing"; - } - - if (!config.appName) { - throw "appName missing"; + var headers; + if (twoFactor) { + headers = { + "X-GitHub-OTP": twoFactor, + "user-agent": config.appName + }; } - - var octokit = octokitWrapper.createOctokit(); - - octokit.authenticate({ - type: "basic", - username: username, - password: password - }); octokit.authorization.create({ scopes: scopes, + note: config.appName, client_id: config.clientId, client_secret: config.clientSecret, - headers: { - "X-GitHub-OTP": twoFactor, - "user-agent": config.appName - } + headers: headers }, function (err, res) { if (err) { - if (lockedRegex.test(err.message)) { - onFailure("Account locked.") + if (twoFactor && err.code && err.code === 422) { + //Two Factor Enterprise workaround + onSuccess(password); + } + else if (twoFactorRegex.test(err.message)) { + onSuccess(password, "2fa"); + } + else if (lockedRegex.test(err.message)) { + onFailure("locked") } else if (badCredentialsRegex.test(err.message)) { - onFailure("Bad credentials.") + onFailure("badcredentials") } else { onFailure(err) @@ -95,6 +65,5 @@ var handleTwoFactorAuthentication = function (username, password, twoFactor, onS } module.exports = { - handleBasicAuthentication: handleBasicAuthentication, - handleTwoFactorAuthentication: handleTwoFactorAuthentication, + handleAuthentication: handleAuthentication, }; \ No newline at end of file diff --git a/octorun/src/bin/app-login.js b/octorun/src/bin/app-login.js index eec473f4d..98137e7a9 100644 --- a/octorun/src/bin/app-login.js +++ b/octorun/src/bin/app-login.js @@ -9,19 +9,24 @@ commander .option('-t, --twoFactor') .parse(process.argv); -var encoding = 'utf-8'; - -if (commander.twoFactor) { - var handleTwoFactorAuthentication = function (username, password, token) { - authentication.handleTwoFactorAuthentication(username, password, token, function (token) { - output.success(token); +var handleAuthentication = function (username, password, twoFactor) { + authentication.handleAuthentication(username, password, function (token, status) { + if (status) { + output.custom(status, token); process.exit(); - }, function (error) { - output.error(error); + } + else { + output.success(token); process.exit(); - }); - } + } + }, function (error) { + output.error(error); + process.exit(); + }, twoFactor); +} +var encoding = 'utf-8'; +if (commander.twoFactor) { if (process.stdin.isTTY) { var readlineSync = require("readline-sync"); var username = readlineSync.question('User: '); @@ -32,7 +37,7 @@ if (commander.twoFactor) { var twoFactor = readlineSync.question('Two Factor: '); try { - handleTwoFactorAuthentication(username, password, twoFactor); + handleAuthentication(username, password, twoFactor); } catch (error) { output.error(error); @@ -56,7 +61,7 @@ if (commander.twoFactor) { .filter(function (item) { return item; }); try { - handleTwoFactorAuthentication(items[0], items[1], items[2]); + handleAuthentication(items[0], items[1], items[2]); } catch (error) { output.error(error); @@ -66,20 +71,6 @@ if (commander.twoFactor) { } } else { - var handleBasicAuthentication = function (username, password) { - authentication.handleBasicAuthentication(username, password, - function (token) { - output.success(token); - process.exit(); - }, function () { - output.custom("2fa", password); - process.exit(); - }, function (error) { - output.error(error); - process.exit(); - }); - } - if (process.stdin.isTTY) { var readlineSync = require("readline-sync"); @@ -89,7 +80,7 @@ else { }); try { - handleBasicAuthentication(username, password); + handleAuthentication(username, password); } catch (error) { output.error(error); @@ -113,7 +104,7 @@ else { .filter(function (item) { return item; }); try { - handleBasicAuthentication(items[0], items[1]); + handleAuthentication(items[0], items[1]); } catch (error) { output.error(error); diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index c7b39dd5a..269458668 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -74,22 +74,23 @@ public async Task Login( keychain.Connect(host); keychain.SetCredentials(new Credential(host, username, password)); - string token; try { - token = await TryLogin(host, username, password); - if (string.IsNullOrEmpty(token)) + var loginResultData = await TryLogin(host, username, password); + if (loginResultData.Code == LoginResultCodes.Success || loginResultData.Code == LoginResultCodes.CodeRequired) { - throw new InvalidOperationException("Returned token is null or empty"); + if (string.IsNullOrEmpty(loginResultData.Token)) + { + throw new InvalidOperationException("Returned token is null or empty"); + } + + keychain.SetToken(host, loginResultData.Token); + await keychain.Save(host); + + return loginResultData; } - } - catch (TwoFactorRequiredException e) - { - LoginResultCodes result; - result = LoginResultCodes.CodeRequired; - logger.Trace("2FA TwoFactorAuthorizationException: {0} {1}", LoginResultCodes.CodeRequired, e.Message); - return new LoginResultData(result, e.Message, host, password); + return loginResultData; } catch (Exception e) { @@ -98,16 +99,10 @@ public async Task Login( await keychain.Clear(host, false); return new LoginResultData(LoginResultCodes.Failed, Localization.LoginFailed, host); } - - keychain.SetToken(host, token); - await keychain.Save(host); - - return new LoginResultData(LoginResultCodes.Success, "Success", host); } public async Task ContinueLogin(LoginResultData loginResultData, string twofacode) { - var token = loginResultData.Token; var host = loginResultData.Host; var keychainAdapter = keychain.Connect(host); var username = keychainAdapter.Credential.Username; @@ -115,24 +110,29 @@ public async Task ContinueLogin(LoginResultData loginResultData try { logger.Trace("2FA Continue"); - token = await TryContinueLogin(host, username, password, twofacode); + loginResultData = await TryLogin(host, username, password, twofacode); - if (string.IsNullOrEmpty(token)) + if (loginResultData.Code == LoginResultCodes.Success) { - throw new InvalidOperationException("Returned token is null or empty"); - } + if (string.IsNullOrEmpty(loginResultData.Token)) + { + throw new InvalidOperationException("Returned token is null or empty"); + } + + keychain.SetToken(host, loginResultData.Token); + await keychain.Save(host); - keychain.SetToken(host, token); - await keychain.Save(host); + return loginResultData; + } - return new LoginResultData(LoginResultCodes.Success, "", host); + return loginResultData; } catch (Exception e) { - logger.Trace(e, "Exception: {0}", e.Message); + logger.Warning(e, "Login Exception"); await keychain.Clear(host, false); - return new LoginResultData(LoginResultCodes.Failed, e.Message, host); + return new LoginResultData(LoginResultCodes.Failed, Localization.LoginFailed, host); } } @@ -144,10 +144,11 @@ public async Task Logout(UriString hostAddress) await new ActionTask(keychain.Clear(hostAddress, true)).StartAwait(); } - private async Task TryLogin( + private async Task TryLogin( UriString host, string username, - string password + string password, + string code = null ) { if (!nodeJsExecutablePath.HasValue) @@ -160,13 +161,20 @@ string password throw new InvalidOperationException("octorunScript must be set"); } + var hasTwoFactorCode = code != null; + + var arguments = hasTwoFactorCode ? "login --twoFactor" : "login"; var loginTask = new OctorunTask(taskManager.Token, nodeJsExecutablePath.Value, octorunScript.Value, - "login", ApplicationInfo.ClientId, ApplicationInfo.ClientSecret); + arguments, ApplicationInfo.ClientId, ApplicationInfo.ClientSecret); loginTask.Configure(processManager, workingDirectory: octorunScript.Value.Parent.Parent, withInput: true); loginTask.OnStartProcess += proc => { proc.StandardInput.WriteLine(username); proc.StandardInput.WriteLine(password); + if (hasTwoFactorCode) + { + proc.StandardInput.WriteLine(code); + } proc.StandardInput.Close(); }; @@ -174,65 +182,33 @@ string password if (ret.IsSuccess) { - return ret.Output[0]; + return new LoginResultData(LoginResultCodes.Success, null, host, ret.Output[0]); } - if (ret.IsCustom && ret.Status == "2fa") + if (ret.IsTwoFactorRequired) { - keychain.SetToken(host, ret.Output[0]); - await keychain.Save(host); - throw new TwoFactorRequiredException(); - } + var resultCodes = hasTwoFactorCode ? LoginResultCodes.CodeFailed : LoginResultCodes.CodeRequired; + var message = hasTwoFactorCode ? "Incorrect code. Two Factor Required." : "Two Factor Required."; - if (ret.Output.Any()) - { - throw new Exception(string.Join(Environment.NewLine, ret.Output)); + return new LoginResultData(resultCodes, message, host, ret.Output[0]); } - throw new Exception("Authentication failed"); - } - - private async Task TryContinueLogin( - UriString host, - string username, - string password, - string code - ) - { - if (!nodeJsExecutablePath.HasValue) + if (ret.IsBadCredentials) { - throw new InvalidOperationException("nodeJsExecutablePath must be set"); + return new LoginResultData(LoginResultCodes.Failed, "Bad credentials.", host, ret.Output[0]); } - if (!octorunScript.HasValue) + if (ret.IsLocked) { - throw new InvalidOperationException("octorunScript must be set"); - } - - var loginTask = new OctorunTask(taskManager.Token, nodeJsExecutablePath.Value, octorunScript.Value, - "login --twoFactor", ApplicationInfo.ClientId, ApplicationInfo.ClientSecret); - loginTask.Configure(processManager, workingDirectory: octorunScript.Value.Parent.Parent, withInput: true); - loginTask.OnStartProcess += proc => - { - proc.StandardInput.WriteLine(username); - proc.StandardInput.WriteLine(password); - proc.StandardInput.WriteLine(code); - proc.StandardInput.Close(); - }; - - var ret = await loginTask.StartAwait(); - - if (ret.IsSuccess) - { - return ret.Output[0]; + return new LoginResultData(LoginResultCodes.LockedOut, "Account locked.", host, ret.Output[0]); } if (ret.Output.Any()) { - throw new Exception(string.Join(Environment.NewLine, ret.Output)); + return new LoginResultData(LoginResultCodes.Failed, "Failed.", host, ret.Output[0]); } - throw new Exception("Authentication failed"); + return new LoginResultData(LoginResultCodes.Failed, "Failed.", host); } } @@ -257,9 +233,4 @@ internal LoginResultData(LoginResultCodes code, string message, UriString host) { } } - - class TwoFactorRequiredException : Exception - { - - } } diff --git a/src/GitHub.Api/Tasks/OctorunTask.cs b/src/GitHub.Api/Tasks/OctorunTask.cs index 982dedfae..369f90f29 100644 --- a/src/GitHub.Api/Tasks/OctorunTask.cs +++ b/src/GitHub.Api/Tasks/OctorunTask.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Reflection; using System.Threading; using GitHub.Logging; @@ -130,6 +131,8 @@ public OctorunResult(string status, string[] output) public bool IsSuccess => Status.Equals("success", StringComparison.InvariantCultureIgnoreCase); public bool IsError => Status.Equals("error", StringComparison.InvariantCultureIgnoreCase); - public bool IsCustom => !IsSuccess && !IsError; + public bool IsTwoFactorRequired => Status.Equals("2fa", StringComparison.InvariantCultureIgnoreCase); + public bool IsLocked => Output.First().Equals("locked", StringComparison.InvariantCultureIgnoreCase); + public bool IsBadCredentials => Output.First().Equals("badcredentials", StringComparison.InvariantCultureIgnoreCase); } } \ No newline at end of file