All Downloads are FREE. Search and download functionalities are using the official Maven repository.

models.privateappsuser.scala Maven / Gradle / Ivy

package otoroshi.models

import java.util.concurrent.TimeUnit
import akka.http.scaladsl.util.FastFuture
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import otoroshi.auth.{AuthModuleConfig, GenericOauth2Module, ValidableUser}
import otoroshi.env.Env
import org.joda.time.DateTime
import play.api.Logger
import play.api.libs.json._
import play.api.mvc.Results.InternalServerError
import play.api.mvc.{RequestHeader, Result, Results}
import otoroshi.storage.BasicStore
import otoroshi.utils.json.JsonImplicits._
import otoroshi.cluster._
import otoroshi.next.models.NgRoute
import otoroshi.next.plugins.{MultiAuthModule, NgMultiAuthModuleConfig}
import otoroshi.utils.TypedMap
import otoroshi.utils.syntax.implicits.BetterSyntax

import java.nio.charset.StandardCharsets
import java.util.Base64
import scala.concurrent.duration._
import scala.concurrent.duration.Duration
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Success, Try}

case class PrivateAppsUser(
    randomId: String,
    name: String,
    email: String,
    profile: JsValue,
    token: JsValue = Json.obj(),
    realm: String,
    authConfigId: String,
    otoroshiData: Option[JsValue],
    createdAt: DateTime = DateTime.now(),
    expiredAt: DateTime = DateTime.now(),
    lastRefresh: DateTime = DateTime.now(),
    tags: Seq[String],
    metadata: Map[String, String],
    location: otoroshi.models.EntityLocation
) extends RefreshableUser
    with ValidableUser
    with otoroshi.models.EntityLocationSupport {

  def theDescription: String           = name
  def theMetadata: Map[String, String] = metadata
  def theName: String                  = name
  def theTags: Seq[String]             = tags

  def picture: Option[String]             = (profile \ "picture").asOpt[String]
  def field(name: String): Option[String] = (profile \ name).asOpt[String]
  def userId: Option[String]              = (profile \ "user_id").asOpt[String].orElse((profile \ "sub").asOpt[String])

  def save(duration: Duration)(implicit ec: ExecutionContext, env: Env): Future[PrivateAppsUser] =
    env.datastores.privateAppsUserDataStore
      .set(this.copy(expiredAt = DateTime.now().plus(duration.toMillis)), Some(duration))
      .map(_ => this.copy(expiredAt = DateTime.now().plus(duration.toMillis)))

  def delete()(implicit ec: ExecutionContext, env: Env): Future[Boolean] =
    env.datastores.privateAppsUserDataStore.delete(randomId)

  def toJson: JsValue        = PrivateAppsUser.fmt.writes(this)
  def lightJson: JsValue     = asJsonCleaned
  def asJsonCleaned: JsValue =
    Json.obj(
      "name"     -> name,
      "email"    -> email,
      "profile"  -> profile,
      "metadata" -> otoroshiData,
      "tags"     -> tags
    )

  def withAuthModuleConfig[A](f: AuthModuleConfig => A)(implicit ec: ExecutionContext, env: Env): Unit = {
    // env.datastores.authConfigsDataStore.findById(authConfigId).map {
    env.proxyState.authModuleAsync(authConfigId).map {
      case None       => ()
      case Some(auth) => f(auth)
    }
  }
  override def updateToken(tok: JsValue)(implicit ec: ExecutionContext, env: Env): Future[Boolean] = {
    env.datastores.privateAppsUserDataStore.set(
      copy(
        token = tok,
        lastRefresh = DateTime.now()
      ),
      Some((expiredAt.toDate.getTime - System.currentTimeMillis()).millis)
    )
  }

  override def internalId: String = randomId
  override def json: JsValue      = PrivateAppsUser.fmt.writes(this)
}

object PrivateAppsUser {

  def fromCookie(sessionId: String, reqOpt: Option[RequestHeader])(implicit env: Env): Option[PrivateAppsUser] = {
    reqOpt
      .map { req =>
        val verifier =
          JWT.require(Algorithm.HMAC512(env.otoroshiSecret)).withIssuer("otoroshi").acceptLeeway(10).build()
        req.cookies
          .filter(_.name.startsWith("oto-papps-tsess-"))
          .map { cookie =>
            Try(verifier.verify(cookie.value))
          }
          .collect { case Success(decodedToken) =>
            decodedToken
          }
          .find { token =>
            Try(token.getClaim("sessid").asString()).toOption.contains(sessionId)
          }
          .flatMap { token =>
            Try(token.getClaim("sess").asString()).toOption
          }
          .flatMap { encryptedSession =>
            Try(env.aesDecrypt(encryptedSession)).toOption
          }
          .flatMap { decryptedSession =>
            PrivateAppsUser.fmt.reads(Json.parse(decryptedSession)).asOpt
          }
      }
      .collectFirst { case Some(session) =>
        session
      }
  }

