lila/app/controllers/Auth.scala

540 lines
20 KiB
Scala
Raw Normal View History

2013-03-14 17:51:39 -06:00
package controllers
2017-09-27 18:23:16 -06:00
import ornicar.scalalib.Zero
import play.api.data.FormError
2015-02-05 07:35:58 -07:00
import play.api.libs.json._
2017-01-15 05:26:08 -07:00
import play.api.mvc._
2020-04-29 08:58:36 -06:00
import scala.annotation.nowarn
import scala.concurrent.duration._
import views._
2013-12-22 06:15:02 -07:00
2015-06-25 08:54:26 -06:00
import lila.api.Context
2013-03-14 17:51:39 -06:00
import lila.app._
import lila.common.{ EmailAddress, HTTPRequest }
import lila.memo.RateLimit
2020-08-21 14:40:37 -06:00
import lila.security.SecurityForm.{ MagicLink, PasswordReset }
import lila.security.{ FingerPrint, Signup }
import lila.user.User.ClearPassword
2019-12-04 16:39:16 -07:00
import lila.user.{ User => UserModel, PasswordHasher }
2013-03-14 16:22:34 -06:00
2019-12-04 16:39:16 -07:00
final class Auth(
env: Env,
2019-12-05 14:51:18 -07:00
accountC: => Account
2019-12-04 16:39:16 -07:00
) extends LilaController(env) {
2013-03-14 16:22:34 -06:00
2019-12-13 07:30:20 -07:00
private def api = env.security.api
2019-12-04 16:39:16 -07:00
private def forms = env.security.forms
2013-05-04 17:12:53 -06:00
private def mobileUserOk(u: UserModel, sessionId: String): Fu[Result] =
2019-12-04 16:39:16 -07:00
env.round.proxyRepo urgentGames u map { povs =>
2015-02-05 07:35:58 -07:00
Ok {
env.user.jsonView.full(u, withOnline = true) ++ Json.obj(
2019-12-04 16:39:16 -07:00
"nowPlaying" -> JsArray(povs take 20 map env.api.lobbyApi.nowPlaying),
2019-12-13 07:30:20 -07:00
"sessionId" -> sessionId
)
2015-02-05 07:35:58 -07:00
}
}
private def getReferrer(implicit ctx: Context) =
get("referrer").filter(env.api.referrerRedirect.valid) orElse
ctxReq.session.get(api.AccessUri) getOrElse
routes.Lobby.home.url
2020-05-05 22:11:15 -06:00
def authenticateUser(u: UserModel, result: Option[String => Result] = None)(implicit
ctx: Context
2020-06-03 21:11:50 -06:00
): Fu[Result] =
api.saveAuthentication(u.id, ctx.mobileApiVersion) flatMap { sessionId =>
negotiate(
html = fuccess {
result.fold(Redirect(getReferrer))(_(getReferrer))
2020-06-03 21:11:50 -06:00
},
api = _ => mobileUserOk(u, sessionId)
) map authenticateCookie(sessionId)
} recoverWith authRecovery
2014-12-14 17:32:18 -07:00
private def authenticateAppealUser(u: UserModel, redirect: String => Result)(implicit
ctx: Context
): Fu[Result] =
api.appeal.saveAuthentication(u.id) flatMap { sessionId =>
negotiate(
2021-04-16 11:08:50 -06:00
html = redirect(routes.Appeal.landing.url).fuccess map authenticateCookie(sessionId),
api = _ => NotFound.fuccess
2021-04-16 11:08:50 -06:00
)
} recoverWith authRecovery
2018-03-29 16:09:46 -06:00
private def authenticateCookie(sessionId: String)(result: Result)(implicit req: RequestHeader) =
result.withCookies(
2019-12-04 16:39:16 -07:00
env.lilaCookie.withSession {
2019-05-14 02:54:37 -06:00
_ + (api.sessionIdKey -> sessionId) - api.AccessUri - lila.security.EmailConfirm.cookie.name
}
)
2018-03-29 16:09:46 -06:00
private def authRecovery(implicit ctx: Context): PartialFunction[Throwable, Fu[Result]] = {
2019-12-13 07:30:20 -07:00
case lila.security.SecurityApi.MustConfirmEmail(_) =>
fuccess {
if (HTTPRequest isXhr ctx.req) Ok(s"ok:${routes.Auth.checkYourEmail}")
2019-12-13 07:30:20 -07:00
else BadRequest(accountC.renderCheckYourEmail)
}
}
2020-05-05 22:11:15 -06:00
def login =
Open { implicit ctx =>
2020-10-03 08:10:55 -06:00
val referrer = get("referrer").filter(env.api.referrerRedirect.valid)
referrer ifTrue ctx.isAuth match {
2020-05-05 22:11:15 -06:00
case Some(url) => Redirect(url).fuccess // redirect immediately if already logged in
case None => Ok(html.auth.login(api.loginForm, referrer)).fuccess
}
}
2013-05-04 17:12:53 -06:00
2018-05-06 11:29:13 -06:00
private val is2fa = Set("MissingTotpToken", "InvalidTotpToken")
2020-05-05 22:11:15 -06:00
def authenticate =
OpenBody { implicit ctx =>
2021-06-06 01:51:02 -06:00
OnlyHumans {
Firewall {
def redirectTo(url: String) = if (HTTPRequest isXhr ctx.req) Ok(s"ok:$url") else Redirect(url)
implicit val req = ctx.body
val referrer = get("referrer").filterNot(env.api.referrerRedirect.sillyLoginReferrers.contains)
api.usernameOrEmailForm
.bindFromRequest()
.fold(
err =>
negotiate(
html = Unauthorized(html.auth.login(api.loginForm, referrer)).fuccess,
api = _ => Unauthorized(ridiculousBackwardCompatibleJsonError(errorsAsJson(err))).fuccess
),
usernameOrEmail =>
HasherRateLimit(usernameOrEmail, ctx.req) { chargeIpLimiter =>
api.loadLoginForm(usernameOrEmail) flatMap {
_.bindFromRequest()
.fold(
err => {
chargeIpLimiter(1)
negotiate(
html = fuccess {
err.errors match {
case List(FormError("", List(err), _)) if is2fa(err) => Ok(err)
case _ => Unauthorized(html.auth.login(err, referrer))
}
},
api = _ =>
Unauthorized(ridiculousBackwardCompatibleJsonError(errorsAsJson(err))).fuccess
)
},
result =>
result.toOption match {
case None => InternalServerError("Authentication error").fuccess
case Some(u) if u.disabled =>
negotiate(
html = env.mod.logApi.closedByMod(u) flatMap {
case true => authenticateAppealUser(u, redirectTo)
case _ => redirectTo(routes.Account.reopen.url).fuccess
},
api = _ => Unauthorized(jsonError("This account is closed.")).fuccess
)
case Some(u) =>
env.user.repo.email(u.id) foreach {
_ foreach { garbageCollect(u, _) }
}
authenticateUser(u, Some(redirectTo))
}
)
}
}(rateLimitedFu)
)
}
2020-05-05 22:11:15 -06:00
}
}
2013-05-04 17:12:53 -06:00
2020-05-05 22:11:15 -06:00
def logout =
Open { implicit ctx =>
val currentSessionId = ~env.security.api.reqSessionId(ctx.req)
env.security.store.delete(currentSessionId) >>
env.push.webSubscriptionApi.unsubscribeBySession(currentSessionId) >>
negotiate(
html = Redirect(routes.Auth.login).fuccess,
2020-05-05 22:11:15 -06:00
api = _ => Ok(Json.obj("ok" -> true)).fuccess
).dmap(_.withCookies(env.lilaCookie.newSession))
}
2013-05-04 17:12:53 -06:00
2017-08-24 09:24:00 -06:00
// mobile app BC logout with GET
2020-05-05 22:11:15 -06:00
def logoutGet =
Auth { implicit ctx => _ =>
2020-05-05 22:11:15 -06:00
negotiate(
html = Ok(html.auth.bits.logout()).fuccess,
2020-05-05 22:11:15 -06:00
api = _ => {
ctxReq.session get api.sessionIdKey foreach env.security.store.delete
Ok(Json.obj("ok" -> true)).withCookies(env.lilaCookie.newSession).fuccess
}
)
}
2017-08-24 09:24:00 -06:00
2020-05-05 22:11:15 -06:00
def signup =
Open { implicit ctx =>
NoTor {
2020-07-17 02:39:51 -06:00
Ok(html.auth.signup(forms.signup.website)).fuccess
2020-05-05 22:11:15 -06:00
}
2013-05-04 17:12:53 -06:00
}
2019-07-22 01:46:07 -06:00
private def authLog(user: String, email: String, msg: String) =
lila.log("auth").info(s"$user $email $msg")
2020-05-05 22:11:15 -06:00
def signupPost =
OpenBody { implicit ctx =>
implicit val req = ctx.body
NoTor {
Firewall {
forms.preloadEmailDns >> negotiate(
html = env.security.signup
.website(ctx.blind)
.flatMap {
2020-05-05 22:11:15 -06:00
case Signup.RateLimited => limitedDefault.zero.fuccess
case Signup.MissingCaptcha =>
BadRequest(html.auth.signup(forms.signup.website)).fuccess
2020-05-05 22:11:15 -06:00
case Signup.Bad(err) =>
2020-07-17 02:39:51 -06:00
BadRequest(html.auth.signup(forms.signup.website withForm err)).fuccess
2020-05-05 22:11:15 -06:00
case Signup.ConfirmEmail(user, email) =>
fuccess {
Redirect(routes.Auth.checkYourEmail) withCookies
2020-05-05 22:11:15 -06:00
lila.security.EmailConfirm.cookie
.make(env.lilaCookie, user, email)(ctx.req)
}
case Signup.AllSet(user, email) =>
welcome(user, email, sendWelcomeEmail = true) >> redirectNewUser(user)
2020-05-05 22:11:15 -06:00
},
api = apiVersion =>
env.security.signup
.mobile(apiVersion)
.flatMap {
case Signup.RateLimited => limitedDefault.zero.fuccess
case Signup.MissingCaptcha => fuccess(BadRequest(jsonError("Missing captcha?!")))
case Signup.Bad(err) => jsonFormError(err)
case Signup.ConfirmEmail(_, _) => Ok(Json.obj("email_confirm" -> true)).fuccess
case Signup.AllSet(user, email) =>
welcome(user, email, sendWelcomeEmail = true) >> authenticateUser(user)
2020-05-05 22:11:15 -06:00
}
)
}
2016-06-20 09:31:53 -06:00
}
2015-06-25 08:54:26 -06:00
}
2015-02-28 07:16:39 -07:00
private def welcome(user: UserModel, email: EmailAddress, sendWelcomeEmail: Boolean)(implicit
ctx: Context
): Funit = {
2017-11-13 15:52:16 -07:00
garbageCollect(user, email)
if (sendWelcomeEmail) env.mailer.automaticEmail.welcomeEmail(user, email)
env.mailer.automaticEmail.welcomePM(user)
2019-12-04 16:39:16 -07:00
env.pref.api.saveNewUserPrefs(user, ctx.req)
2017-11-11 06:49:04 -07:00
}
2017-11-10 09:28:17 -07:00
2017-11-13 15:52:16 -07:00
private def garbageCollect(user: UserModel, email: EmailAddress)(implicit ctx: Context) =
2019-12-04 16:39:16 -07:00
env.security.garbageCollector.delay(user, email, ctx.req)
2017-11-13 15:52:16 -07:00
2020-05-05 22:11:15 -06:00
def checkYourEmail =
Open { implicit ctx =>
RedirectToProfileIfLoggedIn {
lila.security.EmailConfirm.cookie get ctx.req match {
case None => Ok(accountC.renderCheckYourEmail).fuccess
case Some(userEmail) =>
env.user.repo nameExists userEmail.username map {
case false => Redirect(routes.Auth.signup) withCookies env.lilaCookie.newSession(ctx.req)
2020-05-05 22:11:15 -06:00
case true => Ok(accountC.renderCheckYourEmail)
}
}
2019-12-30 14:28:28 -07:00
}
}
2015-02-05 07:35:58 -07:00
2018-03-29 15:46:29 -06:00
// after signup and before confirmation
2020-05-05 22:11:15 -06:00
def fixEmail =
OpenBody { implicit ctx =>
lila.security.EmailConfirm.cookie.get(ctx.req) ?? { userEmail =>
implicit val req = ctx.body
forms.preloadEmailDns >> forms
.fixEmail(userEmail.email)
2020-07-07 02:34:48 -06:00
.bindFromRequest()
2020-05-05 22:11:15 -06:00
.fold(
err => BadRequest(html.auth.checkYourEmail(userEmail.some, err.some)).fuccess,
email =>
env.user.repo.named(userEmail.username) flatMap {
_.fold(Redirect(routes.Auth.signup).fuccess) { user =>
2020-09-21 01:28:28 -06:00
env.user.repo.mustConfirmEmail(user.id) flatMap {
case false => Redirect(routes.Auth.login).fuccess
2020-09-21 01:28:28 -06:00
case _ =>
val newUserEmail = userEmail.copy(email = EmailAddress(email))
EmailConfirmRateLimit(newUserEmail, ctx.req) {
lila.mon.email.send.fix.increment()
env.user.repo.setEmail(user.id, newUserEmail.email) >>
env.security.emailConfirm.send(user, newUserEmail.email) inject {
Redirect(routes.Auth.checkYourEmail) withCookies
2020-05-05 22:11:15 -06:00
lila.security.EmailConfirm.cookie
.make(env.lilaCookie, user, newUserEmail.email)(ctx.req)
}
2020-09-21 01:28:28 -06:00
}(rateLimitedFu)
}
2020-05-05 22:11:15 -06:00
}
2019-12-13 07:30:20 -07:00
}
2020-05-05 22:11:15 -06:00
)
}
2018-03-29 15:46:29 -06:00
}
2020-05-05 22:11:15 -06:00
def signupConfirmEmail(token: String) =
Open { implicit ctx =>
import lila.security.EmailConfirm.Result
env.security.emailConfirm.confirm(token) flatMap {
case Result.NotFound =>
lila.mon.user.register.confirmEmailResult(false).increment()
notFound
case Result.AlreadyConfirmed(user) if ctx.is(user) =>
Redirect(routes.User.show(user.username)).fuccess
case Result.AlreadyConfirmed(_) =>
Redirect(routes.Auth.login).fuccess
2020-05-05 22:11:15 -06:00
case Result.JustConfirmed(user) =>
lila.mon.user.register.confirmEmailResult(true).increment()
env.user.repo.email(user.id).flatMap {
_.?? { email =>
authLog(user.username, email.value, s"Confirmed email ${email.value}")
welcome(user, email, sendWelcomeEmail = false)
2020-05-05 22:11:15 -06:00
}
} >> redirectNewUser(user)
}
2015-06-25 08:54:26 -06:00
}
2014-12-10 15:30:28 -07:00
2016-08-08 06:27:47 -06:00
private def redirectNewUser(user: UserModel)(implicit ctx: Context) = {
api.saveAuthentication(user.id, ctx.mobileApiVersion) flatMap { sessionId =>
negotiate(
html = Redirect(routes.User.show(user.username)).fuccess,
api = _ => mobileUserOk(user, sessionId)
2018-03-29 16:09:46 -06:00
) map authenticateCookie(sessionId)
2015-12-13 23:51:14 -07:00
} recoverWith authRecovery
}
2020-05-05 22:11:15 -06:00
def setFingerPrint(fp: String, ms: Int) =
Auth { ctx => me =>
lila.mon.http.fingerPrint.record(ms)
api.setFingerPrint(ctx.req, FingerPrint(fp)) flatMap {
_ ?? { hash =>
!me.lame ?? (for {
otherIds <- api.recentUserIdsByFingerHash(hash).map(_.filter(me.id.!=))
_ <- (otherIds.sizeIs >= 2) ?? env.user.repo.countLameOrTroll(otherIds).flatMap {
case nb if nb >= 2 && nb >= otherIds.size / 2 => env.report.api.autoAltPrintReport(me.id)
2020-05-05 22:11:15 -06:00
case _ => funit
}
} yield ())
}
} inject NoContent
}
2015-08-11 07:39:22 -06:00
2020-07-17 02:39:51 -06:00
private def renderPasswordReset(form: Option[play.api.data.Form[PasswordReset]], fail: Boolean)(implicit
ctx: Context
) =
html.auth.bits.passwordReset(form.foldLeft(env.security.forms.passwordReset)(_ withForm _), fail)
2020-05-05 22:11:15 -06:00
def passwordReset =
Open { implicit ctx =>
2020-08-16 06:42:29 -06:00
Ok(renderPasswordReset(none, fail = false)).fuccess
2014-12-10 15:30:28 -07:00
}
2020-05-05 22:11:15 -06:00
def passwordResetApply =
OpenBody { implicit ctx =>
implicit val req = ctx.body
env.security.hcaptcha.verify() flatMap { captcha =>
if (captcha.ok)
forms.passwordReset.form
.bindFromRequest()
.fold(
err => BadRequest(renderPasswordReset(err.some, fail = true)).fuccess,
data =>
env.user.repo.enabledWithEmail(data.realEmail.normalize) flatMap {
case Some((user, storedEmail)) =>
lila.mon.user.auth.passwordResetRequest("success").increment()
env.security.passwordReset.send(user, storedEmail) inject Redirect(
routes.Auth.passwordResetSent(storedEmail.conceal)
)
case _ =>
lila.mon.user.auth.passwordResetRequest("noEmail").increment()
Redirect(routes.Auth.passwordResetSent(data.realEmail.conceal)).fuccess
}
)
else BadRequest(renderPasswordReset(none, fail = true)).fuccess
}
2014-12-14 17:32:18 -07:00
}
2020-05-05 22:11:15 -06:00
def passwordResetSent(email: String) =
Open { implicit ctx =>
fuccess {
Ok(html.auth.bits.passwordResetSent(email))
}
2014-12-10 17:47:50 -07:00
}
2020-05-05 22:11:15 -06:00
def passwordResetConfirm(token: String) =
Open { implicit ctx =>
env.security.passwordReset confirm token flatMap {
2020-08-16 06:48:46 -06:00
case None =>
2020-05-05 22:11:15 -06:00
lila.mon.user.auth.passwordResetConfirm("tokenFail").increment()
notFound
2020-08-16 06:48:46 -06:00
case Some(user) =>
2020-05-05 22:11:15 -06:00
authLog(user.username, "-", "Reset password")
lila.mon.user.auth.passwordResetConfirm("tokenOk").increment()
fuccess(html.auth.bits.passwordResetConfirm(user, token, forms.passwdReset, none))
}
2020-05-05 22:11:15 -06:00
}
def passwordResetConfirmApply(token: String) =
OpenBody { implicit ctx =>
env.security.passwordReset confirm token flatMap {
2020-08-16 06:48:46 -06:00
case None =>
2020-05-05 22:11:15 -06:00
lila.mon.user.auth.passwordResetConfirm("tokenPostFail").increment()
notFound
case Some(user) =>
implicit val req = ctx.body
FormFuResult(forms.passwdReset) { err =>
fuccess(html.auth.bits.passwordResetConfirm(user, token, err, false.some))
} { data =>
HasherRateLimit(user.username, ctx.req) { _ =>
env.user.authenticator.setPassword(user.id, ClearPassword(data.newPasswd1)) >>
env.user.repo.setEmailConfirmed(user.id).flatMap {
_ ?? { welcome(user, _, sendWelcomeEmail = false) }
2020-05-05 22:11:15 -06:00
} >>
env.user.repo.disableTwoFactor(user.id) >>
env.security.store.closeAllSessionsOf(user.id) >>
2020-05-05 22:11:15 -06:00
env.push.webSubscriptionApi.unsubscribeByUser(user) >>
authenticateUser(user) >>-
lila.mon.user.auth.passwordResetConfirm("success").increment().unit
}(rateLimitedFu)
2020-05-05 22:11:15 -06:00
}
}
2014-12-10 16:36:14 -07:00
}
2020-07-17 02:39:51 -06:00
private def renderMagicLink(form: Option[play.api.data.Form[MagicLink]], fail: Boolean)(implicit
ctx: Context
) =
html.auth.bits.magicLink(form.foldLeft(env.security.forms.magicLink)(_ withForm _), fail)
2020-05-05 22:11:15 -06:00
def magicLink =
Open { implicit ctx =>
Firewall {
Ok(renderMagicLink(none, fail = false)).fuccess
}
}
2020-05-05 22:11:15 -06:00
def magicLinkApply =
OpenBody { implicit ctx =>
Firewall {
implicit val req = ctx.body
env.security.hcaptcha.verify() flatMap { captcha =>
if (captcha.ok)
forms.magicLink.form
.bindFromRequest()
.fold(
err => BadRequest(renderMagicLink(err.some, fail = true)).fuccess,
data =>
env.user.repo.enabledWithEmail(data.realEmail.normalize)
flatMap {
case Some((user, storedEmail)) =>
MagicLinkRateLimit(user, storedEmail, ctx.req) {
lila.mon.user.auth.magicLinkRequest("success").increment()
env.security.magicLink.send(user, storedEmail) inject Redirect(
routes.Auth.magicLinkSent(storedEmail.value)
)
}(rateLimitedFu)
case _ =>
lila.mon.user.auth.magicLinkRequest("no_email").increment()
Redirect(routes.Auth.magicLinkSent(data.realEmail.value)).fuccess
}
)
else
BadRequest(renderMagicLink(none, fail = true)).fuccess
}
}
}
2020-05-05 22:11:15 -06:00
def magicLinkSent(@nowarn("cat=unused") email: String) =
Open { implicit ctx =>
fuccess {
Ok(html.auth.bits.magicLinkSent)
}
2020-05-05 22:11:15 -06:00
}
private lazy val magicLinkLoginRateLimitPerToken = new RateLimit[String](
credits = 3,
duration = 1 hour,
key = "login.magicLink.token"
)
2020-05-05 22:11:15 -06:00
def magicLinkLogin(token: String) =
Open { implicit ctx =>
Firewall {
magicLinkLoginRateLimitPerToken(token) {
env.security.magicLink confirm token flatMap {
case None =>
lila.mon.user.auth.magicLinkConfirm("token_fail").increment()
notFound
case Some(user) =>
authLog(user.username, "-", "Magic link")
authenticateUser(user) >>-
lila.mon.user.auth.magicLinkConfirm("success").increment().unit
}
}(rateLimitedFu)
}
}
2021-01-14 07:50:52 -07:00
private def loginTokenFor(me: UserModel) = JsonOk {
env.security.loginToken generate me map { token =>
Json.obj(
"userId" -> me.id,
"url" -> s"${env.net.baseUrl}${routes.Auth.loginWithToken(token).url}"
)
}
2021-01-14 07:50:52 -07:00
}
def makeLoginToken =
AuthOrScoped(_.Web.Login)(_ => loginTokenFor, _ => loginTokenFor)
2020-05-05 22:11:15 -06:00
def loginWithToken(token: String) =
Open { implicit ctx =>
if (ctx.isAuth) Redirect(getReferrer).fuccess
else
Firewall {
consumingToken(token) { user =>
env.security.loginToken.generate(user) map { newToken =>
Ok(html.auth.bits.tokenLoginConfirmation(user, newToken, get("referrer")))
}
}
2020-05-05 22:11:15 -06:00
}
}
def loginWithTokenPost(token: String, referrer: Option[String]) =
Open { implicit ctx =>
if (ctx.isAuth) Redirect(getReferrer).fuccess
else
Firewall {
consumingToken(token) { authenticateUser(_) }
}
}
private def consumingToken(token: String)(f: UserModel => Fu[Result])(implicit ctx: Context) =
env.security.loginToken consume token flatMap {
case None =>
BadRequest {
import scalatags.Text.all.stringFrag
html.site.message("This token has expired.")(stringFrag("Please go back and try again."))
}.fuccess
case Some(user) => f(user)
}
2017-09-27 18:23:16 -06:00
implicit private val limitedDefault = Zero.instance[Result](rateLimited)
2017-09-27 18:23:16 -06:00
2018-12-16 09:19:37 -07:00
private[controllers] def HasherRateLimit =
2019-12-04 16:39:16 -07:00
PasswordHasher.rateLimit[Result](enforce = env.net.rateLimit) _
2018-03-29 15:46:29 -06:00
private[controllers] def EmailConfirmRateLimit = lila.security.EmailConfirm.rateLimit[Result] _
2019-11-10 12:36:41 -07:00
private[controllers] def MagicLinkRateLimit = lila.security.MagicLink.rateLimit[Result] _
2019-12-30 14:28:28 -07:00
private[controllers] def RedirectToProfileIfLoggedIn(f: => Fu[Result])(implicit ctx: Context): Fu[Result] =
ctx.me match {
case Some(me) => Redirect(routes.User.show(me.username)).fuccess
case None => f
}
2013-03-14 16:22:34 -06:00
}