internal static void UpdateUi(DuoStatus status, string text, IDuoUi ui) { if (text.IsNullOrEmpty()) { return; } ui.UpdateDuoStatus(status, text); }
// Returns the second factor token from Duo or null when canceled by the user. public static Result Authenticate(string host, string signature, IDuoUi ui, IRestTransport transport) { var rest = new RestClient(transport, $"https://{host}"); var(tx, app) = ParseSignature(signature); var html = DownloadFrame(tx, rest); var(sid, devices) = ParseFrame(html); while (true) { // Ask the user to choose what to do var choice = ui.ChooseDuoFactor(devices); if (choice == null) { return(null); // Canceled by user } // SMS is a special case: it doesn't submit any codes, it rather tells the server to send // a new batch of passcodes to the phone via SMS. if (choice.Factor == DuoFactor.SendPasscodesBySms) { SubmitFactor(sid, choice, "", rest); choice = new DuoChoice(choice.Device, DuoFactor.Passcode, choice.RememberMe); } // Ask for the passcode var passcode = ""; if (choice.Factor == DuoFactor.Passcode) { passcode = ui.ProvideDuoPasscode(choice.Device); if (passcode.IsNullOrEmpty()) { return(null); // Canceled by user } } var token = SubmitFactorAndWaitForToken(sid, choice, passcode, ui, rest); // Flow error like an incorrect passcode. The UI has been updated with the error. Keep going. if (token.IsNullOrEmpty()) { continue; } // All good return(new Result($"{token}:{app}", choice.RememberMe)); } }
// Returns null when a recoverable flow error (like incorrect code or time out) happened // TODO: Don't return null, use something more obvious internal static string SubmitFactorAndWaitForToken(string sid, DuoChoice choice, string passcode, IDuoUi ui, RestClient rest) { var txid = SubmitFactor(sid, choice, passcode, rest); var url = PollForResultUrl(sid, txid, ui, rest); if (url.IsNullOrEmpty()) { return(null); } return(FetchToken(sid, url, ui, rest)); }
internal static string FetchToken(string sid, string url, IDuoUi ui, RestClient rest) { var response = PostForm <R.FetchToken>(url, new Dictionary <string, object> { ["sid"] = sid }, rest); UpdateUi(response, ui); var token = response.Cookie; if (token.IsNullOrEmpty()) { throw MakeInvalidResponseError("Duo: authentication token expected in response but wasn't found"); } return(token); }
// Returns null when a recoverable flow error (like incorrect code or time out) happened // TODO: Don't return null, use something more obvious internal static string PollForResultUrl(string sid, string txid, IDuoUi ui, RestClient rest) { const int maxPollAttempts = 100; // Normally it wouldn't poll nearly as many times. Just a few at most. It either bails on error or // returns the result. This number here just to prevent an infinite loop, which is never a good idea. for (var i = 0; i < maxPollAttempts; i += 1) { var response = PostForm <R.Poll>("frame/status", new Dictionary <string, object> { ["sid"] = sid, ["txid"] = txid }, rest); var(status, text) = GetResponseStatus(response); UpdateUi(status, text, ui); switch (status) { case DuoStatus.Success: var url = response.Url; if (url.IsNullOrEmpty()) { throw MakeInvalidResponseError("Duo: result URL (result_url) was expected but wasn't found"); } // Done return(url); case DuoStatus.Error: return(null); // TODO: Use something better than null } } throw MakeInvalidResponseError("Duo: expected to receive a valid result or error, got none of it"); }
internal static void UpdateUi(R.Status response, IDuoUi ui) { var(status, text) = GetResponseStatus(response); UpdateUi(status, text, ui); }