public bool SubmitSecretQuestionAnswer([FromUrl] string token, SecretQuestionAnswer answer) { Context.WebContext.MarkConfidential(); var process = GetActiveProcess(token); if(process == null) return false; var storedAnswer = process.Login.SecretQuestionAnswers.FirstOrDefault(a => a.Question.Id == answer.QuestionId); Context.ThrowIfNull(storedAnswer, ClientFaultCodes.InvalidValue, "questionId", "Question is not registered user question."); var success = _processService.CheckSecretQuestionAnswer(process, storedAnswer.Question, answer.Answer); return success; }
public bool SubmitQuestionAnswer(string token, SecretQuestionAnswer answer) { Context.WebContext.MarkConfidential(); var session = Context.OpenSession(); var process = GetMutiFactorProcess(session, token); var storedAnswer = process.Login.SecretQuestionAnswers.FirstOrDefault(a => a.Question.Id == answer.QuestionId); Context.ThrowIfNull(storedAnswer, ClientFaultCodes.InvalidValue, "questionId", "Question is not registered user question."); var success = _processService.CheckSecretQuestionAnswer(process, storedAnswer.Question, answer.Answer); return(success); }
public bool SubmitSecretQuestionAnswer([FromQuery] string token, [FromBody] SecretQuestionAnswer answer) { OpContext.WebContext.MarkConfidential(); var session = OpContext.OpenSession(); var process = GetActiveProcess(session, token); if (process == null) { return(false); } var storedAnswer = process.Login.SecretQuestionAnswers.FirstOrDefault(a => a.Question.Id == answer.QuestionId); OpContext.ThrowIfNull(storedAnswer, ClientFaultCodes.InvalidValue, "questionId", "Question is not registered user question."); var success = ProcessService.CheckSecretQuestionAnswer(process, storedAnswer.Question, answer.Answer); return(success); }
public void TestLogin() { var password = Samples.BookStore.SampleData.SampleDataGenerator.DefaultPassword; var app = SetupHelper.BooksApp; var loginService = app.GetService<ILoginService>(); var loginMgr = app.GetService<ILoginManagementService>(); var loginProcessService = app.GetService<ILoginProcessService>(); var context = app.CreateSystemContext(); // Simple login/logout ------------------------------------------------------------------------ //Let's try to login Dora var doraEmail = "*****@*****.**"; var doraLogin = loginService.Login(context, "dora", "invalid password"); // it is Dora, but we configured for case-insensitive user names Assert.AreEqual(LoginAttemptStatus.Failed, doraLogin.Status, "Expected login fail."); doraLogin = loginService.Login(context, "dora", password); Assert.AreEqual(LoginAttemptStatus.Success, doraLogin.Status, "Expected login succeed."); var doraUser = doraLogin.User; loginService.Logout(context); // should write a log entry // Password reset, full process. -------------------------------------------------------- // See detailed discussion here: http://www.troyhunt.com/2012/05/everything-you-ever-wanted-to-know.html // Let's say Dora forgot her password. She comes to Login page and clicks 'Forgot password' link. // She is presented with a box 'Enter your email', and a captcha to verify she's not a robot. // Dora enters email, solves captcha and clicks NEXT. We verify captcha (outside this sample) //if captcha matches, we proceed to search for login record using email var session = app.OpenSystemSession(); var enteredEmail = doraEmail; var doraEmailFactor = loginProcessService.FindLoginExtraFactor(context, ExtraFactorTypes.Email, enteredEmail); // In this test, we know login exists; in real app, if we do not find login, stop here, // but do not disclose that email did not match; say 'Reset URL was sent to this email if it was found in our database' Assert.IsNotNull(doraEmailFactor, "Expected to find login by email."); // login is found; Start login process var process = loginProcessService.StartProcess(doraEmailFactor.Login, LoginProcessType.PasswordReset, enteredEmail); Assert.AreEqual(ExtraFactorTypes.Email | ExtraFactorTypes.SecretQuestions, process.PendingFactors, "Expected Email and Secret questions pending factors"); // send email to email address provided by user with a link containing the flowToken; wait for the user to hit the link. // Do not send anything if login factor was not found by email; otherwise your site becomes email DDOS bot // Important: in both cases (email found or not), present user (dora or not) with the same page // saying 'Reset instructions were sent to email you provided, if it was found in our database. ', without disclosing if email was found or not var processToken = process.Token; //embed it in URL and send it in email loginProcessService.SendPin(process, doraEmailFactor); //Dora receives email, copies pin var emailMsg = SetupHelper.GetLastMessageTo(doraEmail); var pin = (string) emailMsg.Parameters[LoginNotificationKeys.Pin]; //get pin //Find the login process session = app.OpenSystemSession(); process = loginProcessService.GetActiveProcess(context, LoginProcessType.PasswordReset, processToken); // if the process is null, present with page 'oopss..' invalid link or link expired Assert.IsNotNull(process, "Expected to find process."); loginProcessService.SubmitPin(process, pin); // Next - secret questions Assert.AreEqual(ExtraFactorTypes.SecretQuestions, process.PendingFactors, "Expected Secret questions pending factor."); var qaList = process.Login.SecretQuestionAnswers; Assert.AreEqual(3, qaList.Count, "Expected 3 questions/answers"); //present Dora with a page with her 3 questions, wait until she types the answers // Assume we got the answers; we also have flowToken preserved somewhere on the page qaList = process.Login.SecretQuestionAnswers; var answers = new SecretQuestionAnswer[] { new SecretQuestionAnswer() {QuestionId = qaList[0].Question.Id, Answer = "Diego"}, //best friend new SecretQuestionAnswer() {QuestionId = qaList[1].Question.Id, Answer = "Banana"},//favorite fruit new SecretQuestionAnswer() {QuestionId = qaList[2].Question.Id, Answer = "yellow"},//favorite color }; var answersCorrect = loginProcessService.CheckAllSecretQuestionAnswers(process, answers); Assert.IsTrue(answersCorrect, "Secret question answers failed."); process = loginProcessService.GetActiveProcess(context, LoginProcessType.PasswordReset, processToken); Assert.AreEqual(ExtraFactorTypes.None, process.PendingFactors, "Expected no pending factors."); // Dora enters new password and hits Submit // Let's test detection of reuse of old password var oldPass = password; bool wasUsed = loginMgr.CheckPasswordWasUsed(process.Login, oldPass, TimeSpan.FromDays(365), 5); Assert.IsTrue(wasUsed, "Failed to detect password reuse."); // Now set new password var newPass = password + "New"; wasUsed = loginMgr.CheckPasswordWasUsed(process.Login, newPass, TimeSpan.FromDays(365), 5); Assert.IsFalse(wasUsed, "False reuse signal detected."); loginMgr.ChangePassword(process.Login, oldPass, newPass); //we are done; let's try to login Dora with new password doraLogin = loginService.Login(context, "dora", newPass); //user names are case insensitive Assert.IsTrue(doraLogin.Status == LoginAttemptStatus.Success, "Failed to login after password change."); //Change back, to avoid breaking other tests loginMgr.ChangePassword(process.Login, newPass, oldPass); }
public bool SubmitUserQuestionAnswer(string token, SecretQuestionAnswer answer) { Context.WebContext.MarkConfidential(); var process = GetActiveProcess(token); if(process == null) return false; var storedAnswer = process.Login.SecretQuestionAnswers.FirstOrDefault(a => a.Question.Id == answer.QuestionId); Context.ThrowIfNull(storedAnswer, ClientFaultCodes.InvalidValue, "questionId", "Question is not registered user question."); var success = _processService.CheckSecretQuestionAnswer(process, storedAnswer.Question, answer.Answer); return success; }
public void TestLoginAdvancedFeatures() { // ================================ Completing login setup - verifying email, setup secret questions ===================== // User Ferb has email (non-verified) and no secret questions setup. Let's setup his account thru API calls. var client = SetupHelper.Client; var ferbUserName = "******"; var ferbEmail = "*****@*****.**"; var ferbLogin = LoginAs(ferbUserName); Assert.AreEqual(LoginAttemptStatus.Success, ferbLogin.Status, "Ferb login failed."); Assert.IsTrue(ferbLogin.Actions.IsSet(PostLoginActions.SetupExtraFactors), "Expected request to setup extra factors."); // Get login info, check which factors we need to setup var loginInfo = client.ExecuteGet<LoginInfo>("api/mylogin"); Assert.IsNotNull(loginInfo, "Failed to get LoginInfo"); Assert.AreEqual(ExtraFactorTypes.Email | ExtraFactorTypes.SecretQuestions, loginInfo.IncompleteFactors, "Wrong set of incomplete factors."); //We need to setup email; email might exist or not, and if exists, it might be not verified. var factors = client.ExecuteGet<List<LoginExtraFactor>>("api/mylogin/factors"); Assert.AreEqual(0, factors.Count, "expected 0 factors"); var newEmail = new LoginExtraFactor() { Type = ExtraFactorTypes.Email, Value = ferbEmail }; client.ExecutePost<LoginExtraFactor, LoginExtraFactor>(newEmail, "api/mylogin/factors"); //Get factors again - now we have email, unconfirmed factors = client.ExecuteGet<List<LoginExtraFactor>>("api/mylogin/factors"); var emailFactor = factors[0]; Assert.AreEqual(ExtraFactorTypes.Email, emailFactor.Type, "Expected email"); Assert.IsFalse(emailFactor.Confirmed, "Email should not be confirmed."); //Let's confirm it - send pin, read it from email and confirm it client.ExecutePost<object, HttpStatusCode>(null, "api/mylogin/factors/{0}/pin", emailFactor.Id); //let's do it twice - to make sure it works even if we have multiple pins sent client.ExecutePost<object, HttpStatusCode>(null, "api/mylogin/factors/{0}/pin", emailFactor.Id); var pinEmail = SetupHelper.GetLastMessageTo(ferbEmail); Assert.IsNotNull(pinEmail, "Pin email not received."); var pin = pinEmail.GetString("Pin"); //get pin from email Assert.IsFalse(string.IsNullOrEmpty(pin), "Expected non-null pin"); //submit pin var pinOk = client.ExecutePut<object, bool>(null, "api/mylogin/factors/{0}/pin/{1}", emailFactor.Id, pin); Assert.IsTrue(pinOk, "Pin submit failed."); //Now email should not be listed as incomplete factor loginInfo = client.ExecuteGet<LoginInfo>("api/mylogin"); Assert.AreEqual(ExtraFactorTypes.SecretQuestions, loginInfo.IncompleteFactors, "Expected only questions as incomplete factors."); //Let's setup secret questions/answers. Let's get all secret questions, choose three and submit answers var allQuestions = client.ExecuteGet<List<SecretQuestion>>("api/mylogin/allquestions"); Assert.IsTrue(allQuestions.Count > 20, "Failed to retrieve all questions."); // let's choose 3 var qFriend = allQuestions.First(q => q.Question.Contains("friend")); // childhood friend var qFood = allQuestions.First(q => q.Question.Contains("favorite food")); var qColor = allQuestions.First(q => q.Question.Contains("favorite color")); var answers = new SecretQuestionAnswer[] { new SecretQuestionAnswer() {QuestionId = qFriend.Id, Answer = "Phineas"}, new SecretQuestionAnswer() {QuestionId = qFood.Id, Answer = "Potato"}, new SecretQuestionAnswer() {QuestionId = qColor.Id, Answer = "Blue"} }; //submit answers client.ExecutePut<SecretQuestionAnswer[], HttpStatusCode>(answers, "api/mylogin/answers"); // Read back LoginInfo - now it should have no incomplete factors loginInfo = client.ExecuteGet<LoginInfo>("api/mylogin"); Assert.AreEqual(ExtraFactorTypes.None, loginInfo.IncompleteFactors, "Expected no incomplete factors."); //Now if Ferb logs in again, no post-login actions should be required Logout(); ferbLogin = LoginAs(ferbUserName); Assert.AreEqual(LoginAttemptStatus.Success, ferbLogin.Status, "Ferb login failed."); Assert.AreEqual(PostLoginActions.None, ferbLogin.Actions, "Expected no post-login actions."); //============================== Password change ============================================= // Ferb changes his password; let's first try invalid old password var oldPassword = Samples.BookStore.SampleData.SampleDataGenerator.DefaultPassword; var newPass = oldPassword + "New"; var pwdChange = new PasswordChangeInfo() { OldPassword = "******", NewPassword = newPass }; var cfExc = TestUtil.ExpectClientFault(() => client.ExecutePut<PasswordChangeInfo, HttpStatusCode>(pwdChange, "api/mylogin/password")); Assert.AreEqual(ClientFaultCodes.InvalidValue, cfExc.Faults[0].Code, "Expected 'InvalidValue' for OldPassword"); Assert.AreEqual("OldPassword", cfExc.Faults[0].Tag, "Expected OldPassword as invalid value"); // Let's try weak password and check it pwdChange = new PasswordChangeInfo() { OldPassword = oldPassword, NewPassword = "******" }; var strength = client.ExecutePut<PasswordChangeInfo, PasswordStrength>(pwdChange, "api/login/passwordcheck"); Assert.AreEqual(PasswordStrength.Weak, strength, "Expected Weak or unacceptable result."); // We set min strength to Medium in login settings // let's try weak password - should fail with 'WeakPassword' fault; cfExc = TestUtil.ExpectClientFault(() => client.ExecutePut<PasswordChangeInfo, HttpStatusCode>(pwdChange, "api/mylogin/password")); Assert.AreEqual(LoginFaultCodes.WeakPassword, cfExc.Faults[0].Code, "Expected WeakPassword fault."); // good password pwdChange.NewPassword = newPass; // check strength strength = client.ExecutePut<PasswordChangeInfo, PasswordStrength>(pwdChange, "api/login/passwordcheck"); Assert.AreEqual(PasswordStrength.Strong, strength, "Expected Strong result."); // actually change var status = client.ExecutePut<PasswordChangeInfo, HttpStatusCode>(pwdChange, "api/mylogin/password"); Assert.AreEqual(HttpStatusCode.NoContent, status, "Password change failed"); //verify it Logout(); ferbLogin = LoginAs(ferbUserName, pwdChange.NewPassword); Assert.AreEqual(LoginAttemptStatus.Success, ferbLogin.Status, "Failed to login with new password."); Logout(); //========================================== Password reset ============================================================= // Ferb has everything setup for proper password reset (email is comfirmed and secret questions are entered). // Lets do password reset. Ferb forgets his password and comes back to our site // 1. Start password reset process. Ferb clicks Forgot Password link and is redirected to initial reset page. // He enters email in a text box, solves captcha and clicks Submit. The client code executes a request to start reset process // "Magic" is a magic captcha value (it is set in login module settings) to bypass captcha check in unit tests. var request = new PasswordResetStartRequest() { Factor = ferbEmail, Captcha = "Magic" }; var processToken = client.ExecutePost<PasswordResetStartRequest, string>(request, "api/passwordreset"); Assert.IsFalse(string.IsNullOrWhiteSpace(processToken), "Expected process token."); // We do not disclose any details, even the fact that actual process started or not; // even if ferb's email is not found, the server returns a process token as if everything is ok. // This is done to avoid disclosing if the user is signed up at our site or not (if we are a p**n site we should not disclose membership) // Client can use process token to retrieve LoginProcess object - except right after process is started, it returns null - to avoid disclosing membership. // Only after at least one factor (email or phone) is confirmed (pin submitted back), the process information becomes visible. // So at this point trying to get process returns null // NOTE: in some cases hiding membership is not needed - when we have a business system with employees as users. // For this case, you can set a flag DoNotConcealMembership in ILogin record(s), and system would behave accordingly var process = client.ExecuteGet<LoginProcess>("api/passwordreset/{0}", processToken); Assert.IsNull(process, "Expected server hiding process object"); // 2. Send pin using email var sendPinRequest = new SendPinRequest() { Factor = ferbEmail }; var httpStatus = client.ExecutePost<SendPinRequest, HttpStatusCode>(sendPinRequest, "api/passwordreset/{0}/pin", processToken); Assert.AreEqual(HttpStatusCode.NoContent, httpStatus, "Failed to send pin."); // 3. Ferb receives email - we check our mock email service, retrieve the message and pin pinEmail = SetupHelper.GetLastMessageTo(ferbEmail); Assert.IsNotNull(pinEmail, "Email with pin not received."); pin = (string)pinEmail.GetString("Pin"); Assert.IsTrue(!string.IsNullOrWhiteSpace(pin), "Failed to receive/extract pin."); // 4. Ferb copies pin from email and enters it in a page. The UI submits the pin httpStatus = client.ExecutePut<object, HttpStatusCode>(null, "api/passwordreset/{0}/pin/{1}", processToken, pin); Assert.AreEqual(HttpStatusCode.NoContent, httpStatus, "Failed to submit pin."); // 5. UI retrieves the process to see if pin was correct and to see further steps. // If the pin was correct, the email is confirmed, and now we can retrieve the process object; otherwise the call would return null. process = client.ExecuteGet<LoginProcess>("api/passwordreset/{0}", processToken); Assert.IsNotNull(process, "Failed to retrieve process object."); Assert.AreEqual(LoginProcessType.PasswordReset, process.ProcessType, "Process type does not match."); Assert.AreEqual(ExtraFactorTypes.Email, process.CompletedFactors, "Expected email as completed factor."); Assert.AreEqual(ExtraFactorTypes.SecretQuestions, process.PendingFactors, "Expected SecretQuestions as pending factor."); // 6. Next step is in process.PendingFactors - it is secret questions; get Ferb's questions and submit answers. var questions = client.ExecuteGet<IList<SecretQuestion>>("api/passwordreset/{0}/userquestions", processToken); Assert.AreEqual(3, questions.Count, "Expected 3 questions"); //Let's first try incorrect answers var ferbAnswers = new List<SecretQuestionAnswer>(); ferbAnswers.Add(new SecretQuestionAnswer() { QuestionId = questions[0].Id, Answer = "Candice" }); //best childhood friend - incorrect ferbAnswers.Add(new SecretQuestionAnswer() { QuestionId = questions[1].Id, Answer = "Potato" }); //favorite food ferbAnswers.Add(new SecretQuestionAnswer() { QuestionId = questions[2].Id, Answer = "blue" }); //favorite color var answersOk = client.ExecutePut<List<SecretQuestionAnswer>, bool>(ferbAnswers, "api/passwordreset/{0}/questionanswers", processToken); Assert.IsFalse(answersOk, "Expected bad answers to fail."); //Now correct answers ferbAnswers[0].Answer = "Phineas"; //this is correct answersOk = client.ExecutePut<List<SecretQuestionAnswer>, bool>(ferbAnswers, "api/passwordreset/{0}/questionanswers", processToken); Assert.IsTrue(answersOk, "Expected answers to succeed."); // 7. Get the process object - there should be no pending factors process = client.ExecuteGet<LoginProcess>("api/passwordreset/{0}", processToken); Assert.AreEqual(ExtraFactorTypes.None, process.PendingFactors, "Expected no pending factors"); // 8. Once all steps are completed, and server cleared all pending factors, the server will allow us to change password // in the context of the process. So let's actually change the password var passwordResetReq = new PasswordChangeInfo() { NewPassword = oldPassword }; //same as the original one var success = client.ExecutePut<PasswordChangeInfo, bool>(passwordResetReq, "api/passwordreset/{0}", processToken); Assert.IsTrue(success, "Failed to change password"); // 9. Verify that email notification was sent about password change var notifEmail = SetupHelper.GetLastMessageTo(ferbEmail); Assert.IsNotNull(notifEmail, "Password change notification was not sent."); Assert.AreEqual(LoginNotificationTypes.PasswordReset, notifEmail.Type, "Expected password change message."); // 10. Try to login with changed password ferbLogin = LoginAs(ferbUserName, oldPassword); Assert.AreEqual(LoginAttemptStatus.Success, ferbLogin.Status, "Login failed after reset."); // ============================ Multi-factor login ============================================== // Ferb decides to enable multi-factor login - with email and google authenticator; we need to add GA first as a factor loginInfo = client.ExecuteGet<LoginInfo>("api/mylogin"); Assert.IsNotNull(loginInfo, "Failed to get LoginInfo"); //Add GoogleAuthenticator factor var googleAuth = new LoginExtraFactor() { Type = ExtraFactorTypes.GoogleAuthenticator, Value = null }; // value is ignored, but returned object contains secret var gAuth = client.ExecutePost<LoginExtraFactor, LoginExtraFactor>(googleAuth, "api/mylogin/factors"); var gSecret = gAuth.Value; // Ferb can use it to entry secret manually on his phone // Ferb can also use QR reader; var qrUrl = client.ExecuteGet<string>("api/mylogin/factors/{0}/qr", gAuth.Id); Assert.IsTrue(!string.IsNullOrWhiteSpace(qrUrl), "Expected QR url."); // Find the URL in debug output, paste it in browser address line, see the picture and use Google Authenticator app on your phone // to add an account by scanning the QR pic. It should add "BooksEntityApp:ferb" account, and start showing 6 digit code Debug.WriteLine("Ferb's QR URL: " + qrUrl); //Enable multi-factor login loginInfo.RequireMultiFactorLogin = true; loginInfo.MultiFactorLoginFactors = ExtraFactorTypes.Email | ExtraFactorTypes.GoogleAuthenticator; client.ExecutePut<LoginInfo, HttpStatusCode>(loginInfo, "api/mylogin"); Logout(); // now if Ferb tries to login, he gets multi-factor pending status; the server process (represented by process token) is started automatically ferbLogin = LoginAs(ferbUserName, assertSuccess: false); Assert.AreEqual(LoginAttemptStatus.PendingMultifactor, ferbLogin.Status, "Expected multi-factor status."); processToken = ferbLogin.MultiFactorProcessToken; //the process already started Assert.IsFalse(string.IsNullOrEmpty(processToken), "Expected process token"); // We do not need to conceal the existense of the process like we do in password reset, so request for process returns non-null object process = client.ExecuteGet<LoginProcess>("api/login/{0}", processToken); Assert.IsNotNull(process, "Expected process object."); Assert.AreEqual(ExtraFactorTypes.Email | ExtraFactorTypes.GoogleAuthenticator, process.PendingFactors, "Expected email and Google Auth pending factors."); // Email: Ask server to send pin by email httpStatus = client.ExecutePost<object, HttpStatusCode>(null, "api/login/{0}/pin?factortype={1}", processToken, ExtraFactorTypes.Email); Assert.AreEqual(HttpStatusCode.NoContent, httpStatus, "Expected NoContent status"); //Get message with pin from mock inbox and extract pin pinEmail = SetupHelper.GetLastMessageTo(ferbEmail); Assert.IsNotNull(pinEmail, "Email with pin not sent."); pin = pinEmail.GetString("Pin"); // Ferb copies pin from email and enters it in UI. UI submits the pin success = client.ExecutePut<object, bool>(null, "api/login/{0}/pin/{1}", processToken, pin); Assert.IsTrue(success, "Email pin submit failed"); // Google authenticator. //Tell server to 'send pin' - it won't send anything, but will set GA as current factor in the process httpStatus = client.ExecutePost<object, HttpStatusCode>(null, "api/login/{0}/pin?factortype={1}", processToken, ExtraFactorTypes.GoogleAuthenticator); // Pretend Ferb has GA installed on his phone, he opens the app and reads the current value. // In this test we use back door and compute it - we know the secret from the call when we added the factor (Google Authenticator as extra factor) var gaPassCode = Vita.Modules.Login.GoogleAuthenticator.GoogleAuthenticatorUtil.GeneratePasscode(gSecret); //Submit passcode as pin success = client.ExecutePut<object, bool>(null, "api/login/{0}/pin/{1}", processToken, gaPassCode); Assert.IsTrue(success, "Google authenticator pin failed."); //Get process again - now there should be no pending factors process = client.ExecuteGet<LoginProcess>("api/login/{0}", processToken); Assert.AreEqual(ExtraFactorTypes.None, process.PendingFactors, "Expected no pending factors"); //complete login using process token - returned LoginResponse object represents successful login ferbLogin = client.ExecutePost<object, LoginResponse>(null, "api/login/{0}", processToken); Assert.IsNotNull(ferbLogin, "Failed to complete login."); Assert.AreEqual(LoginAttemptStatus.Success, ferbLogin.Status, "Expected success status"); client.AddRequestHeader("Authorization", ferbLogin.AuthenticationToken); //we have to add it explicitly here // Ferb identifies the computer as personal (safe) device, and sets to skip multi-factor on this device var deviceInfo = new DeviceInfo() { Type = DeviceType.Computer, TrustLevel = DeviceTrustLevel.AllowSingleFactor }; //register the computer deviceInfo = client.ExecutePost<DeviceInfo, DeviceInfo>(deviceInfo, "api/mylogin/device"); // The returned token should be saved in local storage and used in future logins var deviceToken = deviceInfo.Token; //Now let's try to logout and login again, using deviceToken Logout(); ferbLogin = LoginAs(ferbUserName, deviceToken: deviceToken, assertSuccess: false); Assert.AreEqual(LoginAttemptStatus.Success, ferbLogin.Status, "Expected no multi-factor on trusted device"); Logout(); }