  def select(from: JsValue, selector: String): JsValue = {
    val parts = selector.split("\\|")
    parts.foldLeft(from) { (o, rawPath) =>
      val path = rawPath.trim
      (o \ path).asOpt[JsValue].getOrElse(JsNull)
    }
  }

  val fmt = new Format[PrivateAppsUser] {
    override def reads(json: JsValue) =
      Try {
        JsSuccess(
          PrivateAppsUser(
            randomId = (json \ "randomId").as[String],
            name = (json \ "name").as[String],
            email = (json \ "email").as[String],
            authConfigId = (json \ "authConfigId").asOpt[String].getOrElse("none"),
            profile = (json \ "profile").as[JsValue],
            token = (json \ "token").asOpt[JsValue].getOrElse(Json.obj()),
            realm = (json \ "realm").asOpt[String].getOrElse("none"),
            otoroshiData = (json \ "otoroshiData").asOpt[JsValue],
            createdAt = new DateTime((json \ "createdAt").as[Long]),
            expiredAt = new DateTime((json \ "expiredAt").as[Long]),
            lastRefresh = new DateTime((json \ "lastRefresh").as[Long]),
            metadata = (json \ "metadata").asOpt[Map[String, String]].getOrElse(Map.empty),
            tags = (json \ "tags").asOpt[Seq[String]].getOrElse(Seq.empty[String]),
            location = otoroshi.models.EntityLocation.readFromKey(json)
          )
        )
      } recover { case e =>
        JsError(e.getMessage)
      } get

    override def writes(o: PrivateAppsUser) =
      o.location.jsonWithKey ++ Json.obj(
        "randomId"     -> o.randomId,
        "name"         -> o.name,
        "email"        -> o.email,
        "authConfigId" -> o.authConfigId,
        "profile"      -> o.profile,
        "token"        -> o.token,
        "realm"        -> o.realm,
        "otoroshiData" -> o.otoroshiData,
        "createdAt"    -> o.createdAt.toDate.getTime,
        "expiredAt"    -> o.expiredAt.toDate.getTime,
        "lastRefresh"  -> o.lastRefresh.toDate.getTime,
        "metadata"     -> o.metadata,
        "tags"         -> JsArray(o.tags.map(JsString.apply))
      )
  }
}

trait PrivateAppsUserDataStore extends BasicStore[PrivateAppsUser]

object PrivateAppsUserHelper {

  import otoroshi.utils.http.RequestImplicits._

  case class PassWithAuthContext(
      req: RequestHeader,
      query: ServiceDescriptorQuery,
      descriptor: ServiceDescriptor,
      attrs: TypedMap,
      config: GlobalConfig,
      logger: Logger
  )

  def isPrivateAppsSessionValid(req: RequestHeader, desc: ServiceDescriptor, attrs: TypedMap)(implicit
      executionContext: ExecutionContext,
      env: Env
  ): Future[Option[PrivateAppsUser]] = {
    attrs.get(otoroshi.plugins.Keys.UserKey) match {
      case Some(preExistingUser) => FastFuture.successful(Some(preExistingUser))
      case _                     => {
        desc.authConfigRef match {
          case Some(ref) =>
            env.proxyState.authModuleAsync(ref).flatMap {
              case None       => FastFuture.successful(None)
              case Some(auth) => isPrivateAppsSessionValidWithAuth(req, desc, auth)
            }
          case None      =>
            FastFuture.successful(None)
        }
      }
    }
  }

  def isPrivateAppsSessionValidWithAuth(req: RequestHeader, desc: ServiceDescriptor, auth: AuthModuleConfig)(implicit
      ec: ExecutionContext,
      env: Env
  ): Future[Option[PrivateAppsUser]] = {
    val expected = "oto-papps-" + auth.cookieSuffix(desc)
    req.cookies
      .get(expected)
      .flatMap { cookie =>
        env.extractPrivateSessionId(cookie)
      }
      .orElse(
        req.getQueryString("pappsToken").flatMap(value => env.extractPrivateSessionIdFromString(value))
      )
      .orElse(
        req.headers.get("Otoroshi-Token").flatMap(value => env.extractPrivateSessionIdFromString(value))
      )
      .map { id =>
        if (Cluster.logger.isDebugEnabled) Cluster.logger.debug(s"private apps session checking for $id - from helper")
        env.datastores.privateAppsUserDataStore.findById(id).flatMap {
          case Some(user)                                           =>
            GenericOauth2Module.handleTokenRefresh(auth, user)
            FastFuture.successful(Some(user))
          case None if env.clusterConfig.mode == ClusterMode.Worker => {
            if (Cluster.logger.isDebugEnabled)
              Cluster.logger.debug(s"private apps session $id not found locally - from helper")
            env.clusterAgent.isSessionValid(id, Some(req)).map {
              case Some(user) =>
                user.save(
                  Duration(user.expiredAt.getMillis - System.currentTimeMillis(), TimeUnit.MILLISECONDS)
                )
                Some(user)
              case None       => None
            }
          }
          case None                                                 => FastFuture.successful(None)
        }
      } getOrElse {
      FastFuture.successful(None)
    }
  }

