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

next.plugins.jwt.scala Maven / Gradle / Ivy

The newest version!
package otoroshi.next.plugins

import akka.stream.Materializer
import com.auth0.jwt.JWT
import otoroshi.env.Env
import otoroshi.models.{DefaultToken, InCookie, InHeader, InQueryParam, OutputMode, RefJwtVerifier}
import otoroshi.next.plugins.Keys.JwtInjectionKey
import otoroshi.next.plugins.api._
import otoroshi.security.IdGenerator
import otoroshi.utils.syntax.implicits.{BetterJsValue, BetterSyntax}
import play.api.libs.json._
import play.api.libs.ws.DefaultWSCookie
import play.api.mvc.{Result, Results}
import org.apache.commons.codec.binary.{Base64 => ApacheBase64}
import otoroshi.el.JwtExpressionLanguage

import java.nio.charset.StandardCharsets
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.util.{Failure, Success, Try}

case class NgJwtVerificationConfig(verifiers: Seq[String] = Seq.empty) extends NgPluginConfig {
  def json: JsValue = NgJwtVerificationConfig.format.writes(this)
}

object NgJwtVerificationConfig {
  val format = new Format[NgJwtVerificationConfig] {
    override def reads(json: JsValue): JsResult[NgJwtVerificationConfig] = Try {
      NgJwtVerificationConfig(
        verifiers = json.select("verifiers").asOpt[Seq[String]].getOrElse(Seq.empty)
      )
    } match {
      case Failure(e) => JsError(e.getMessage)
      case Success(c) => JsSuccess(c)
    }
    override def writes(o: NgJwtVerificationConfig): JsValue             = Json.obj("verifiers" -> o.verifiers)
  }
}

class JwtVerification extends NgAccessValidator with NgRequestTransformer {

  override def steps: Seq[NgStep]                = Seq(NgStep.ValidateAccess, NgStep.TransformRequest)
  override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.AccessControl, NgPluginCategory.Classic)
  override def visibility: NgPluginVisibility    = NgPluginVisibility.NgUserLand

  override def multiInstance: Boolean      = true
  override def core: Boolean               = true
  override def usesCallbacks: Boolean      = false
  override def transformsRequest: Boolean  = true
  override def transformsResponse: Boolean = false
  override def transformsError: Boolean    = false

  override def isAccessAsync: Boolean                      = true
  override def isTransformRequestAsync: Boolean            = false
  override def isTransformResponseAsync: Boolean           = true
  override def name: String                                = "Jwt verifiers"
  override def description: Option[String]                 =
    "This plugin verifies the current request with one or more jwt verifier".some
  override def defaultConfigObject: Option[NgPluginConfig] = NgJwtVerificationConfig().some

  override def access(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
    val config = ctx.cachedConfig(internalName)(NgJwtVerificationConfig.format).getOrElse(NgJwtVerificationConfig())

    config.verifiers match {
      case Nil         => JwtVerifierUtils.onError()
      case verifierIds => JwtVerifierUtils.verify(ctx, verifierIds)
    }
  }

  override def transformRequestSync(
      ctx: NgTransformerRequestContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Either[Result, NgPluginHttpRequest] = {
    ctx.attrs.get(JwtInjectionKey) match {
      case None            => ctx.otoroshiRequest.right
      case Some(injection) => {
        ctx.otoroshiRequest
          .applyOnIf(injection.removeCookies.nonEmpty) { req =>
            req.copy(cookies = req.cookies.filterNot(c => injection.removeCookies.contains(c.name)))
          }
          .applyOnIf(injection.removeHeaders.nonEmpty) { req =>
            req.copy(headers =
              req.headers.filterNot(tuple => injection.removeHeaders.map(_.toLowerCase).contains(tuple._1.toLowerCase))
            )
          }
          .applyOnIf(injection.additionalHeaders.nonEmpty) { req =>
            req.copy(headers = req.headers ++ injection.additionalHeaders)
          }
          .applyOnIf(injection.additionalCookies.nonEmpty) { req =>
            req.copy(cookies = req.cookies ++ injection.additionalCookies.map(t => DefaultWSCookie(t._1, t._2)))
          }
          .right
      }
    }
  }
}

