OAuth personal access tokens WIP
This commit is contained in:
parent
2a4a89ac95
commit
5d3c4d5b56
66
modules/oauth/src/main/AccessToken.scala
Normal file
66
modules/oauth/src/main/AccessToken.scala
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
22
modules/oauth/src/main/OAuthScope.scala
Normal file
22
modules/oauth/src/main/OAuthScope.scala
Normal 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
|
||||
}
|
|
@ -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 {
|
||||
|
|
21
modules/oauth/src/main/PersonalTokenApi.scala
Normal file
21
modules/oauth/src/main/PersonalTokenApi.scala
Normal 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)
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue