2fa login form

pull/4336/head
Thibault Duplessis 2018-05-06 19:10:04 +02:00
parent 4fd644e10b
commit e6f5b8fdbf
6 changed files with 69 additions and 20 deletions

View File

@ -86,10 +86,16 @@ object Auth extends LilaController {
err => {
chargeIpLimiter(1)
negotiate(
html = Unauthorized(html.auth.login(err, referrer)).fuccess,
html = fuccess {
err.errors match {
case List(play.api.data.FormError("", List("MissingTotpToken" | "InvalidTotpToken"), _)) => Ok("2fa")
case _ => Unauthorized(html.auth.login(err, referrer))
}
},
api = _ => Unauthorized(errorsAsJson(err)).fuccess
)
}, {
},
result => result.toOption match {
case None => InternalServerError("Authentication error").fuccess
case Some(u) =>
UserRepo.email(u.id) foreach {

View File

@ -8,12 +8,23 @@ moreJs = jsTag("login.js")) {
<h1 class="lichess_title">@trans.signIn()</h1>
<form class="login" action="@routes.Auth.authenticate@referrer.map { ref =>?referrer=@{java.net.URLEncoder.encode(ref, "US-ASCII")}}" method="POST">
@globalError(form)
<ul>
<ul class="one-factor">
@auth.formFields(form("username"), form("password"), none, register = false)
<li>
<button type="submit" class="submit button" data-icon="F"> @trans.signIn()</button>
</li>
</ul>
<ul class="two-factor none">
@defining(form("token")) { field =>
<li class="token">
<label for="@field.name">2FA Token</label>
<input type="text" id="@field.name" name="@field.name" />
</li>
<li>
<button type="submit" class="submit button" data-icon="F"> @trans.signIn()</button>
</li>
}
</ul>
</form>
</div>
<div class="alternative">

View File

@ -4,6 +4,7 @@ import org.joda.time.DateTime
import ornicar.scalalib.Random
import play.api.data._
import play.api.data.Forms._
import play.api.data.validation.{ Constraint, Valid => FormValid, Invalid, ValidationError }
import play.api.mvc.RequestHeader
import reactivemongo.api.ReadPreference
import reactivemongo.bson._
@ -13,6 +14,7 @@ import lila.common.{ ApiVersion, IpAddress, EmailAddress }
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.dsl._
import lila.user.{ User, UserRepo }
import User.LoginCandidate
final class SecurityApi(
coll: Coll,
@ -34,14 +36,22 @@ final class SecurityApi(
"password" -> nonEmptyText
))
private def loadedLoginForm(candidate: Option[User.LoginCandidate]) = Form(mapping(
private def loadedLoginForm(candidate: Option[LoginCandidate]) = Form(mapping(
"username" -> nonEmptyText,
"password" -> nonEmptyText,
"token" -> optional(nonEmptyText)
)(authenticateCandidate(candidate))(_.map(u => (u.username, "", none)))
.verifying("invalidUsernameOrPassword", _.isDefined))
)(authenticateCandidate(candidate)) {
case LoginCandidate.Success(user) => (user.username, "", none).some
case _ => none
}.verifying(Constraint { (t: LoginCandidate.Result) =>
t match {
case LoginCandidate.Success(_) => FormValid
case LoginCandidate.InvalidUsernameOrPassword => Invalid(Seq(ValidationError("invalidUsernameOrPassword")))
case err => Invalid(Seq(ValidationError(err.toString)))
}
}))
def loadLoginForm(str: String): Fu[Form[Option[User]]] = {
def loadLoginForm(str: String): Fu[Form[LoginCandidate.Result]] = {
emailValidator.validate(EmailAddress(str)) match {
case Some(email) => authenticator.loginCandidateByEmail(email)
case None if User.couldBeUsername(str) => authenticator.loginCandidateById(User normalize str)
@ -49,11 +59,11 @@ final class SecurityApi(
}
} map loadedLoginForm _
private def authenticateCandidate(candidate: Option[User.LoginCandidate])(
private def authenticateCandidate(candidate: Option[LoginCandidate])(
username: String,
password: String,
token: Option[String]
): Option[User] = candidate ?? {
): LoginCandidate.Result = candidate.fold[LoginCandidate.Result](LoginCandidate.InvalidUsernameOrPassword) {
_(User.PasswordAndToken(User.ClearPassword(password), token map User.TotpToken.apply))
}

View File

@ -24,10 +24,10 @@ final class Authenticator(
}
def authenticateById(id: User.ID, passwordAndToken: PasswordAndToken): Fu[Option[User]] =
loginCandidateById(id) map { _ flatMap { _(passwordAndToken) } }
loginCandidateById(id) map { _ flatMap { _ option passwordAndToken } }
def authenticateByEmail(email: EmailAddress, passwordAndToken: PasswordAndToken): Fu[Option[User]] =
loginCandidateByEmail(email) map { _ flatMap { _(passwordAndToken) } }
loginCandidateByEmail(email) map { _ flatMap { _ option passwordAndToken } }
def loginCandidate(u: User): Fu[User.LoginCandidate] =
loginCandidateById(u.id) map { _ | User.LoginCandidate(u, _ => false) }

View File

@ -122,13 +122,28 @@ object User {
type CredentialCheck = ClearPassword => Boolean
case class LoginCandidate(user: User, check: CredentialCheck) {
def apply(p: PasswordAndToken): Option[User] = {
val res = check(p.password) && user.totpSecret.fold(true) { tp =>
p.token ?? tp.verify
}
lila.mon.user.auth.result(res)()
res option user
import LoginCandidate._
def apply(p: PasswordAndToken): Result = {
val res =
if (check(p.password)) user.totpSecret.fold[Result](Success(user)) { tp =>
p.token.fold[Result](MissingTotpToken) { token =>
if (tp verify token) Success(user) else InvalidTotpToken
}
}
else InvalidUsernameOrPassword
lila.mon.user.auth.result(res.success)()
res
}
def option(p: PasswordAndToken): Option[User] = apply(p).toOption
}
object LoginCandidate {
sealed abstract class Result(val toOption: Option[User]) {
def success = toOption.isDefined
}
case class Success(user: User) extends Result(user.some)
case object InvalidUsernameOrPassword extends Result(none)
case object MissingTotpToken extends Result(none)
case object InvalidTotpToken extends Result(none)
}
val anonymous = "Anonymous"

View File

@ -4,16 +4,23 @@ $(function() {
function load($f) {
$f.submit(function() {
$f.find('.submit').attr('disabled', true).attr('data-icon', null).html(lichess.spinnerHtml);
$f.find('.submit').attr('disabled', true);
$.ajax({
url: $f.attr('action'),
method: $f.attr('method'),
data: {
username: $f.find('.username input').val(),
password: $f.find('.password input').val()
password: $f.find('.password input').val(),
token: $f.find('.token input').val()
},
success: function(res) {
return lichess.redirect(res.substr(3));
if (res === '2fa') {
$f.find('.one-factor').hide();
$f.find('.two-factor').show();
$f.find('.token input').val('');
$f.find('.submit').attr('disabled', false);
}
else lichess.redirect(res);
},
error: function(err) {
$f.replaceWith($(err.responseText).find('form.login'));