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

plugins.jwt.scala Maven / Gradle / Ivy

package otoroshi.plugins.jwt

import otoroshi.env.Env
import otoroshi.models.PrivateAppsUser
import org.joda.time.DateTime
import otoroshi.next.plugins.api.{NgPluginCategory, NgPluginVisibility, NgStep}
import otoroshi.utils.JsonPathUtils
import otoroshi.script.{PreRouting, PreRoutingContext, PreRoutingError}
import otoroshi.utils.syntax.implicits._
import play.api.libs.json.{JsBoolean, JsNull, JsNumber, JsObject, JsString, JsValue, Json}
import play.api.mvc.Results
import otoroshi.security.{IdGenerator, OtoroshiClaim}

import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try

// MIGRATED
class JwtUserExtractor extends PreRouting {

  override def name: String = "Jwt user extractor"

  override def defaultConfig: Option[JsObject] =
    Some(
      Json.obj(
        "JwtUserExtractor" -> Json.obj(
          "verifier"  -> "",
          "strict"    -> true,
          "namePath"  -> "name",
          "emailPath" -> "email",
          "metaPath"  -> JsNull
        )
      )
    )

  override def configSchema: Option[JsObject] =
    super.configSchema.map(
      _ ++ Json.obj(
        "verifier" -> Json.obj(
          "type"  -> "select",
          "props" -> Json.obj(
            "label"              -> "JWT Verifier",
            "placeholer"         -> "JWT verifier to use to validate token",
            "valuesFrom"         -> "/bo/api/proxy/api/verifiers",
            "transformerMapping" -> Json.obj("label" -> "name", "value" -> "id")
          )
        )
      )
    )

  override def description: Option[String] =
    Some(
      s"""This plugin extract a user from a JWT token
        |
        |This plugin can accept the following configuration
        |
        |```json
        |${defaultConfig.get.prettify}
        |```
      """.stripMargin
    )

  private val registeredClaims = Seq(
    "iss",
    "sub",
    "aud",
    "exp",
    "nbf",
    "iat",
    "jti"
  )

  override def visibility: NgPluginVisibility    = NgPluginVisibility.NgUserLand
  override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Authentication)
  override def steps: Seq[NgStep]                = Seq(NgStep.PreRoute)

  override def preRoute(ctx: PreRoutingContext)(implicit env: Env, ec: ExecutionContext): Future[Unit] = {
    val config        = ctx.configFor("JwtUserExtractor")
    val jwtVerifierId = (config \ "verifier").as[String]
    val strict        = (config \ "strict").asOpt[Boolean].getOrElse(true)
    val strip         = (config \ "strip").asOpt[Boolean].getOrElse(false)
    val namePath      = (config \ "namePath").asOpt[String].getOrElse("name")
    val emailPath     = (config \ "emailPath").asOpt[String].getOrElse("email")
    val metaPath      = (config \ "metaPath").asOpt[String]
    env.datastores.globalJwtVerifierDataStore.findById(jwtVerifierId).flatMap {
      case None if !strict =>
        ().future
      case None if strict  =>
        Future.failed(
          PreRoutingError.fromJson(
            Json.obj("error" -> "unauthorized", "error_description" -> "You have to provide a valid user"),
            401
          )
        )
      case Some(verifier)  => {
        verifier
          .verify(
            ctx.request,
            ctx.descriptor,
            None,
            None,
            ctx.attrs.get(otoroshi.plugins.Keys.ElCtxKey).get,
            ctx.attrs
          ) { jwtInjection =>
            jwtInjection.decodedToken match {
              case None if !strict => Results.Unauthorized(Json.obj()).future
              case None if strict  => Results.Ok(Json.obj()).future
              case Some(token)     => {
                val jsonToken                         = new String(OtoroshiClaim.decoder.decode(token.getPayload))
                val parsedJsonToken                   = Json.parse(jsonToken).as[JsObject]
                val strippedJsonToken                 = JsObject(parsedJsonToken.value.filter {
                  case (key, _) if registeredClaims.contains(key) => false
                  case _                                          => true
                })
                val tokenMap: Map[String, String]     = parsedJsonToken.value.collect {
                  case (key, JsNumber(number)) => (key, number.toString())
                  case (key, JsString(value))  => (key, value)
                  case (key, JsBoolean(value)) => (key, value.toString)
                }.toMap
                val meta: Option[JsValue]             =
                  metaPath.flatMap(path => Try(JsonPathUtils.getAt[JsObject](jsonToken, path)).toOption.flatten)
                val user: PrivateAppsUser             = PrivateAppsUser(
                  randomId = IdGenerator.uuid,
                  name = JsonPathUtils.getAt[String](jsonToken, namePath).getOrElse("--"),
                  email = JsonPathUtils.getAt[String](jsonToken, emailPath).getOrElse("--"),
                  profile = if (strip) strippedJsonToken else parsedJsonToken,
                  token = Json.obj("jwt" -> token.getToken, "payload" -> parsedJsonToken),
                  realm = s"JwtUserExtractor@${ctx.descriptor.id}",
                  authConfigId = s"JwtUserExtractor@${ctx.descriptor.id}",
                  otoroshiData = meta,
                  createdAt = DateTime.now(),
                  expiredAt = DateTime.now().plusHours(1),
                  lastRefresh = DateTime.now(),
                  tags = Seq.empty,
                  metadata = Map.empty,
                  location = ctx.descriptor.location
                )
                ctx.attrs.put(otoroshi.plugins.Keys.UserKey -> user)
                val newElContext: Map[String, String] = ctx.attrs.get(otoroshi.plugins.Keys.ElCtxKey).get ++ tokenMap
                ctx.attrs.put(otoroshi.plugins.Keys.ElCtxKey -> newElContext)
                Results.Ok(Json.obj()).future
              }
            }
          }
          .recover { case e: Throwable =>
            Results.Unauthorized(Json.obj())
          }
          .flatMap { result =>
            result.header.status match {
              case 200 =>
                ().future
              case _   =>
                Future.failed(
                  PreRoutingError.fromJson(
                    Json.obj("error" -> "unauthorized", "error_description" -> "You have to provide a valid user"),
                    401
                  )
                )
            }
          }
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy