package com.weEat.services import play.api.Configuration import codes.reactive.scalatime._ import com.github.t3hnar.bcrypt._ import com.weEat.models.{User,Authorization} import java.time.Instant import javax.inject.{Inject,Singleton} import scala.concurrent.{ExecutionContext,Future} import scala.util.Success import scalaoauth2.provider._ import org.mongodb.scala.model.Filters._ import org.mongodb.scala.bson.DefaultBsonTransformers import org.mongodb.scala.bson.conversions.Bson import scala.language.implicitConversions // TOOD: Migrate to SecureSocial OAuth lib @Singleton class OAuth2Service @Inject()(db: MongoDBService, config: Configuration) extends AuthorizationHandler[User] with ProtectedResourceHandler[Authorization] with DefaultBsonTransformers { implicit val ec: ExecutionContext = ExecutionContext.global import db.withCollection override def createAccessToken(auth: AuthInfo[User]) = { println("Create token") val newAuth = Authorization(auth.user) withCollection(Authorization) {auths => auths.insertOne(newAuth).head().map(_ => newAuth.toToken()) }.flatten } val devMode = config.getOptional[Boolean](s"oauth.dev").getOrElse(false) override def validateClient( cred: Option[ClientCredential], requ: AuthorizationRequest ): Future[Boolean] = { val cc = cred.getOrElse(throw new UnauthorizedClient("username required")) requ.grantType match { case OAuthGrantType.PASSWORD => validateUsernamePassword( cc.clientId, cc.clientSecret.getOrElse(throw new AccessDenied("password required")) ) case OAuthGrantType.REFRESH_TOKEN => validateUsernameRefresh( cc.clientId, Authorization.decodeToken(RefreshTokenRequest(requ).refreshToken) ) case OAuthGrantType.AUTHORIZATION_CODE => throw new UnsupportedGrantType() case OAuthGrantType.CLIENT_CREDENTIALS => throw new UnsupportedGrantType() case OAuthGrantType.IMPLICIT => throw new UnsupportedGrantType() } } private def withDevModeCond(bson: Bson) = if (devMode) bson else and(bson, not(equal("devOnly", true))) private def validateUsernamePassword( username: String, pass: String ): Future[Boolean] = withCollection(User) {users => users.find(withDevModeCond(equal("email", username))) .first() .toFutureOption() .map(res => res.map { x => pass.isBcryptedSafeBounded(x.password)} .getOrElse(Success(false)) ).transform ({ _.flatten }) }.flatten private def validateUsernameRefresh(username: String, refresh: Array[Byte]) = withCollection(Authorization) {auths => auths.find(and( equal("email", username), equal("refreshToken", refresh) )).first().toFutureOption().map { _.nonEmpty } }.flatten override def findUser( cred: Option[ClientCredential], requ: AuthorizationRequest ): Future[Option[User]] = withCollection(User) {users => users.find(withDevModeCond(equal("email", cred.getOrElse( throw new UnsupportedGrantType("client_id required") ).clientId))).first().toFutureOption() }.flatten.map({x => {println(s"findUser: $x"); x}}) /* 2020-07-25: Never re-issue the same authorization token. Always generate a * new one. */ override def getStoredAccessToken(auth: AuthInfo[User]) = { Future.successful(None) } private def freshAccessToken(token: Array[Byte]) = and( gt("created", Instant.now() - Authorization.accessFreshTime), equal("accessToken", token) ) override def findAccessToken(token: String): Future[Option[AccessToken]] = withCollection(Authorization) {auths => auths.find( freshAccessToken(Authorization.decodeToken(token)) ).first().toFutureOption() .map(_.map(_.toToken())) }.flatten.map({x => {println(s"findAccessToken: $x"); x}}) override def findAuthInfoByAccessToken(token: AccessToken) = withCollection(Authorization) {collection => collection.find(freshAccessToken(Authorization.decodeToken(token.token))) .first() .toFutureOption() .map(_.map(auth => AuthInfo(auth, Some(auth.email), None, None))) }.flatten implicit def optFut2FutOpt[A]( x: Option[Future[A]] )(implicit ec: ExecutionContext): Future[Option[A]] = x match { case Some(f) => f.map(Some(_)) case None => Future.successful(None) } override def findAuthInfoByRefreshToken(token: String) = withCollection(Authorization) {auths => auths.find(equal("refreshToken", Authorization.decodeToken(token))) .first() .toFutureOption() }.flatten .map(authOpt => withCollection(User) {users => optFut2FutOpt(authOpt.map({ auth => users.find( withDevModeCond(equal("_id", auth.userId)) ).first() .toFuture() .map({user => AuthInfo(user, Some(user.email), None, None)}) })) }.flatten).flatten override def refreshAccessToken( auth: AuthInfo[User], refreshToken: String ): Future[AccessToken] = { val newAuth = Authorization(auth.user) withCollection(Authorization) {auths => auths.replaceOne( and( equal("userId", auth.user._id), equal("refreshToken", Authorization.decodeToken(refreshToken)) ), newAuth ).head().map(_ => newAuth.toToken()) }.flatten } def revokeAccessToken( auth: AuthInfo[Authorization], user: String, refresh: String ): Future[Unit] = if (user != auth.user.email) Future.failed( new InvalidClient("Invalid client or client is not authorized") ) else withCollection(Authorization) {auths => auths.deleteOne(and( equal("userId", auth.user.userId), equal("refreshToken", Authorization.decodeToken(refresh)) )).head().map { _ => {} } }.flatten override def deleteAuthCode(token: String): Future[Unit] = throw new UnsupportedGrantType("Code grant authorizations are not supported.") override def findAuthInfoByCode( code: String ): Future[Option[AuthInfo[User]]] = throw new UnsupportedGrantType("Code grant authorizations are not supported.") }