GoogleAuthenticator (2FA) fails validation on mobile browsers but works on desktop in C# MVC

2 days ago 4
ARTICLE AD BOX

I am implementing Two-Factor Authentication in a C# ASP.NET MVC project using the GoogleAuthenticator NuGet package (v3.2.0).

The Issue: The authentication works perfectly on all desktop browsers. However, when using a mobile browser (Chrome or Safari on iOS/Android), the ValidateTwoFactorPIN method consistently returns false, even when the code entered is exactly what is shown in the Google Authenticator app.

What I've tried:

Confirmed that the passcode string reaches the controller correctly on mobile (exactly 6 digits).

Confirmed the UserUniqueKey (the secret) is consistent across desktop and mobile sessions.

Verified that desktop browsers work 100% of the time, while mobile fails 100% of the time.

Since TOTP is time-based, I suspect a synchronization issue. However, the mobile phone and the server are both synced to internet time.

Why would this validation fail specifically on mobile browsers? Is there a known issue with how GoogleAuthenticator handles time windows or how mobile browsers might be sending the request?

My Setup:

The View (OTP Input): I use six separate input boxes for a better UX. I use JavaScript to join them into a single string before sending via AJAX.

<div class="col-12 col-sm-10 col-md-6 col-lg-4 login-sec otp_form" style="display:none;"> <h2 class="text-center">Enter OTP</h2> <div class="otp-text"><span>Enter OTP from the Google Authenticator App...</span></div> <div><span id="lblSkills" style="color: red; font-weight: bold;"></span></div> <div class="digit-group"> <input class="otpClass" id="codeBox1" type="tel" inputmode="numeric" pattern="[0-9]" maxlength="1" onkeyup="onKeyUpEvent(1, event)" onfocus="onFocusEvent(1)" oninput="this.value=this.value.replace(/[^0-9]/g,'').slice(0,1);" autocomplete="off" /> <input class="otpClass" id="codeBox2" type="tel" inputmode="numeric" pattern="[0-9]" maxlength="1" onkeyup="onKeyUpEvent(2, event)" onfocus="onFocusEvent(2)" oninput="this.value=this.value.replace(/[^0-9]/g,'').slice(0,1);" autocomplete="off" /> <input class="otpClass" id="codeBox3" type="tel" inputmode="numeric" pattern="[0-9]" maxlength="1" onkeyup="onKeyUpEvent(3, event)" onfocus="onFocusEvent(3)" oninput="this.value=this.value.replace(/[^0-9]/g,'').slice(0,1);" autocomplete="off" /> <input class="otpClass" id="codeBox4" type="tel" inputmode="numeric" pattern="[0-9]" maxlength="1" onkeyup="onKeyUpEvent(4, event)" onfocus="onFocusEvent(4)" oninput="this.value=this.value.replace(/[^0-9]/g,'').slice(0,1);" autocomplete="off" /> <input class="otpClass" id="codeBox5" type="tel" inputmode="numeric" pattern="[0-9]" maxlength="1" onkeyup="onKeyUpEvent(5, event)" onfocus="onFocusEvent(5)" oninput="this.value=this.value.replace(/[^0-9]/g,'').slice(0,1);" autocomplete="off" /> <input class="otpClass" id="codeBox6" type="tel" inputmode="numeric" pattern="[0-9]" maxlength="1" onkeyup="onKeyUpEvent(6, event)" onfocus="onFocusEvent(6)" oninput="this.value=this.value.replace(/[^0-9]/g,'').slice(0,1);" autocomplete="off" /> </div> <input id="passcode" type="text" maxlength="6" name="passcode" class="d-none" /> <div class="container-login100-form-btn"> <button type="button" class="btn btn-login float-right" id="btnVerify">Verify</button> </div>

Client-Side Script: I clean the input to ensure only digits are sent.

$("#btnVerify").on("click", function () { const passcodeDigits = []; for (let i = 1; i <= 6; i++) { let value = $("#codeBox" + i).val().replace(/\D/g, ''); if (value.length > 0) passcodeDigits.push(value.charAt(0)); } const passcode = passcodeDigits.join(""); const UserUniqueKey = $("#HfUserUniqueKey").val(); $.ajax({ type: "POST", url: "/Login/Verify2FA", contentType: "application/json; charset=utf-8", data: JSON.stringify({ passcode: passcode, UserUniqueKey: UserUniqueKey }), success: function (d) { if (d === "Y") window.location.href = "/Home/Index"; else alert("Enter Correct OTP..."); } }); });

Server-Side Controller:

[HttpPost] public JsonResult Verify2FA(string passcode, string UserUniqueKey) { if (string.IsNullOrWhiteSpace(passcode) || UserUniqueKey == null) return Json("N"); TwoFactorAuthenticator tfa = new TwoFactorAuthenticator(); // UserUniqueKey is the secret saved in the DB bool isValid = tfa.ValidateTwoFactorPIN(UserUniqueKey, passcode.Trim()); if (isValid) { FormsAuthentication.SetAuthCookie(Session["UserID_Temp"].ToString(), false); return Json("Y"); } return Json("N"); }
Read Entire Article