public async Task SendPin([FromBody] SendPinRequest request) { OpContext.WebContext.MarkConfidential(); OpContext.ThrowIfNull(request, ClientFaultCodes.ContentMissing, "SendPinRequest", "Pin request object must be provided."); OpContext.ValidateNotEmpty(request.ProcessToken, "ProcessToken", "Process token should be provided."); OpContext.ValidateNotEmpty(request.Factor, "Factor", "Factor (email or phone) should be provided."); OpContext.ThrowValidation(); var session = OpContext.OpenSession(); var process = GetActiveProcess(session, request.ProcessToken, confirmedOnly: false); if (process == null) { return; //no indication process exist or not } OpContext.ThrowIf(process.CurrentFactor != null, ClientFaultCodes.InvalidAction, "token", "The previous process step is not completed."); var iFactor = ProcessService.FindLoginExtraFactor(process.Login, request.Factor); //now having completed at least one extra factor, we can openly indicate that we could not find next factor OpContext.ThrowIfNull(iFactor, ClientFaultCodes.InvalidValue, "factor", "Login factor (email or phone) is not found for a user."); //Check that factor type is one in the pending steps var factorOk = process.PendingFactors.IsSet(iFactor.FactorType); OpContext.ThrowIf(!factorOk, ClientFaultCodes.InvalidValue, "factor", "Login factor type attempted (email or phone) is not pending in the process."); await ProcessService.SendPinAsync(process, iFactor, request.Factor); //we use factor from request, to avoid unencrypting twice }
public void SendPinForMultiFactor(SendPinRequest pinRequest) { var process = GetMutiFactorProcess(pinRequest.ProcessToken); Context.ThrowIf(process.CurrentFactor != null, ClientFaultCodes.InvalidAction, "token", "Factor verification pending, the previous process step is not completed."); var pendingFactorTypes = process.PendingFactors; Context.ThrowIf(!pendingFactorTypes.IsSet(pinRequest.FactorType), ClientFaultCodes.InvalidValue, "factortype", "Factor type is not pending in login process"); var factor = process.Login.ExtraFactors.FirstOrDefault(f => f.FactorType == pinRequest.FactorType); Context.ThrowIfNull(factor, ClientFaultCodes.ObjectNotFound, "factor", "Login factor (email or phone) not setup in user account; factor type: {0}", pinRequest.FactorType); _processService.SendPin(process, factor); }
public async Task SendPinForMultiFactor([FromBody] SendPinRequest pinRequest) { var session = OpContext.OpenSession(); var process = GetMutiFactorProcess(session, pinRequest.ProcessToken); OpContext.ThrowIf(process.CurrentFactor != null, ClientFaultCodes.InvalidAction, "token", "Factor verification pending, the previous process step is not completed."); var pendingFactorTypes = process.PendingFactors; OpContext.ThrowIf(!pendingFactorTypes.IsSet(pinRequest.FactorType), ClientFaultCodes.InvalidValue, "factortype", "Factor type is not pending in login process"); var factor = process.Login.ExtraFactors.FirstOrDefault(f => f.FactorType == pinRequest.FactorType); OpContext.ThrowIfNull(factor, ClientFaultCodes.ObjectNotFound, "factor", "Login factor (email or phone) not setup in user account; factor type: {0}", pinRequest.FactorType); var processService = OpContext.App.GetService <ILoginProcessService>(); await processService.SendPinAsync(process, factor); }
public void SendPin(string token, SendPinRequest request) { Context.WebContext.MarkConfidential(); Context.ThrowIfNull(request, ClientFaultCodes.ContentMissing, "SendPinRequest", "Pin request object must be provided."); Context.ValidateNotEmpty(token, "ProcessToken", "Process token should be provided."); Context.ValidateNotEmpty(request.Factor, "Factor", "Factor (email or phone) should be provided."); Context.ThrowValidation(); var process = GetActiveProcess(token, confirmedOnly: false); if(process == null) return; //no indication process exist or not Context.ThrowIf(process.CurrentFactor != null, ClientFaultCodes.InvalidAction, "token", "The previous process step is not completed."); var iFactor = _processService.FindLoginExtraFactor(process.Login, request.Factor); //now having completed at least one extra factor, we can openly indicate that we could not find next factor Context.ThrowIfNull(iFactor, ClientFaultCodes.InvalidValue, "factor", "Login factor (email or phone) is not found for a user."); //Check that factor type is one in the pending steps var factorOk = process.PendingFactors.IsSet(iFactor.FactorType); Context.ThrowIf(!factorOk, ClientFaultCodes.InvalidValue, "factor", "Login factor type attempted (email or phone) is not pending in the process."); _processService.SendPin(process, iFactor, request.Factor); //we use factor from request, to avoid unencrypting twice }
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(); }