object JwtVerifierUtils {
  def onError(result: Option[Result] = None): Future[NgAccess] = {
    NgAccess
      .NgDenied(
        result.getOrElse(Results.BadRequest(Json.obj("error" -> "bad request")))
      )
      .vfuture
  }

  def verify(ctx: NgAccessContext, verifierIds: Seq[String])(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[NgAccess] = {
    val verifier = RefJwtVerifier(verifierIds, enabled = true, Seq.empty)
    if (verifier.isAsync) {
      verifier
        .verifyFromCache(
          request = ctx.request,
          desc = ctx.route.serviceDescriptor.some,
          apikey = ctx.apikey,
          user = ctx.user,
          elContext = ctx.attrs.get(otoroshi.plugins.Keys.ElCtxKey).getOrElse(Map.empty),
          attrs = ctx.attrs
        )
        .flatMap {
          case Left(result)     => onError(result.some)
          case Right(injection) =>
            ctx.attrs.put(JwtInjectionKey -> injection)
            NgAccess.NgAllowed.vfuture
        }
    } else {
      verifier.verifyFromCacheSync(
        request = ctx.request,
        desc = ctx.route.serviceDescriptor.some,
        apikey = ctx.apikey,
        user = ctx.user,
        elContext = ctx.attrs.get(otoroshi.plugins.Keys.ElCtxKey).getOrElse(Map.empty),
        attrs = ctx.attrs
      ) match {
        case Left(result)     => onError(result.some)
        case Right(injection) =>
          ctx.attrs.put(JwtInjectionKey -> injection)
          NgAccess.NgAllowed.vfuture
      }
    }
  }
}

case class NgJwtVerificationOnlyConfig(verifier: Option[String] = None, failIfAbsent: Boolean = true)
    extends NgPluginConfig {
  def json: JsValue = NgJwtVerificationOnlyConfig.format.writes(this)
}

object NgJwtVerificationOnlyConfig {
  val format = new Format[NgJwtVerificationOnlyConfig] {
    override def reads(json: JsValue): JsResult[NgJwtVerificationOnlyConfig] = Try {
      NgJwtVerificationOnlyConfig(
        verifier = json.select("verifier").asOpt[String],
        failIfAbsent = json.select("fail_if_absent").asOpt[Boolean].getOrElse(true)
      )
    } match {
      case Failure(e) => JsError(e.getMessage)
      case Success(c) => JsSuccess(c)
    }
    override def writes(o: NgJwtVerificationOnlyConfig): JsValue             = Json.obj(
      "verifier"       -> o.verifier,
      "fail_if_absent" -> o.failIfAbsent
    )
  }
}

class JwtVerificationOnly extends NgAccessValidator with NgRequestTransformer {

  override def defaultConfigObject: Option[NgPluginConfig] = NgJwtVerificationOnlyConfig().some

  override def steps: Seq[NgStep]                = Seq(NgStep.ValidateAccess)
  override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.AccessControl, NgPluginCategory.Classic)
  override def visibility: NgPluginVisibility    = NgPluginVisibility.NgUserLand

  override def multiInstance: Boolean      = true
  override def core: Boolean               = true
  override def usesCallbacks: Boolean      = false
  override def transformsRequest: Boolean  = true
  override def transformsResponse: Boolean = false
  override def transformsError: Boolean    = false

  override def isAccessAsync: Boolean            = true
  override def isTransformRequestAsync: Boolean  = false
  override def isTransformResponseAsync: Boolean = true
  override def name: String                      = "Jwt verification only"
  override def description: Option[String]       =
    "This plugin verifies the current request with one jwt verifier".some

  override def access(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
    val config =
      ctx.cachedConfig(internalName)(NgJwtVerificationOnlyConfig.format).getOrElse(NgJwtVerificationOnlyConfig())

    config.verifier match {
      case None             => JwtVerifierUtils.onError()
      case Some(verifierId) =>
        env.proxyState.jwtVerifier(verifierId) match {
          case None           => JwtVerifierUtils.onError()
          case Some(verifier) =>
            verifier.source.token(ctx.request) match {
              case None if !config.failIfAbsent => NgAccess.NgAllowed.vfuture
              case _                            => JwtVerifierUtils.verify(ctx, Seq(verifierId))
            }
        }
    }
  }
}

