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
parent
83bb43e714
commit
09c86f69dd
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue