OAuth personal access tokens WIP

This commit is contained in:
Thibault Duplessis 2018-03-09 11:28:18 -05:00
parent 2a4a89ac95
commit 5d3c4d5b56
5 changed files with 120 additions and 12 deletions

View file

@ -0,0 +1,66 @@
package lila.oauth
import org.joda.time.DateTime
import lila.user.User
case class AccessToken(
id: AccessToken.Id,
clientId: String,
userId: User.ID,
expiresAt: DateTime,
createdAt: Option[DateTime] = None, // for personal access tokens
description: Option[String] = None, // for personal access tokens
usedAt: Option[DateTime] = None,
scopes: List[OAuthScope]
)
object AccessToken {
case class Id(value: String) extends AnyVal
object BSONFields {
val id = "access_token_id"
val clientId = "client_id"
val userId = "user_id"
val createdAt = "create_date"
val expiresAt = "expire_date"
val usedAt = "used_at"
val scopes = "scopes"
}
import reactivemongo.bson._
import lila.db.BSON
import lila.db.dsl._
import BSON.BSONJodaDateTimeHandler
private[oauth] implicit val accessTokenIdHandler = stringAnyValHandler[Id](_.value, Id.apply)
private[oauth] implicit val scopeHandler = new BSONHandler[BSONString, OAuthScope] {
def read(b: BSONString): OAuthScope = OAuthScope.byKey.get(b.value) err s"No such scope: ${b.value}"
def write(s: OAuthScope) = BSONString(s.key)
}
implicit val AccessTokenBSONHandler = new BSON[AccessToken] {
import BSONFields._
def reads(r: BSON.Reader): AccessToken = AccessToken(
id = r.get[Id](id),
clientId = r str clientId,
userId = r str userId,
expiresAt = r.get[DateTime](expiresAt),
usedAt = r.getO[DateTime](usedAt),
scopes = r.get[List[OAuthScope]](scopes)
)
def writes(w: BSON.Writer, o: AccessToken) = BSONDocument(
id -> o.id,
clientId -> o.clientId,
userId -> o.userId,
expiresAt -> o.expiresAt,
usedAt -> o.usedAt,
scopes -> o.scopes
)
}
}

View file

@ -0,0 +1,22 @@
package lila.oauth
sealed abstract class OAuthScope(val key: String, val name: String)
object OAuthScope {
object Game {
case object Read extends OAuthScope("game:read", "Download all games")
}
object Preference {
case object Read extends OAuthScope("preference:read", "Read preferences")
case object Write extends OAuthScope("preference:write", "Write preferences")
}
val all = List(
Game.Read,
Preference.Read, Preference.Write
)
val byKey: Map[String, OAuthScope] = all.map { s => s.key -> s } toMap
}

View file

@ -5,7 +5,6 @@ import pdi.jwt.{ Jwt, JwtAlgorithm }
import play.api.libs.json.Json
import play.api.mvc.RequestHeader
import lila.common.Iso
import lila.db.dsl._
import lila.user.{ User, UserRepo }
@ -15,7 +14,8 @@ final class OAuthServer(
jwtPublicKey: JWT.PublicKey
) {
private implicit val tokenIdHandler = stringAnyValHandler[AccessTokenId](_.value, AccessTokenId.apply)
import AccessToken.accessTokenIdHandler
import AccessToken.{ BSONFields => F }
def activeUser(req: RequestHeader): Fu[Option[User]] = {
req.headers get "Authorization" map (_.split(" ", 2))
@ -23,27 +23,27 @@ final class OAuthServer(
case Array("Bearer", token) => for {
jsonStr <- Jwt.decodeRaw(token, jwtPublicKey.value, Seq(JwtAlgorithm.RS256)).future
json = Json.parse(jsonStr)
accessToken = AccessTokenId((json str "jti" err s"Bad token json $json"))
user <- activeUser(accessToken)
accessTokenId = AccessToken.Id((json str "jti" err s"Bad token json $json"))
user <- activeUser(accessTokenId)
} yield Some(user err "No user found for access token")
case _ => fuccess(none)
} mapFailure { e =>
new OauthException { val message = e.getMessage }
}
def activeUser(token: AccessTokenId): Fu[Option[User]] =
def activeUser(tokenId: AccessToken.Id): Fu[Option[User]] =
tokenColl.primitiveOne[User.ID]($doc(
"access_token_id" -> token,
"expire_date" $gt DateTime.now
), "user_id") flatMap {
F.id -> tokenId,
F.expiresAt $gt DateTime.now
), F.userId) flatMap {
_ ?? { userId =>
setUsedNow(token)
setUsedNow(tokenId)
UserRepo byId userId
}
}
private def setUsedNow(token: AccessTokenId): Unit =
tokenColl.updateFieldUnchecked($doc("access_token_id" -> token), "used_at", DateTime.now)
private def setUsedNow(tokenId: AccessToken.Id): Unit =
tokenColl.updateFieldUnchecked($doc(F.id -> tokenId), F.usedAt, DateTime.now)
}
object OAuthServer {

View file

@ -0,0 +1,21 @@
package lila.oauth
import org.joda.time.DateTime
import lila.user.User
import lila.db.dsl._
final class PersonalTokenApi(
tokenColl: Coll
) {
import AccessToken.{ BSONFields => F, _ }
private val clientId = "lichess_personal_token"
def list(u: User): Fu[List[AccessToken]] =
tokenColl.find($doc(
F.userId -> u.id,
F.clientId -> clientId
)).sort($sort desc F.createdAt).list[AccessToken](100)
}

View file

@ -1,7 +1,6 @@
package lila.oauth
case class AccessTokenJWT(value: String) extends AnyVal
case class AccessTokenId(value: String) extends AnyVal
object JWT {
case class PublicKey(value: String) extends AnyVal