2fa login form
parent
4fd644e10b
commit
e6f5b8fdbf
|
@ -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 {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'));
|
||||
|
|
Loading…
Reference in New Issue