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 @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]) = { 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] = { cred.fold[Future[Boolean]](Future.failed(new UnauthorizedClient("username required"))) { (cc) => requ.grantType match { case OAuthGrantType.PASSWORD => cc.clientSecret.fold[Future[Boolean]](Future.failed(new UnauthorizedClient("password required"))) { (sec) => validateUsernamePassword(cc.clientId, sec).map(_.isDefined) } case OAuthGrantType.REFRESH_TOKEN => validateUsernameRefresh( cc.clientId, Authorization.decodeToken(RefreshTokenRequest(requ).refreshToken) ) case OAuthGrantType.AUTHORIZATION_CODE => Future.failed(new UnsupportedGrantType()) case OAuthGrantType.CLIENT_CREDENTIALS => Future.failed(new UnsupportedGrantType()) case OAuthGrantType.IMPLICIT => Future.failed(new UnsupportedGrantType()) } } } def validateUsernamePassword( username: String, pass: String ): Future[Option[User]] = withCollection(User) { (users) => users.find(withDevModeCond(equal("email", username))) .first() .toFutureOption() // TODO: Replace bcrypt with something that doesn't use String objects. .filter { case Some(user) => pass.isBcryptedSafeBounded(user.password) == Success(true) case None => false } }.flatten private def withDevModeCond(bson: Bson) = if (devMode) bson else and(bson, not(equal("devOnly", true))) 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) => cred.fold[Future[Option[User]]](Future.failed(new UnsupportedGrantType("client_id required"))) { (cc) => users.find(withDevModeCond(equal("email", cc.clientId))).first().toFutureOption() } }.flatten /* 2020-07-25: Never re-issue the same authorization token. Always generate a * new one. */ override def getStoredAccessToken(auth: AuthInfo[User]) = { Future.successful(None) } override def findAccessToken(token: String): Future[Option[AccessToken]] = withCollection(Authorization) { (auths) => auths.find( freshAccessToken(Authorization.decodeToken(token)) ).first().toFutureOption() .map(_.map(_.toToken())) }.flatten override def findAuthInfoByAccessToken(token: AccessToken) = withCollection(Authorization) { (auths) => auths.find(freshAccessToken(Authorization.decodeToken(token.token))) .first() .toFutureOption() .map(_.map(auth => AuthInfo(auth, Some(auth.email), None, None))) }.flatten private def freshAccessToken(token: Array[Byte]) = and( gt("created", Instant.now() - Authorization.accessFreshTime), equal("accessToken", token) ) implicit def optFut2FutOpt[A]( x: Option[Future[A]] )(implicit ec: ExecutionContext): Future[Option[A]] = x.fold[Future[Option[A]]](Future.successful(None)) { _.map(Some(_)) } override def findAuthInfoByRefreshToken(token: String) = withCollection(Authorization) { (auths) => auths.find(freshRefreshToken(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 private def freshRefreshToken(token: Array[Byte]) = and( gt("created", Instant.now() - Authorization.refreshFreshTime), equal("refreshToken", token) ) 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)), gt("created", Instant.now() - Authorization.refreshFreshTime), ), 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] = Future.failed(new UnsupportedGrantType("Code grant authorizations are not supported.")) override def findAuthInfoByCode( code: String ): Future[Option[AuthInfo[User]]] = Future.failed(new UnsupportedGrantType("Code grant authorizations are not supported.")) }