token login API

As a registered user:

```
POST /auth/token
{"userId":"thibault","url":"https://lichess.org/auth/token/dGhpYmF1bHR8MTQ4MTYyODQyMjAxN3xlNDIzYTFhMTdjNjkwOQ=="}
```

Opening the URL in a browser signs you in as thibault.
The URL is valid for one minute only.
pull/2467/head
Thibault Duplessis 2016-12-13 12:29:48 +01:00
parent 83bb43e714
commit 09c86f69dd
6 changed files with 82 additions and 0 deletions

View File

@ -243,4 +243,23 @@ object Auth extends LilaController {
case _ => notFound
}
}
def makeLoginToken = Auth { implicit ctx => me =>
JsonOk {
val baseUrl = Env.api.Net.BaseUrl
val url = routes.Auth.loginWithToken(env.loginToken generate me).url
fuccess(Json.obj(
"userId" -> me.id,
"url" -> s"$baseUrl$url"
))
}
}
def loginWithToken(token: String) = Open { implicit ctx =>
Firewall {
env.loginToken consume token flatMap {
_.fold(notFound)(authenticateUser)
}
}
}
}

View File

@ -255,6 +255,9 @@ security {
mailgun = ${mailgun}
secret = "???"
}
login_token {
secret = "???"
}
tor {
provider_url = "https://check.torproject.org/cgi-bin/TorBulkExitList.py?ip="${net.ip}"&port=80"
refresh_delay = 1 hour

View File

@ -326,6 +326,8 @@ GET /password/reset/sent/:email controllers.Auth.passwordResetSent(email:
GET /password/reset/confirm/:token controllers.Auth.passwordResetConfirm(token: String)
POST /password/reset/confirm/:token controllers.Auth.passwordResetConfirmApply(token: String)
POST /set-fingerprint/:hash/:ms controllers.Auth.setFingerprint(hash: String, ms: Int)
POST /auth/token controllers.Auth.makeLoginToken
GET /auth/token/:token controllers.Auth.loginWithToken(token: String)
# Mod
POST /mod/:username/engine controllers.Mod.engine(username: String)

View File

@ -69,6 +69,13 @@ trait PackageObject extends Steroids with WithFuture {
case e: NumberFormatException => None
}
def parseLongOption(str: String): Option[Long] = try {
Some(java.lang.Long.parseLong(str))
}
catch {
case e: NumberFormatException => None
}
def intBox(in: Range.Inclusive)(v: Int): Int =
math.max(in.start, math.min(v, in.end))

View File

@ -38,6 +38,7 @@ final class Env(
val PasswordResetMailgunSender = config getString "password_reset.mailgun.sender"
val PasswordResetMailgunBaseUrl = config getString "password_reset.mailgun.base_url"
val PasswordResetSecret = config getString "password_reset.secret"
val LoginTokenSecret = config getString "login_token.secret"
val TorProviderUrl = config getString "tor.provider_url"
val TorRefreshDelay = config duration "tor.refresh_delay"
val DisposableEmailProviderUrl = config getString "disposable_email.provider_url"
@ -97,6 +98,9 @@ final class Env(
baseUrl = PasswordResetMailgunBaseUrl,
secret = PasswordResetSecret)
lazy val loginToken = new LoginToken(
secret = LoginTokenSecret)
lazy val emailAddress = new EmailAddress(disposableEmailDomain)
private lazy val disposableEmailDomain = new DisposableEmailDomain(

View File

@ -0,0 +1,47 @@
package lila.security
import com.roundeights.hasher.{ Hasher, Algo }
import org.joda.time.DateTime
import lila.common.String.base64
import lila.user.{ User, UserRepo }
final class LoginToken(secret: String) {
def generate(user: User): String = tokener make user
def consume(token: String): Fu[Option[User]] = tokener read token
private object tokener {
private val separator = '|'
private def makeHash(msg: String) = Algo.hmac(secret).sha1(msg).hex take 14
private def getPasswd(userId: User.ID) = UserRepo getPasswordHash userId map { p =>
makeHash(~p) take 6
}
private def makePayload(userId: String, milliStr: String) = s"$userId$separator$milliStr"
private object DateStr {
def toStr(date: DateTime) = date.getMillis.toString
def toDate(str: String) = parseLongOption(str) map { new DateTime(_) }
}
def make(user: User) = {
val payload = makePayload(user.id, DateStr.toStr(DateTime.now))
val hash = makeHash(payload)
val token = s"$payload$separator$hash"
base64 encode token
}
def read(token: String): Fu[Option[User]] = (base64 decode token) ?? {
_ split separator match {
case Array(userId, milliStr, hash) if makeHash(makePayload(userId, milliStr)) == hash =>
DateStr.toDate(milliStr).exists(DateTime.now.minusMinutes(1).isBefore) ?? {
UserRepo enabledById userId
}
case _ => fuccess(none)
}
}
}
}