  def isPrivateAppsSessionValidWithMultiAuth(req: RequestHeader, route: NgRoute)(implicit
      ec: ExecutionContext,
      env: Env
  ) = {
    route.plugins
      .getPluginByClass[MultiAuthModule]
      .map(multiAuth =>
        NgMultiAuthModuleConfig.format.reads(multiAuth.config.raw) match {
          case JsSuccess(config, _) =>
            req.cookies.filter(cookie => cookie.name.startsWith("oto-papps")) match {
              case cookies if cookies.nonEmpty =>
                val r: Future[Option[JsObject]] = config.modules
                  .flatMap(module => env.proxyState.authModule(module))
                  .find(module =>
                    cookies.exists(cookie => cookie.name == s"oto-papps-${module.routeCookieSuffix(route)}")
                  )
                  .map(authModuleConfig =>
                    PrivateAppsUserHelper.isPrivateAppsSessionValidWithAuth(req, route.legacy, authModuleConfig).map {
                      case None          => None
                      case Some(session) => (session.profile.as[JsObject] ++ Json.obj("access_type" -> "session")).some
                    }
                  )
                  .getOrElse(None.vfuture)

                r
            }
          case JsError(_)           => None.vfuture
        }
      )
      .getOrElse(None.vfuture)
  }

  def passWithAuth[T](
      ctx: PassWithAuthContext,
      callDownstream: (GlobalConfig, Option[ApiKey], Option[PrivateAppsUser]) => Future[Either[Result, T]],
      errorResult: (Results.Status, String, String) => Future[Either[Result, T]]
  )(implicit ec: ExecutionContext, env: Env): Future[Either[Result, T]] = {

    val PassWithAuthContext(req, query, descriptor, attrs, config, logger) = ctx

    isPrivateAppsSessionValid(req, descriptor, attrs).flatMap {
      case Some(paUsr) =>
        callDownstream(config, None, Some(paUsr))
      case None        => {
        // val redirect   = req
        //   .getQueryString("redirect")
        //   .getOrElse(s"${req.theProtocol}://${req.theHost}${req.relativeUri}")
        val baseRedirect    = s"${req.theProtocol}://${req.theHost}${req.relativeUri}"
        val redirect        =
          if (env.allowRedirectQueryParamOnLogin) req.getQueryString("redirect").getOrElse(baseRedirect)
          else baseRedirect
        val encodedRedirect = Base64.getUrlEncoder.encodeToString(redirect.getBytes(StandardCharsets.UTF_8))
        val descriptorId    = descriptor.id
        val hash            = env.sign(s"desc=${descriptorId}&redirect=${encodedRedirect}")
        val redirectTo      =
          env.rootScheme + env.privateAppsHost + env.privateAppsPort + otoroshi.controllers.routes.AuthController
            .confidentialAppLoginPage()
            .url + s"?desc=${descriptorId}&redirect=${encodedRedirect}&hash=${hash}"
        if (logger.isTraceEnabled) logger.trace("should redirect to " + redirectTo)
        descriptor.authConfigRef match {
          case None      =>
            errorResult(
              InternalServerError,
              "Auth. config. ref not found on the descriptor",
              "errors.auth.config.ref.not.found"
            )
          case Some(ref) => {
            // env.datastores.authConfigsDataStore.findById(ref).flatMap {
            env.proxyState.authModuleAsync(ref).flatMap {
              case None       =>
                errorResult(
                  InternalServerError,
                  "Auth. config. not found on the descriptor",
                  "errors.auth.config.not.found"
                )
              case Some(auth) => {
                FastFuture
                  .successful(
                    Left(
                      Results
                        .Redirect(redirectTo)
                        .discardingCookies(
                          env.removePrivateSessionCookies(
                            query.toHost,
                            descriptor,
                            auth
                          ): _*
                        )
                    )
                  )
              }
            }
          }
        }
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy