From 5d3c4d5b566625591deeae60ee03e970894302b3 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 9 Mar 2018 11:28:18 -0500 Subject: [PATCH] OAuth personal access tokens WIP --- modules/oauth/src/main/AccessToken.scala | 66 +++++++++++++++++++ modules/oauth/src/main/OAuthScope.scala | 22 +++++++ modules/oauth/src/main/OAuthServer.scala | 22 +++---- modules/oauth/src/main/PersonalTokenApi.scala | 21 ++++++ modules/oauth/src/main/model.scala | 1 - 5 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 modules/oauth/src/main/AccessToken.scala create mode 100644 modules/oauth/src/main/OAuthScope.scala create mode 100644 modules/oauth/src/main/PersonalTokenApi.scala diff --git a/modules/oauth/src/main/AccessToken.scala b/modules/oauth/src/main/AccessToken.scala new file mode 100644 index 0000000000..b6792de954 --- /dev/null +++ b/modules/oauth/src/main/AccessToken.scala @@ -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 + ) + } +} diff --git a/modules/oauth/src/main/OAuthScope.scala b/modules/oauth/src/main/OAuthScope.scala new file mode 100644 index 0000000000..d651ee1f91 --- /dev/null +++ b/modules/oauth/src/main/OAuthScope.scala @@ -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 +} diff --git a/modules/oauth/src/main/OAuthServer.scala b/modules/oauth/src/main/OAuthServer.scala index 12055bf0ca..99a7628709 100644 --- a/modules/oauth/src/main/OAuthServer.scala +++ b/modules/oauth/src/main/OAuthServer.scala @@ -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 { diff --git a/modules/oauth/src/main/PersonalTokenApi.scala b/modules/oauth/src/main/PersonalTokenApi.scala new file mode 100644 index 0000000000..90be824937 --- /dev/null +++ b/modules/oauth/src/main/PersonalTokenApi.scala @@ -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) +} diff --git a/modules/oauth/src/main/model.scala b/modules/oauth/src/main/model.scala index 527c647577..04dfcabd38 100644 --- a/modules/oauth/src/main/model.scala +++ b/modules/oauth/src/main/model.scala @@ -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