// // Internal (accessed by the tests) // internal static (byte[] EncryptedToken, string Passcode) GetEncryptedTokenAndPasscode(string username, string deviceId, IUi ui, RestClient rest) { byte[] GetToken(string code) => GetEncryptedToken(username, deviceId, code, DateTime.Now, rest); // First request without any passcode. This should succeed for known devices. var encryptedToken = GetToken(NoPasscode); if (encryptedToken != EmailPasscodeRequired) { return(encryptedToken, NoPasscode); } // It's a new device, the email PIN is required for (;;) { var passcode = ui.ProvideEmailPasscode(); if (passcode == Passcode.Cancel) { throw new CanceledMultiFactorException("Second factor step is canceled by the user"); } if (passcode == Passcode.Resend) { GetToken(NoPasscode); continue; } // Now try with the PIN from the email encryptedToken = GetToken(passcode.Code); if (encryptedToken == EmailPasscodeRequired) { throw new InternalErrorException("Unexpected response"); } return(encryptedToken, passcode.Code); } }
internal static string Login(string username, byte[] passwordHash, string deviceId, IUi ui, ISecureStorage storage, RestClient rest) { // Try simple password login, potentially with a stored second factor token if // "remember me" was used before. var rememberMeOptions = GetRememberMeOptions(storage); var response = RequestAuthToken(username, passwordHash, deviceId, rememberMeOptions, rest); // Simple password login (no 2FA) succeeded if (response.AuthToken != null) { return(response.AuthToken); } var secondFactor = response.SecondFactor; if (secondFactor.Methods == null || secondFactor.Methods.Count == 0) { throw new InternalErrorException("Expected a non empty list of available 2FA methods"); } // We had a "remember me" token saved, but the login failed anyway. This token is not valid anymore. if (rememberMeOptions != null) { EraseRememberMeToken(storage); } var method = ChooseSecondFactorMethod(secondFactor, ui); var extra = secondFactor.Methods[method]; Passcode passcode = null; switch (method) { case Response.SecondFactorMethod.GoogleAuth: passcode = ui.ProvideGoogleAuthPasscode(); break; case Response.SecondFactorMethod.Email: // When only the email 2FA present, the email is sent by the server right away. // Trigger only when other methods are present. if (secondFactor.Methods.Count != 1) { TriggerEmailMfaPasscode(username, passwordHash, rest); } passcode = ui.ProvideEmailPasscode((string)extra["Email"] ?? ""); break; case Response.SecondFactorMethod.Duo: case Response.SecondFactorMethod.DuoOrg: { var duo = Duo.Authenticate((string)extra["Host"] ?? "", (string)extra["Signature"] ?? "", ui, rest.Transport); if (duo != null) { passcode = new Passcode(duo.Passcode, duo.RememberMe); } break; } case Response.SecondFactorMethod.YubiKey: passcode = ui.ProvideYubiKeyPasscode(); break; case Response.SecondFactorMethod.U2f: passcode = AskU2fPasscode(JObject.Parse((string)extra["Challenge"]), ui); break; default: throw new UnsupportedFeatureException($"2FA method {method} is not supported"); } // We're done interacting with the UI ui.Close(); if (passcode == null) { throw MakeCancelledMfaError(); } var secondFactorResponse = RequestAuthToken(username, passwordHash, deviceId, new SecondFactorOptions(method, passcode.Code, passcode.RememberMe), rest); // Password + 2FA is successful if (secondFactorResponse.AuthToken != null) { SaveRememberMeToken(secondFactorResponse, storage); return(secondFactorResponse.AuthToken); } throw new BadMultiFactorException("Second factor code is not correct"); }