We are building an ASP.NET MVC3 web application for a client.
In this application, the client would like the user to log on using his username/password combination. However, if the userName password is correct, a one-time-pin should be sent to the user’s cell phone. The user should only be authenticated once he enters the correct one-time-pin(OTP).
So I am wondering if the following solution is secure:
- The user logs on with his userName and password. We use Membership.ValidateUser to verify the userName/password, but we don’t set the Auth Cookie yet.
- We store the user’s userName and password in Session.
- We store the fact that the user is ‘half’ logged-in in Session.
- We redirect back to the Login Page.
- The user now gets a chance to enter the OTP and submits to the server a second time.
- We validate the username/password/OTP combination.
- If valid, we set the Forms Authentication Cookie.
PS: this will all happen on SSL.
The hypothetical LogIn page may look as follows (notice the three states):
<h2>Log On</h2>
<div>
@if (User.Identity.IsAuthenticated)
{
<p class="green bold">
You are logged-on fully. Your UserName and Password match, and the OTP you have entered was correct.
</p>
<form action="/Account/LogOff">
<input type="submit" value="Log Off" />
</form>
}
else if (ViewBag.AwaitingOTP)
{
<p>
Hi @ViewBag.UserName
</p>
<p class="orange">
Step 2/2: Please enter the OTP sent to your cell phone.
</p>
<form method="post" action="/Account/VerifyOTPAndLogOn">
<input name="otp" type="text" placeholder="One-Time Pin" />
<input type="submit" />
</form>
}
else
{
<p>
Hi stranger!
</p>
<p class="orange">
Step 1/2: Please enter your username and password.
</p>
<form method="post" action="/Account/LogOnHalfwayAndRequestOTP">
<input name="userName" type="text" placeholder="userName" />
<input name="password" type="password" placeholder="password" />
<input type="submit" value="Log On" />
</form>
}
</div>
The controller code looks as follows:
public class AccountController : ControllerBase
{
public bool HalfwayLoggedOnStillAwaitingOTP
{
get
{
if (Session["AwaitingOTP"] != null)
return (bool)Session["AwaitingOTP"];
return false;
}
set
{
Session["AwaitingOTP"] = value;
}
}
public ActionResult LogOn()
{
ViewBag.AwaitingOTP = HalfwayLoggedOnStillAwaitingOTP;
ViewBag.UserName = Session["UserName"] ?? null;
return View();
}
public ActionResult LogOnHalfwayAndRequestOTP(string userName, string password)
{
//Authenticate user, but not fully... (i.e. we're not setting FormsAuthentication.SetAuthCookie yet)
if (Membership.ValidateUser(userName, password))
HalfwayLoggedOnStillAwaitingOTP = true;
else
HalfwayLoggedOnStillAwaitingOTP = false;
ViewBag.AwaitingOTP = HalfwayLoggedOnStillAwaitingOTP;
ViewBag.UserName = Session["UserName"] ?? null;
//...
//...Call service that sends OTP to the user's cellPhone.
//...
return View("LogOn");
}
public ActionResult VerifyOTPAndLogOn(string otp)
{
string userName = (string) Session["UserName"];
string password = (string) Session["Password"];
if (OTPIsValid(otp, userName, password))
{
//Set the Forms Auth cookie...
FormsAuthentication.SetAuthCookie(userName, false);
return RedirectToAction("Index", "Home");
}
else
{
//Display a nice error message here.
return View();
}
}
private bool OTPIsValid(string otp, string userName, string password)
{
//...
//...Validate OTP here. For now we assume the user entered the correct OTP.
//...
return true;
}
}
Are there any security holes in this implementation? I’m not sure how secure it is to store the username/password in session, or if it’s safe to trust that the user is really authenticated when the Session[“AwaitingOTP”] value is set.
I will answer my own question with information that I have gathered over the past 24 hours. Hopefully this is correct.
What worried me about the proposed solution is the short period of time that we rely solely on Session state to store the fact that the user was authenticated. (because only after the OTP is entered do we use proper Forms Authentication).
The ASP.NET session key is stored in a cookie. It is encrypted, so is relatively secure. But it is still vulnerable to the same threats as standard forms authentication:
I would change the proposed solution to not store the user’s password in session (and only store his userName). And I would make sure that SSL is used.