case class NgJwtSignerConfig(
    verifier: Option[String] = None,
    replaceIfPresent: Boolean = true,
    failIfPresent: Boolean = false
) extends NgPluginConfig {
  def json: JsValue = NgJwtSignerConfig.format.writes(this)
}

object NgJwtSignerConfig {
  val format = new Format[NgJwtSignerConfig] {
    override def reads(json: JsValue): JsResult[NgJwtSignerConfig] = Try {
      NgJwtSignerConfig(
        verifier = json.select("verifier").asOpt[String],
        replaceIfPresent = json.select("replace_if_present").asOpt[Boolean].getOrElse(true),
        failIfPresent = json.select("fail_if_present").asOpt[Boolean].getOrElse(true)
      )
    } match {
      case Failure(e) => JsError(e.getMessage)
      case Success(c) => JsSuccess(c)
    }
    override def writes(o: NgJwtSignerConfig): JsValue             = Json.obj(
      "verifier"           -> o.verifier,
      "replace_if_present" -> o.replaceIfPresent,
      "fail_if_present"    -> o.failIfPresent
    )
  }
}

class JwtSigner extends NgAccessValidator with NgRequestTransformer {

  override def defaultConfigObject: Option[NgPluginConfig] = NgJwtSignerConfig().some

  override def steps: Seq[NgStep]                = Seq(NgStep.ValidateAccess, NgStep.TransformRequest)
  override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Transformations)
  override def visibility: NgPluginVisibility    = NgPluginVisibility.NgUserLand

  override def multiInstance: Boolean      = true
  override def core: Boolean               = true
  override def usesCallbacks: Boolean      = false
  override def transformsRequest: Boolean  = true
  override def transformsResponse: Boolean = false
  override def transformsError: Boolean    = false

  override def isAccessAsync: Boolean            = true
  override def isTransformRequestAsync: Boolean  = false
  override def isTransformResponseAsync: Boolean = true
  override def name: String                      = "Jwt signer"
  override def description: Option[String]       = "This plugin can only generate token".some

  override def access(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
    val config = ctx.cachedConfig(internalName)(NgJwtSignerConfig.format).getOrElse(NgJwtSignerConfig())

    if (config.failIfPresent) {
      config.verifier match {
        case None             => NgAccess.NgAllowed.vfuture
        case Some(verifierId) =>
          env.proxyState.jwtVerifier(verifierId) match {
            case None           => NgAccess.NgAllowed.vfuture
            case Some(verifier) =>
              verifier.source.token(ctx.request) match {
                case None => NgAccess.NgDenied(Results.BadRequest(Json.obj("error" -> "bad request"))).vfuture
                case _    => NgAccess.NgAllowed.vfuture
              }
          }
      }
    } else {
      NgAccess.NgAllowed.vfuture
    }
  }

  override def transformRequestSync(
      ctx: NgTransformerRequestContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Either[Result, NgPluginHttpRequest] = {
    val config = ctx.cachedConfig(internalName)(NgJwtSignerConfig.format).getOrElse(NgJwtSignerConfig())

    config.verifier match {
      case None             =>
        Results
          .BadRequest(Json.obj("error" -> "bad request"))
          .left
      case Some(verifierId) =>
        env.proxyState.jwtVerifier(verifierId) match {
          case None                 =>
            Results
              .BadRequest(Json.obj("error" -> "bad request"))
              .left
          case Some(globalVerifier) =>
            if (!config.replaceIfPresent && globalVerifier.source.token(ctx.request).isDefined) {
              ctx.otoroshiRequest.right
            } else {
              globalVerifier.algoSettings.asAlgorithm(OutputMode) match {
                case None                        =>
                  Results
                    .BadRequest(Json.obj("error" -> "bad request"))
                    .left
                case Some(tokenSigningAlgorithm) => {
                  val user                   = ctx.user.orElse(ctx.attrs.get(otoroshi.plugins.Keys.UserKey))
                  val apikey                 = ctx.attrs.get(otoroshi.plugins.Keys.ApiKeyKey)
                  val optSub: Option[String] = apikey.map(_.clientName).orElse(user.map(_.email))

                  val token = JsObject(
                    JwtExpressionLanguage
                      .fromJson(
                        Json.obj(
                          "jti" -> IdGenerator.uuid,
                          "iat" -> Math.floor(System.currentTimeMillis() / 1000L).toLong,
                          "nbf" -> Math.floor(System.currentTimeMillis() / 1000L).toLong,
                          "iss" -> "Otoroshi",
                          "exp" -> Math.floor((System.currentTimeMillis() + 60000L) / 1000L).toLong,
                          "sub" -> JsString(optSub.getOrElse("anonymous")),
                          "aud" -> "backend"
                        ) ++ globalVerifier.strategy.asInstanceOf[DefaultToken].token.as[JsObject],
                        Some(ctx.request),
                        None,
                        ctx.route.some,
                        apikey,
                        user,
                        Map.empty,
                        ctx.attrs,
                        env
                      )
                      .as[JsObject]
                      .value
                      .map { case (key, value) =>
                        value match {
                          case JsString(v) if v == "{iat}" =>
                            (key, JsNumber(Math.floor(System.currentTimeMillis() / 1000L).toLong))
                          case JsString(v) if v == "{nbf}" =>
                            (key, JsNumber(Math.floor(System.currentTimeMillis() / 1000L).toLong))
                          case JsString(v) if v == "{exp}" =>
                            (key, JsNumber(Math.floor((System.currentTimeMillis() + 60000L) / 1000L).toLong))
                          case _                           => (key, value.as[JsValue])
                        }
                      }
                  )

                  val headerJson     = Json
                    .obj("alg" -> tokenSigningAlgorithm.getName, "typ" -> "JWT")
                    .applyOnWithOpt(globalVerifier.algoSettings.keyId)((h, id) => h ++ Json.obj("kid" -> id))
                  val header         =
                    ApacheBase64.encodeBase64URLSafeString(Json.stringify(headerJson).getBytes(StandardCharsets.UTF_8))
                  val payload        =
                    ApacheBase64.encodeBase64URLSafeString(Json.stringify(token).getBytes(StandardCharsets.UTF_8))
                  val content        = String.format("%s.%s", header, payload)
                  val signatureBytes =
                    tokenSigningAlgorithm.sign(
                      header.getBytes(StandardCharsets.UTF_8),
                      payload.getBytes(StandardCharsets.UTF_8)
                    )
                  val signature      = ApacheBase64.encodeBase64URLSafeString(signatureBytes)

                  val signedToken = s"$content.$signature"

                  val originalToken = JWT.decode(signedToken)
                  ctx.attrs.put(otoroshi.plugins.Keys.MatchedOutputTokenKey -> token)

                  (globalVerifier.source match {
                    case _: InQueryParam =>
                      globalVerifier.source.asJwtInjection(originalToken, signedToken).right[Result]
                    case InHeader(n, _)  =>
                      val inj = globalVerifier.source.asJwtInjection(originalToken, signedToken)
                      globalVerifier.source match {
                        case InHeader(nn, _) if nn == n => inj.right[Result]
                        case _                          => inj.copy(removeHeaders = Seq(n)).right[Result]
                      }
                    case InCookie(n)     =>
                      globalVerifier.source
                        .asJwtInjection(originalToken, signedToken)
                        .copy(removeCookies = Seq(n))
                        .right[Result]
                  }) match {
                    case Left(result)    => result.left
                    case Right(newValue) =>
                      ctx.otoroshiRequest
                        .applyOnIf(newValue.removeCookies.nonEmpty) { req =>
                          req.copy(cookies = req.cookies.filterNot(c => newValue.removeCookies.contains(c.name)))
                        }
                        .applyOnIf(newValue.removeHeaders.nonEmpty) { req =>
                          req.copy(headers =
                            req.headers.filterNot(tuple =>
                              newValue.removeHeaders.map(_.toLowerCase).contains(tuple._1.toLowerCase)
                            )
                          )
                        }
                        .applyOnIf(newValue.additionalHeaders.nonEmpty) { req =>
                          req.copy(headers = req.headers ++ newValue.additionalHeaders)
                        }
                        .applyOnIf(newValue.additionalCookies.nonEmpty) { req =>
                          req.copy(cookies =
                            req.cookies ++ newValue.additionalCookies.map(t => DefaultWSCookie(t._1, t._2))
                          )
                        }
                        .right
                  }
                }
              }
            }
        }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy