refactor Authenticator
This commit is contained in:
parent
dff3f69cc2
commit
a545324172
|
@ -14,15 +14,14 @@ object Account extends LilaController {
|
|||
|
||||
private def env = Env.user
|
||||
private def relationEnv = Env.relation
|
||||
private def forms = lila.user.DataForm
|
||||
|
||||
def profile = Auth { implicit ctx => me =>
|
||||
Ok(html.account.profile(me, forms profileOf me)).fuccess
|
||||
Ok(html.account.profile(me, env.forms profileOf me)).fuccess
|
||||
}
|
||||
|
||||
def profileApply = AuthBody { implicit ctx => me =>
|
||||
implicit val req: Request[_] = ctx.body
|
||||
FormFuResult(forms.profile) { err =>
|
||||
FormFuResult(env.forms.profile) { err =>
|
||||
fuccess(html.account.profile(me, err))
|
||||
} { profile =>
|
||||
UserRepo.setProfile(me.id, profile) inject Redirect(routes.User show me.username)
|
||||
|
@ -79,7 +78,7 @@ object Account extends LilaController {
|
|||
}
|
||||
|
||||
def passwd = Auth { implicit ctx => me =>
|
||||
forms passwd me map { form =>
|
||||
env.forms passwd me map { form =>
|
||||
Ok(html.account.passwd(form))
|
||||
}
|
||||
}
|
||||
|
@ -87,11 +86,11 @@ object Account extends LilaController {
|
|||
def passwdApply = AuthBody { implicit ctx => me =>
|
||||
HasherRateLimit(me.username) {
|
||||
implicit val req = ctx.body
|
||||
forms passwd me flatMap { form =>
|
||||
env.forms passwd me flatMap { form =>
|
||||
FormFuResult(form) { err =>
|
||||
fuccess(html.account.passwd(err))
|
||||
} { data =>
|
||||
UserRepo.passwd(me.id, data.newPasswd1) inject
|
||||
Env.user.authenticator.setPassword(me.id, data.newPasswd1) inject
|
||||
Redirect(s"${routes.Account.passwd}?ok=1")
|
||||
}
|
||||
}
|
||||
|
@ -144,7 +143,7 @@ object Account extends LilaController {
|
|||
FormFuResult(Env.security.forms.closeAccount) { err =>
|
||||
fuccess(html.account.close(me, err))
|
||||
} { password =>
|
||||
UserRepo.authenticateById(me.id, password).map(_.isDefined) flatMap {
|
||||
Env.user.authenticator.authenticateById(me.id, password).map(_.isDefined) flatMap {
|
||||
case false => BadRequest(html.account.close(me, Env.security.forms.closeAccount)).fuccess
|
||||
case true => doClose(me) inject {
|
||||
Redirect(routes.User show me.username) withCookies LilaCookie.newSession
|
||||
|
|
|
@ -167,7 +167,8 @@ object Auth extends LilaController {
|
|||
lila.mon.user.register.mustConfirmEmail(mustConfirm.value)()
|
||||
authLog(data.username, s"Signup website must confirm email: $mustConfirm")
|
||||
val email = env.emailAddressValidator.validate(data.realEmail) err s"Invalid email ${data.email}"
|
||||
UserRepo.create(data.username, data.password, email, ctx.blindMode, none,
|
||||
val passwordHash = Env.user.authenticator passEnc data.password
|
||||
UserRepo.create(data.username, passwordHash, email, ctx.blindMode, none,
|
||||
mustConfirmEmail = mustConfirm.value)
|
||||
.flatten(s"No user could be created for ${data.username}")
|
||||
.map(_ -> email).flatMap {
|
||||
|
@ -192,7 +193,8 @@ object Auth extends LilaController {
|
|||
lila.mon.user.register.mustConfirmEmail(mustConfirm.value)()
|
||||
authLog(data.username, s"Signup mobile must confirm email: $mustConfirm")
|
||||
val email = env.emailAddressValidator.validate(data.realEmail) err s"Invalid email ${data.email}"
|
||||
UserRepo.create(data.username, data.password, email, false, apiVersion.some,
|
||||
val passwordHash = Env.user.authenticator passEnc data.password
|
||||
UserRepo.create(data.username, passwordHash, email, false, apiVersion.some,
|
||||
mustConfirmEmail = mustConfirm.value)
|
||||
.flatten(s"No user could be created for ${data.username}")
|
||||
.map(_ -> email).flatMap {
|
||||
|
@ -315,7 +317,7 @@ object Auth extends LilaController {
|
|||
FormFuResult(forms.passwdReset) { err =>
|
||||
fuccess(html.auth.passwordResetConfirm(user, token, err, false.some))
|
||||
} { data =>
|
||||
UserRepo.passwd(user.id, data.newPasswd1) >>
|
||||
Env.user.authenticator.setPassword(user.id, data.newPasswd1) >>
|
||||
env.store.disconnect(user.id) >>
|
||||
authenticateUser(user) >>-
|
||||
lila.mon.user.auth.passwordResetConfirm("success")()
|
||||
|
|
|
@ -24,7 +24,7 @@ object Cli extends LilaController {
|
|||
}
|
||||
|
||||
private def CliAuth(password: String)(op: => Fu[Result]): Fu[Result] =
|
||||
lila.user.UserRepo.authenticateById(Env.api.CliUsername, password).map(_.isDefined) flatMap {
|
||||
Env.user.authenticator.authenticateById(Env.api.CliUsername, password).map(_.isDefined) flatMap {
|
||||
_.fold(op, fuccess(Unauthorized))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,6 @@ private[security] final class Cli extends lila.common.Cli {
|
|||
|
||||
def process = {
|
||||
|
||||
case "security" :: "passwd" :: uid :: pwd :: Nil =>
|
||||
perform(uid, user => UserRepo.passwd(user.id, pwd))
|
||||
|
||||
case "security" :: "roles" :: uid :: Nil =>
|
||||
UserRepo named uid map {
|
||||
_.fold("User %s not found" format uid)(_.roles mkString " ")
|
||||
|
|
|
@ -9,6 +9,7 @@ import lila.user.{ User, UserRepo }
|
|||
|
||||
final class DataForm(
|
||||
val captcher: akka.actor.ActorSelection,
|
||||
authenticator: lila.user.Authenticator,
|
||||
emailValidator: EmailAddressValidator
|
||||
) extends lila.hub.CaptchedForm {
|
||||
|
||||
|
@ -81,7 +82,7 @@ final class DataForm(
|
|||
_.samePasswords
|
||||
))
|
||||
|
||||
def changeEmail(u: User, old: Option[EmailAddress]) = UserRepo loginCandidate u map { candidate =>
|
||||
def changeEmail(u: User, old: Option[EmailAddress]) = authenticator loginCandidate u map { candidate =>
|
||||
Form(mapping(
|
||||
"passwd" -> nonEmptyText.verifying("incorrectPassword", candidate.check),
|
||||
"email" -> acceptableUniqueEmail(candidate.user.some).verifying(emailValidator differentConstraint old)
|
||||
|
|
|
@ -8,6 +8,7 @@ import lila.common.PimpedConfig._
|
|||
final class Env(
|
||||
config: Config,
|
||||
captcher: akka.actor.ActorSelection,
|
||||
authenticator: lila.user.Authenticator,
|
||||
system: akka.actor.ActorSystem,
|
||||
scheduler: lila.common.Scheduler,
|
||||
asyncCache: lila.memo.AsyncCache.Builder,
|
||||
|
@ -66,6 +67,7 @@ final class Env(
|
|||
|
||||
lazy val forms = new DataForm(
|
||||
captcher = captcher,
|
||||
authenticator = authenticator,
|
||||
emailValidator = emailAddressValidator
|
||||
)
|
||||
|
||||
|
@ -129,7 +131,7 @@ final class Env(
|
|||
scheduler.once(30 seconds)(tor.refresh(_ => funit))
|
||||
scheduler.effect(TorRefreshDelay, "Refresh Tor exit nodes")(tor.refresh(firewall.unblockIps))
|
||||
|
||||
lazy val api = new SecurityApi(storeColl, firewall, geoIP, emailAddressValidator)
|
||||
lazy val api = new SecurityApi(storeColl, firewall, geoIP, authenticator, emailAddressValidator)
|
||||
|
||||
lazy val csrfRequestHandler = new CSRFRequestHandler(NetDomain)
|
||||
|
||||
|
@ -144,6 +146,7 @@ object Env {
|
|||
lazy val current = "security" boot new Env(
|
||||
config = lila.common.PlayApp loadConfig "security",
|
||||
db = lila.db.Env.current,
|
||||
authenticator = lila.user.Env.current.authenticator,
|
||||
system = lila.common.PlayApp.system,
|
||||
scheduler = lila.common.PlayApp.scheduler,
|
||||
asyncCache = lila.memo.Env.current.asyncCache,
|
||||
|
|
|
@ -17,6 +17,7 @@ final class SecurityApi(
|
|||
coll: Coll,
|
||||
firewall: Firewall,
|
||||
geoIP: GeoIP,
|
||||
authenticator: lila.user.Authenticator,
|
||||
emailValidator: EmailAddressValidator
|
||||
) {
|
||||
|
||||
|
@ -39,8 +40,8 @@ final class SecurityApi(
|
|||
|
||||
def loadLoginForm(str: String): Fu[Form[Option[User]]] = {
|
||||
emailValidator.validate(EmailAddress(str)) match {
|
||||
case Some(email) => UserRepo.loginCandidateByEmail(email)
|
||||
case None if User.couldBeUsername(str) => UserRepo.loginCandidateById(User normalize str)
|
||||
case Some(email) => authenticator.loginCandidateByEmail(email)
|
||||
case None if User.couldBeUsername(str) => authenticator.loginCandidateById(User normalize str)
|
||||
case _ => fuccess(none)
|
||||
}
|
||||
} map loadedLoginForm _
|
||||
|
|
92
modules/user/src/main/Authenticator.scala
Normal file
92
modules/user/src/main/Authenticator.scala
Normal file
|
@ -0,0 +1,92 @@
|
|||
package lila.user
|
||||
|
||||
import com.roundeights.hasher.Implicits._
|
||||
import reactivemongo.bson.Macros
|
||||
|
||||
import lila.common.EmailAddress
|
||||
import lila.db.dsl._
|
||||
import lila.user.User.{ BSONFields => F }
|
||||
|
||||
final class Authenticator(
|
||||
passHasher: PasswordHasher,
|
||||
userRepo: UserRepo.type,
|
||||
upgradeShaPasswords: Boolean,
|
||||
onShaLogin: () => Unit
|
||||
) {
|
||||
import Authenticator._
|
||||
|
||||
def passEnc(pass: String): Array[Byte] = passHasher.hash(pass)
|
||||
|
||||
def compare(auth: AuthData, p: String): Boolean = {
|
||||
val newP = auth.salt.fold(p) { s =>
|
||||
val salted = s"$p{$s}" // BC
|
||||
(~auth.sha512).fold(salted.sha512, salted.sha1).hex
|
||||
}
|
||||
auth.bpass match {
|
||||
// Deprecated fallback. Log & fail after DB migration.
|
||||
case None => auth.password ?? { p => onShaLogin(); p == newP }
|
||||
case Some(bHash) => passHasher.check(bHash, newP)
|
||||
}
|
||||
}
|
||||
|
||||
def authenticateById(id: User.ID, password: String): Fu[Option[User]] =
|
||||
loginCandidateById(id) map { _ flatMap { _(password) } }
|
||||
|
||||
def authenticateByEmail(email: EmailAddress, password: String): Fu[Option[User]] =
|
||||
loginCandidateByEmail(email) map { _ flatMap { _(password) } }
|
||||
|
||||
// This creates a bcrypt hash using the existing sha as input,
|
||||
// allowing us to migrate all users in bulk.
|
||||
def upgradePassword(a: AuthData) = (a.bpass, a.password) match {
|
||||
case (None, Some(pass)) => Some(userRepo.coll.update(
|
||||
$id(a._id),
|
||||
$set(F.bpass -> passEnc(pass)) ++ $unset(F.password)
|
||||
).void >>- lila.mon.user.auth.shaBcUpgrade())
|
||||
|
||||
case _ => None
|
||||
}
|
||||
|
||||
def loginCandidate(u: User): Fu[User.LoginCandidate] =
|
||||
loginCandidateById(u.id) map { _ | User.LoginCandidate(u, _ => false) }
|
||||
|
||||
def loginCandidateById(id: User.ID): Fu[Option[User.LoginCandidate]] =
|
||||
loginCandidate($id(id))
|
||||
|
||||
def loginCandidateByEmail(email: EmailAddress): Fu[Option[User.LoginCandidate]] =
|
||||
loginCandidate($doc(F.email -> email))
|
||||
|
||||
def setPassword(id: User.ID, pass: String): Funit =
|
||||
userRepo.coll.update(
|
||||
$id(id),
|
||||
$set(F.bpass -> passEnc(pass)) ++ $unset(F.salt, F.password, F.sha512)
|
||||
).void
|
||||
|
||||
private def authWithBenefits(auth: AuthData)(p: String): Boolean = {
|
||||
val res = compare(auth, p)
|
||||
if (res && auth.salt.isDefined && upgradeShaPasswords)
|
||||
setPassword(id = auth._id, pass = p) >>- lila.mon.user.auth.bcFullMigrate()
|
||||
res
|
||||
}
|
||||
|
||||
private def loginCandidate(select: Bdoc): Fu[Option[User.LoginCandidate]] =
|
||||
userRepo.coll.uno[AuthData](select)(AuthDataBSONHandler) zip userRepo.coll.uno[User](select) map {
|
||||
case (Some(authData), Some(user)) if user.enabled =>
|
||||
User.LoginCandidate(user, authWithBenefits(authData)).some
|
||||
case _ => none
|
||||
}
|
||||
}
|
||||
|
||||
object Authenticator {
|
||||
|
||||
case class AuthData(
|
||||
_id: User.ID,
|
||||
bpass: Option[Array[Byte]] = None,
|
||||
password: Option[String] = None,
|
||||
salt: Option[String] = None,
|
||||
sha512: Option[Boolean] = None
|
||||
) {
|
||||
|
||||
def hashToken: String = bpass.fold(~password) { _.sha512.hex }
|
||||
}
|
||||
implicit val AuthDataBSONHandler = Macros.handler[AuthData]
|
||||
}
|
|
@ -3,7 +3,7 @@ package lila.user
|
|||
import play.api.data._
|
||||
import play.api.data.Forms._
|
||||
|
||||
object DataForm {
|
||||
final class DataForm(authenticator: Authenticator) {
|
||||
|
||||
val note = Form(mapping(
|
||||
"text" -> nonEmptyText(minLength = 3, maxLength = 2000),
|
||||
|
@ -36,13 +36,16 @@ object DataForm {
|
|||
def samePasswords = newPasswd1 == newPasswd2
|
||||
}
|
||||
|
||||
def passwd(u: User) = UserRepo loginCandidate u map { candidate =>
|
||||
def passwd(u: User) = authenticator loginCandidate u map { candidate =>
|
||||
Form(mapping(
|
||||
"oldPasswd" -> nonEmptyText.verifying("incorrectPassword", candidate.check),
|
||||
"newPasswd1" -> nonEmptyText(minLength = 2),
|
||||
"newPasswd2" -> nonEmptyText(minLength = 2)
|
||||
)(Passwd.apply)(Passwd.unapply).verifying("the new passwords don't match", _.samePasswords))
|
||||
}
|
||||
}
|
||||
|
||||
object DataForm {
|
||||
|
||||
val title = Form(single("title" -> optional(nonEmptyText)))
|
||||
}
|
||||
|
|
|
@ -44,8 +44,6 @@ final class Env(
|
|||
|
||||
lazy val jsonView = new JsonView(isOnline)
|
||||
|
||||
val forms = DataForm
|
||||
|
||||
def lightUser(id: User.ID): Fu[Option[lila.common.LightUser]] = lightUserApi async id
|
||||
def lightUserSync(id: User.ID): Option[lila.common.LightUser] = lightUserApi sync id
|
||||
|
||||
|
@ -90,16 +88,18 @@ final class Env(
|
|||
rankingApi = rankingApi
|
||||
)
|
||||
|
||||
lazy val passwordAuth = new Authenticator(
|
||||
new PasswordHasher(
|
||||
lazy val authenticator = new Authenticator(
|
||||
passHasher = new PasswordHasher(
|
||||
secret = PasswordBPassSecret,
|
||||
logRounds = 10,
|
||||
hashTimer = lila.mon.measure(_.user.auth.hashTime)
|
||||
),
|
||||
userRepo = UserRepo,
|
||||
upgradeShaPasswords = PasswordUpgradeSha,
|
||||
onShaLogin = lila.mon.user.auth.shaLogin
|
||||
)
|
||||
|
||||
lazy val upgradeShaPasswords = PasswordUpgradeSha
|
||||
lazy val forms = new DataForm(authenticator)
|
||||
}
|
||||
|
||||
object Env {
|
||||
|
|
|
@ -258,34 +258,3 @@ object User {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class Authenticator(passHasher: PasswordHasher, onShaLogin: () => Unit) {
|
||||
import com.roundeights.hasher.Implicits._
|
||||
|
||||
def passEnc(pass: String): Array[Byte] = passHasher.hash(pass)
|
||||
|
||||
case class AuthData(
|
||||
_id: String,
|
||||
bpass: Option[Array[Byte]] = None,
|
||||
password: Option[String] = None,
|
||||
salt: Option[String] = None,
|
||||
sha512: Option[Boolean] = None
|
||||
) {
|
||||
def compare(p: String): Boolean = {
|
||||
val newP = salt.fold(p) { s =>
|
||||
val salted = s"$p{$s}" // BC
|
||||
(~sha512).fold(salted.sha512, salted.sha1).hex
|
||||
}
|
||||
|
||||
bpass match {
|
||||
// Deprecated fallback. Log & fail after DB migration.
|
||||
case None => password ?? { p => onShaLogin(); p == newP }
|
||||
case Some(bHash) => passHasher.check(bHash, newP)
|
||||
}
|
||||
}
|
||||
|
||||
def hashToken = bpass.fold(~password) { _.sha512.hex }
|
||||
}
|
||||
|
||||
implicit val AuthDataBSONHandler = Macros.handler[AuthData]
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ object UserRepo {
|
|||
import User.{ BSONFields => F }
|
||||
|
||||
// dirty
|
||||
private val coll = Env.current.userColl
|
||||
private[user] val coll = Env.current.userColl
|
||||
import reactivemongo.api.collections.bson.BSONBatchCommands.AggregationFramework.{ Match, Group, SumField }
|
||||
|
||||
val normalize = User normalize _
|
||||
|
@ -224,55 +224,9 @@ object UserRepo {
|
|||
def incToints(id: ID, nb: Int) = coll.update($id(id), $inc("toints" -> nb))
|
||||
def removeAllToints = coll.update($empty, $unset("toints"), multi = true)
|
||||
|
||||
def authenticateById(id: ID, password: String): Fu[Option[User]] =
|
||||
loginCandidateById(id) map { _ flatMap { _(password) } }
|
||||
|
||||
def authenticateByEmail(email: EmailAddress, password: String): Fu[Option[User]] =
|
||||
loginCandidateByEmail(email) map { _ flatMap { _(password) } }
|
||||
|
||||
import Env.current.passwordAuth._
|
||||
|
||||
// This creates a bcrypt hash using the existing sha as input,
|
||||
// allowing us to migrate all users in bulk.
|
||||
def upgradePassword(a: AuthData) = (a.bpass, a.password) match {
|
||||
case (None, Some(pass)) => Some(coll.update(
|
||||
$id(a._id),
|
||||
$set(F.bpass -> passEnc(pass)) ++ $unset(F.password)
|
||||
).void >>- lila.mon.user.auth.shaBcUpgrade())
|
||||
|
||||
case _ => None
|
||||
}
|
||||
|
||||
def loginCandidateById(id: ID): Fu[Option[User.LoginCandidate]] =
|
||||
loginCandidate($id(id))
|
||||
|
||||
def loginCandidateByEmail(email: EmailAddress): Fu[Option[User.LoginCandidate]] =
|
||||
loginCandidate($doc(F.email -> email))
|
||||
|
||||
def loginCandidate(u: User): Fu[User.LoginCandidate] =
|
||||
loginCandidateById(u.id) map { _ | User.LoginCandidate(u, _ => false) }
|
||||
|
||||
def authWithBenefits(auth: AuthData)(p: String) = {
|
||||
val res = auth compare p
|
||||
if (res && auth.salt.isDefined && Env.current.upgradeShaPasswords)
|
||||
passwd(id = auth._id, pass = p) >>- lila.mon.user.auth.bcFullMigrate()
|
||||
res
|
||||
}
|
||||
|
||||
private def loginCandidate(select: Bdoc): Fu[Option[User.LoginCandidate]] =
|
||||
coll.uno[AuthData](select) zip coll.uno[User](select) map {
|
||||
case (Some(authData), Some(user)) if user.enabled =>
|
||||
User.LoginCandidate(user, authWithBenefits(authData)).some
|
||||
case _ => none
|
||||
}
|
||||
|
||||
def getPasswordHash(id: ID): Fu[Option[String]] = coll.byId[AuthData](id) map {
|
||||
_.map { _.hashToken }
|
||||
}
|
||||
|
||||
def create(
|
||||
username: String,
|
||||
password: String,
|
||||
passwordHash: Array[Byte],
|
||||
email: EmailAddress,
|
||||
blind: Boolean,
|
||||
mobileApiVersion: Option[ApiVersion],
|
||||
|
@ -280,7 +234,7 @@ object UserRepo {
|
|||
): Fu[Option[User]] =
|
||||
!nameExists(username) flatMap {
|
||||
_ ?? {
|
||||
val doc = newUser(username, password, email, blind, mobileApiVersion, mustConfirmEmail) ++
|
||||
val doc = newUser(username, passwordHash, email, blind, mobileApiVersion, mustConfirmEmail) ++
|
||||
("len" -> BSONInteger(username.size))
|
||||
coll.insert(doc) >> named(normalize(username))
|
||||
}
|
||||
|
@ -347,9 +301,11 @@ object UserRepo {
|
|||
}
|
||||
)
|
||||
|
||||
def passwd(id: ID, pass: String): Funit =
|
||||
coll.update($id(id), $set(F.bpass -> passEnc(pass))
|
||||
++ $unset(F.salt, F.password, F.sha512)).void
|
||||
import Authenticator.AuthDataBSONHandler
|
||||
def getPasswordHash(id: User.ID): Fu[Option[String]] =
|
||||
coll.byId[Authenticator.AuthData](id) map {
|
||||
_.map { _.hashToken }
|
||||
}
|
||||
|
||||
def email(id: ID, email: EmailAddress): Funit =
|
||||
coll.update($id(id), $set(F.email -> email) ++ $unset(F.prevEmail)).void
|
||||
|
@ -444,7 +400,7 @@ object UserRepo {
|
|||
|
||||
private def newUser(
|
||||
username: String,
|
||||
password: String,
|
||||
passwordHash: Array[Byte],
|
||||
email: EmailAddress,
|
||||
blind: Boolean,
|
||||
mobileApiVersion: Option[ApiVersion],
|
||||
|
@ -460,7 +416,7 @@ object UserRepo {
|
|||
F.username -> username,
|
||||
F.email -> email,
|
||||
F.mustConfirmEmail -> mustConfirmEmail.option(DateTime.now),
|
||||
F.bpass -> passEnc(password),
|
||||
F.bpass -> passwordHash,
|
||||
F.perfs -> $empty,
|
||||
F.count -> Count.default,
|
||||
F.enabled -> true,
|
||||
|
|
Loading…
Reference in a new issue