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

plugins.apikeys.scala Maven / Gradle / Ivy

The newest version!
package otoroshi.plugins.apikeys

import akka.http.scaladsl.util.FastFuture
import akka.stream.Materializer
import akka.stream.scaladsl.{Sink, Source}
import akka.util.ByteString
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import org.biscuitsec.biscuit.datalog.SymbolTable
import org.biscuitsec.biscuit.token.builder.parser.Parser
import com.github.blemale.scaffeine.{Cache, Scaffeine}
import com.google.common.base.Charsets
import com.nimbusds.jose.jwk.{Curve, ECKey, RSAKey}
import org.apache.commons.codec.binary.Base64
import org.joda.time.DateTime
import otoroshi.cluster.ClusterAgent
import otoroshi.env.Env
import otoroshi.models._
import otoroshi.next.plugins.api.{NgPluginCategory, NgPluginVisibility, NgStep}
import otoroshi.utils.JsonPathUtils
import otoroshi.script._
import otoroshi.security.{IdGenerator, OtoroshiClaim}
import otoroshi.ssl.{Cert, DynamicSSLEngineProvider}
import otoroshi.utils.cache.types.UnboundedTrieMap
import otoroshi.utils.crypto.Signatures
import otoroshi.utils.http.DN
import otoroshi.utils.jwk.JWKSHelper
import otoroshi.utils.syntax.implicits._
import play.api.libs.json._
import play.api.mvc.{Result, Results}
import play.core.parsers.FormUrlEncodedParser

import java.security.interfaces.{ECPrivateKey, ECPublicKey, RSAPrivateKey, RSAPublicKey}
import java.security.{KeyPair, SecureRandom}
import java.util.concurrent.atomic.AtomicBoolean
import scala.collection.concurrent.TrieMap
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.util.{Failure, Success, Try}

// DEPRECATED
class HasAllowedApiKeyValidator extends AccessValidator {

  override def deprecated: Boolean = true

  override def name: String = "[DEPRECATED] Allowed apikeys only"

  override def configRoot: Option[String] = Some("HasAllowedApiKeyValidator")

  override def configFlow: Seq[String] = Seq("clientIds", "tags", "metadata")

  override def configSchema: Option[JsObject] =
    Some(
      Json
        .parse("""{
      |  "clientIds": {
      |    "type": "array",
      |    "props": { "label": "Allowed apikeys", "valuesFrom": "/bo/api/proxy/api/apikeys", "transformerMapping": { "label":"clientName", "value":"clientId" } }
      |  },
      |  "tags": {
      |    "type": "array",
      |    "props": { "label": "Allowed tags" }
      |  },
      |  "metadata": {
      |    "type": "object",
      |    "props": { "label": "Allowed metadata" }
      |  },
      |  "apikeyMatch": {
      |    "type": "array",
      |    "props": { "label": "Apikey matching", "placeholder": "JSON Path query" }
      |  },
      |  "apikeyNotMatch": {
      |    "type": "array",
      |    "props": { "label": "Apikey not matching", "placeholder": "JSON Path query" }
      |  }
      |}""".stripMargin)
        .as[JsObject]
    )

  override def defaultConfig: Option[JsObject] =
    Some(
      Json.obj(
        configRoot.get -> Json.obj(
          "clientIds"      -> Json.arr(),
          "tags"           -> Json.arr(),
          "metadata"       -> Json.obj(),
          "apikeyMatch"    -> Json.obj(),
          "apikeyNotMatch" -> Json.obj()
        )
      )
    )

  override def description: Option[String] =
    Some("""Validation based on apikeys
      |
      |```json
      |{
      |  "HasAllowedApiKeyValidator": {
      |    "clientIds": [],  // list of allowed client ids,
      |    "tags": [],       // list of allowed tags
      |    "metadata": {}    // allowed metadata,
      |    "apikeyMatch": [],    // json path expressions to match against apikey. passes if one match
      |    "apikeyNotMatch": [], // json path expressions to match against apikey. passes if none match
      |  }
      |}
      |```
    """.stripMargin)

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

  override def canAccess(context: AccessContext)(implicit env: Env, ec: ExecutionContext): Future[Boolean] = {
    context.apikey match {
      case Some(apiKey) => {
        val config           = (context.config \ "HasAllowedApiKeyValidator")
          .asOpt[JsValue]
          .orElse((context.config \ "HasAllowedApiKeyValidator").asOpt[JsValue])
          .getOrElse(context.config)
        val allowedClientIds =
          (config \ "clientIds").asOpt[JsArray].map(_.value.map(_.as[String])).getOrElse(Seq.empty[String])
        val allowedTags      = (config \ "tags").asOpt[JsArray].map(_.value.map(_.as[String])).getOrElse(Seq.empty[String])
        val allowedMetadatas = (config \ "metadata").asOpt[Map[String, String]].getOrElse(Map.empty[String, String])
        val apikeyMatch      =
          (config \ "apikeyMatch").asOpt[JsArray].map(_.value.map(_.as[String])).getOrElse(Seq.empty[String])
        val apikeyNotMatch   =
          (config \ "apikeyNotMatch").asOpt[JsArray].map(_.value.map(_.as[String])).getOrElse(Seq.empty[String])
        lazy val apikeyJson  = apiKey.toJson
        if (
          allowedClientIds.contains(apiKey.clientId) ||
          allowedTags.exists(tag => apiKey.tags.contains(tag)) ||
          allowedMetadatas.exists(meta => apiKey.metadata.get(meta._1).contains(meta._2)) ||
          (apikeyMatch.exists(JsonPathUtils.matchWith(apikeyJson, "apikey")) && !apikeyNotMatch.exists(
            JsonPathUtils.matchWith(apikeyJson, "apikey")
          ))
        ) {
          FastFuture.successful(true)
        } else {
          FastFuture.successful(false)
        }
      }
      case _            =>
        FastFuture.successful(false)
    }
  }
}

// DEPRECATED
class ApiKeyAllowedOnThisServiceValidator extends AccessValidator {

  override def deprecated: Boolean = true

  override def name: String = "[DEPRECATED] Allowed apikeys for this service only (service packs)"

  override def description: Option[String] =
    Some(
      """This plugin only let pass apikeys containing the id of the service on their tags. It is quite useful to create apikeys that
      |can access a `pack` of services. Apikeys should have tags named like
      |
      |```
      |"allowed-on-${service.id}"
      |```
      |
    """.stripMargin
    )

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

  override def canAccess(ctx: AccessContext)(implicit env: Env, ec: ExecutionContext): Future[Boolean] = {
    ctx.apikey match {
      case Some(apiKey) => {
        val serviceIds = apiKey.tags.map(tag => tag.replace("allowed-on-", ""))
        FastFuture.successful(serviceIds.exists(id => id == ctx.descriptor.id))
      }
      case _            => FastFuture.successful(false)
    }
  }
}

// MIGRATED
class CertificateAsApikey extends PreRouting {

  override def name: String = "Client certificate as apikey"

  override def defaultConfig: Option[JsObject] =
    Some(
      Json.obj(
        "CertificateAsApikey" -> Json.obj(
          "readOnly"                -> false,
          "allowClientIdOnly"       -> false,
          "throttlingQuota"         -> 100,
          "dailyQuota"              -> RemainingQuotas.MaxValue,
          "monthlyQuota"            -> RemainingQuotas.MaxValue,
          "constrainedServicesOnly" -> false,
          "tags"                    -> Json.arr(),
          "metadata"                -> Json.obj()
        )
      )
    )

  override def description: Option[String] =
    Some(
      s"""This plugin uses client certificate as an apikey. The apikey will be stored for classic apikey usage
        |
        |```json
        |${Json.prettyPrint(defaultConfig.get)}
        |```
      """.stripMargin
    )

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

  override def preRoute(context: PreRoutingContext)(implicit env: Env, ec: ExecutionContext): Future[Unit] = {
    context.request.clientCertificateChain.flatMap(_.headOption) match {
      case None       => FastFuture.successful(())
      case Some(cert) => {
        val conf         = context.configFor("CertificateAsApikey")
        val serialNumber = cert.getSerialNumber.toString
        val subjectDN    = DN(cert.getSubjectDN.getName).stringify
        val clientId     = Base64.encodeBase64String((subjectDN + "-" + serialNumber).getBytes)
        // TODO: validate CA DN based on config array
        // TODO: validate CA serial based on config array
        env.datastores.apiKeyDataStore
          .findById(clientId)
          .flatMap {
            case Some(apikey) => FastFuture.successful(apikey)
            case None         => {
              val apikey = ApiKey(
                clientId = clientId,
                clientSecret = IdGenerator.token(128),
                clientName = s"$subjectDN ($serialNumber)",
                authorizedEntities = Seq(ServiceDescriptorIdentifier(context.descriptor.id)),
                validUntil = Some(new DateTime(cert.getNotAfter)),
                readOnly = (conf \ "readOnly").asOpt[Boolean].getOrElse(false),
                allowClientIdOnly = (conf \ "allowClientIdOnly").asOpt[Boolean].getOrElse(false),
                throttlingQuota = (conf \ "throttlingQuota").asOpt[Long].getOrElse(100),
                dailyQuota = (conf \ "dailyQuota").asOpt[Long].getOrElse(RemainingQuotas.MaxValue),
                monthlyQuota = (conf \ "monthlyQuota").asOpt[Long].getOrElse(RemainingQuotas.MaxValue),
                constrainedServicesOnly = (conf \ "constrainedServicesOnly").asOpt[Boolean].getOrElse(false),
                tags = (conf \ "tags").asOpt[Seq[String]].getOrElse(Seq.empty),
                metadata = (conf \ "metadata").asOpt[Map[String, String]].getOrElse(Map.empty)
              )
              if (env.clusterConfig.mode.isWorker) {
                ClusterAgent.clusterSaveApikey(env, apikey)(ec, env.otoroshiMaterializer)
              }
              apikey.save().map(_ => apikey)
            }
          }
          .map { apikey =>
            context.attrs.put(otoroshi.plugins.Keys.ApiKeyKey -> apikey)
            ()
          }
      }
    }
  }
}

case class ClientCredentialFlowBody(
    grantType: String,
    clientId: String,
    clientSecret: String,
    scope: Option[String],
    bearerKind: String
)

// DEPRECATED
class ClientCredentialFlowExtractor extends PreRouting {

  override def name: String = "Client Credential Flow ApiKey extractor"

  override def description: Option[String] =
    Some(
      s"""This plugin can extract an apikey from an opaque access_token generate by the `ClientCredentialFlow` plugin""".stripMargin
    )

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

  override def preRoute(ctx: PreRoutingContext)(implicit env: Env, ec: ExecutionContext): Future[Unit] = {
    ctx.request.headers.get("Authorization") match {
      case Some(auth) if auth.startsWith("Bearer ") => {
        val token = auth.replace("Bearer ", "")
        env.datastores.rawDataStore
          .get(s"${env.storageRoot}:plugins:client-credentials-flow:access-tokens:$token")
          .flatMap {
            case Some(clientId) =>
              env.datastores.apiKeyDataStore.findById(clientId.utf8String).map {
                case Some(apikey) => {
                  env.datastores.rawDataStore
                    .get(s"${env.storageRoot}:plugins:client-credentials-flow:revoked-tokens:$token")
                    .map(_.map(_.utf8String.toBoolean).getOrElse(false))
                    .flatMap {
                      case true  =>
                        FastFuture.successful(())
                      case false =>
                        ctx.attrs.put(otoroshi.plugins.Keys.ApiKeyKey -> apikey)
                        FastFuture.successful(())
                    }
                }
                case _            => FastFuture.successful(())
              }
            case _              => FastFuture.successful(())
          }
      }
      case _                                        => FastFuture.successful(())
    }
  }
}

// DEPRECATED
class ClientCredentialFlow extends RequestTransformer {

  import otoroshi.utils.http.RequestImplicits._
  import otoroshi.utils.syntax.implicits._

  private val revokedCache: Cache[String, Boolean] = Scaffeine()
    .recordStats()
    .expireAfterWrite(1.hour)
    .maximumSize(1000)
    .build()

  override def deprecated: Boolean = true

  override def name: String =
    "[DEPRECATED] Client Credential Flow (deprecated, use the 'Client Credential Service' sink)"

  override def defaultConfig: Option[JsObject] = {
    Some(
      Json.obj(
        "ClientCredentialFlow" -> Json.obj(
          "expiration"        -> 1.hour.toMillis,
          "supportRevoke"     -> true,
          "supportIntrospect" -> true,
          "jwtToken"          -> true,
          "autonomous"        -> false,
          "signWithKeyPair"   -> false,
          "defaultKeyPair"    -> JsNull,
          "rootPath"          -> "/.well-known/otoroshi/oauth"
        )
      )
    )
  }

  override def description: Option[String] = {
    Some(
      s"""This plugin enables the oauth client credentials flow on a service and add an endpoint (`/.well-known/otoroshi/oauth/token`) to create an access_token given a client id and secret.
         |If you don't want to have access_tokens as JWT tokens, don't forget to use `ClientCredentialFlowExtractor` pre-routing plugin.
         |Don't forget to authorize access to `/.well-known/otoroshi/oauth/token` in service settings (public paths)
         |
         |```json
         |${Json.prettyPrint(defaultConfig.get)}
         |```
      """.stripMargin
    )
  }

  override def visibility: NgPluginVisibility    = NgPluginVisibility.NgInternal
  override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.AccessControl)
  override def steps: Seq[NgStep]                = Seq(NgStep.TransformRequest)

  private val awaitingRequests = new UnboundedTrieMap[String, Promise[Source[ByteString, _]]]()

  override def beforeRequest(
      ctx: BeforeRequestContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Unit] = {
    awaitingRequests.putIfAbsent(ctx.snowflake, Promise[Source[ByteString, _]])
    funit
  }

  override def afterRequest(
      ctx: AfterRequestContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Unit] = {
    awaitingRequests.remove(ctx.snowflake)
    funit
  }

  override def transformRequestWithCtx(
      ctx: TransformerRequestContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, HttpRequest]] = {
    val conf              = ctx.configFor("ClientCredentialFlow")
    val _signWithKeyPair  = (conf \ "signWithKeyPair").asOpt[Boolean].getOrElse(false)
    val useJwtToken       = (conf \ "jwtToken").asOpt[Boolean].getOrElse(true)
    val autonomous        = (conf \ "autonomous").asOpt[Boolean].getOrElse(false)
    val supportRevoke     = (conf \ "supportRevoke").asOpt[Boolean].getOrElse(false)
    val supportIntrospect = (conf \ "supportIntrospect").asOpt[Boolean].getOrElse(false)
    val expiration        = (conf \ "expiration").asOpt[Long].map(_.millis).getOrElse(1.hour)
    val rootPath          = (conf \ "rootPath").asOpt[String].getOrElse("/.well-known/otoroshi/oauth")
    val defaultKeyPair    = (conf \ "defaultKeyPair").asOpt[String].filter(_.trim.nonEmpty)

    def handleBody[A](
        f: Map[String, String] => Future[Either[Result, HttpRequest]]
    ): Future[Either[Result, HttpRequest]] = {
      awaitingRequests.get(ctx.snowflake).map { promise =>
        val consumed = new AtomicBoolean(false)

        val bodySource: Source[ByteString, _] = Source
          .future(promise.future)
          .flatMapConcat(s => s)
          .alsoTo(Sink.onComplete { case _ =>
            consumed.set(true)
          })

        bodySource.runFold(ByteString.empty)(_ ++ _).flatMap { bodyRaw =>
          ctx.request.headers.get("Content-Type") match {
            case Some(ctype) if ctype.toLowerCase().contains("application/x-www-form-urlencoded") => {

              val charset                  = ctx.request.charset.getOrElse("UTF-8")
              val urlEncodedString         = bodyRaw.utf8String
              val body                     = FormUrlEncodedParser.parse(urlEncodedString, charset).mapValues(_.head)
              val map: Map[String, String] = body ++ ctx.request.headers
                .get("Authorization")
                .filter(_.startsWith("Basic "))
                .map(_.replace("Basic ", ""))
                .map(v => org.apache.commons.codec.binary.Base64.decodeBase64(v))
                .map(v => new String(v))
                .filter(_.contains(":"))
                .map(_.split(":").toSeq)
                .map(v => Map("client_id" -> v.head, "client_secret" -> v.tail.mkString(":")))
                .getOrElse(Map.empty[String, String])
              f(map)
            }
            case Some(ctype) if ctype.toLowerCase().contains("application/json") => {
              val json                     = Json.parse(bodyRaw.utf8String).as[JsObject]
              val map: Map[String, String] = json.value.toSeq.collect {
                case (key, JsString(v))  => (key, v)
                case (key, JsNumber(v))  => (key, v.toString())
                case (key, JsBoolean(v)) => (key, v.toString)
              }.toMap ++ ctx.request.headers
                .get("Authorization")
                .filter(_.startsWith("Basic "))
                .map(_.replace("Basic ", ""))
                .map(v => org.apache.commons.codec.binary.Base64.decodeBase64(v))
                .map(v => new String(v))
                .filter(_.contains(":"))
                .map(_.split(":").toSeq)
                .map(v => Map("client_id" -> v.head, "client_secret" -> v.tail.mkString(":")))
                .getOrElse(Map.empty[String, String])
              f(map)
            }
            case _                                                               =>
              // bad content type
              Results.Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized")).leftf
          }
        } andThen { case _ =>
          if (!consumed.get()) bodySource.runWith(Sink.ignore)
        }
      } getOrElse {
        // no body
        Results.Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized")).leftf
      }
    }

    (ctx.rawRequest.method.toLowerCase(), ctx.rawRequest.path) match {
      case ("get", path) if supportRevoke && path == s"/.well-known/jwks.json"                          => {
        env.datastores.apiKeyDataStore.findAll().flatMap { apikeys =>
          val ids =
            apikeys.map(_.metadata.get("jwt-sign-keypair")).collect { case Some(value) => value } ++ defaultKeyPair
          env.datastores.certificatesDataStore.findAll().map { certs =>
            val exposedCerts: Seq[JsValue] =
              certs.filter(c => ids.contains(c.id)).map(c => (c.id, c.cryptoKeyPair.getPublic)).flatMap {
                case (id, pub: RSAPublicKey) => new RSAKey.Builder(pub).keyID(id).build().toJSONString.parseJson.some
                case (id, pub: ECPublicKey)  =>
                  new ECKey.Builder(Curve.forECParameterSpec(pub.getParams), pub)
                    .keyID(id)
                    .build()
                    .toJSONString
                    .parseJson
                    .some
                case _                       => None
              }
            Results.Ok(Json.obj("keys" -> JsArray(exposedCerts))).left
          }
        }
      }
      case ("post", path) if supportRevoke && path == s"$rootPath/token/revoke" && useJwtToken          =>
        handleBody { body =>
          (
            body.get("token"),
            body.get("revoke")
          ) match {
            case (Some(token), revokeRaw) => {
              val revoke  = revokeRaw.map(_.toBoolean).getOrElse(true)
              val decoded = JWT.decode(token)
              Try(decoded.getId).toOption match {
                case None                 => Results.Ok("").leftf
                case Some(jti) if revoke  =>
                  env.datastores.rawDataStore
                    .set(
                      s"${env.storageRoot}:plugins:client-credentials-flow:revoked-tokens:$jti",
                      token.byteString,
                      None
                    )
                    .map(_ => Results.Ok("").left)
                case Some(jti) if !revoke =>
                  env.datastores.rawDataStore
                    .del(Seq(s"${env.storageRoot}:plugins:client-credentials-flow:revoked-tokens:$jti"))
                    .map(_ => Results.Ok("").left)
              }
            }
            case _                        =>
              // bad body
              Results.Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized")).leftf
          }
        }
      case ("post", path) if supportRevoke && path == s"$rootPath/token/revoke" && !useJwtToken         =>
        handleBody { body =>
          (
            body.get("token"),
            body.get("revoke")
          ) match {
            case (Some(token), revokeRaw) => {
              val revoke = revokeRaw.map(_.toBoolean).getOrElse(true)
              if (revoke) {
                env.datastores.rawDataStore
                  .set(
                    s"${env.storageRoot}:plugins:client-credentials-flow:revoked-tokens:$token",
                    token.byteString,
                    None
                  )
                  .map(_ => Results.Ok("").left)
              } else {
                env.datastores.rawDataStore
                  .del(Seq(s"${env.storageRoot}:plugins:client-credentials-flow:revoked-tokens:$token"))
                  .map(_ => Results.Ok("").left)
              }
            }
            case _                        =>
              // bad body
              Results.Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized")).leftf
          }
        }
      case ("post", path) if supportIntrospect && path == s"$rootPath/token/introspect" && useJwtToken  =>
        handleBody { body =>
          body.get("token") match {
            case Some(token) => {
              val decoded        = JWT.decode(token)
              val clientId       =
                Try(decoded.getClaim("clientId").asString()).orElse(Try(decoded.getIssuer())).getOrElse("--")
              val possibleApiKey = env.datastores.apiKeyDataStore.findById(clientId)
              possibleApiKey.flatMap {
                case Some(apiKey) => {
                  Try(
                    JWT.require(Algorithm.HMAC512(apiKey.clientSecret)).acceptLeeway(10).build().verify(token)
                  ) match {
                    case Failure(e) =>
                      Results
                        .Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized"))
                        .leftf
                    case Success(_) => Results.Ok(apiKey.lightJson ++ Json.obj("access_type" -> "apikey")).leftf
                  }
                }
                case None         =>
                  // apikey not found
                  Results
                    .Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized"))
                    .leftf
              }
            }
            case _           =>
              // bad body
              Results.Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized")).leftf
          }
        }
      case ("post", path) if supportIntrospect && path == s"$rootPath/token/introspect" && !useJwtToken =>
        handleBody { body =>
          body.get("token") match {
            case Some(token) => {
              val possibleApiKey: Future[Option[ApiKey]] = env.datastores.rawDataStore
                .get(s"${env.storageRoot}:plugins:client-credentials-flow:access-tokens:$token")
                .flatMap {
                  case Some(clientId) => env.datastores.apiKeyDataStore.findById(clientId.utf8String)
                  case _              => None.future
                }
              possibleApiKey.flatMap {
                case Some(apiKey) => Results.Ok(apiKey.lightJson ++ Json.obj("access_type" -> "apikey")).leftf
                case None         =>
                  // apikey not found
                  Results
                    .Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized"))
                    .leftf
              }
            }
            case _           =>
              // bad body
              Results.Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized")).leftf
          }
        }
      case ("post", path) if path == s"$rootPath/token"                                                 => {

        awaitingRequests.get(ctx.snowflake).map { promise =>
          val consumed = new AtomicBoolean(false)

          val bodySource: Source[ByteString, _] = Source
            .future(promise.future)
            .flatMapConcat(s => s)
            .alsoTo(Sink.onComplete { case _ =>
              consumed.set(true)
            })

          def handleTokenRequest(ccfb: ClientCredentialFlowBody): Future[Either[Result, HttpRequest]] =
            ccfb match {
              case ClientCredentialFlowBody("client_credentials", clientId, clientSecret, scope, kind) => {
                val possibleApiKey = env.datastores.apiKeyDataStore.findById(clientId)
                possibleApiKey.flatMap {
                  case Some(apiKey)
                      if (apiKey.clientSecret == clientSecret || apiKey.rotation.nextSecret
                        .contains(clientSecret)) && useJwtToken => {

                    val keyPairId       = apiKey.metadata.get("jwt-sign-keypair").orElse(defaultKeyPair)
                    val signWithKeyPair = _signWithKeyPair && keyPairId.isDefined

                    val maybeKeyPair: Option[KeyPair] = if (signWithKeyPair) {
                      keyPairId.flatMap(id => DynamicSSLEngineProvider.certificates.get(id)).map(_.cryptoKeyPair)
                    } else {
                      None
                    }

                    val algo: Algorithm = maybeKeyPair.map { kp =>
                      (kp.getPublic, kp.getPrivate) match {
                        case (pub: RSAPublicKey, priv: RSAPrivateKey) => Algorithm.RSA256(pub, priv)
                        case (pub: ECPublicKey, priv: ECPrivateKey)   => Algorithm.ECDSA384(pub, priv)
                        case _                                        => Algorithm.HMAC512(apiKey.clientSecret)
                      }
                    } getOrElse {
                      Algorithm.HMAC512(apiKey.clientSecret)
                    }

                    val accessToken = JWT
                      .create()
                      .withJWTId(IdGenerator.uuid)
                      .withExpiresAt(DateTime.now().plus(expiration.toMillis).toDate)
                      .withIssuedAt(DateTime.now().toDate)
                      .withNotBefore(DateTime.now().toDate)
                      .withClaim("cid", apiKey.clientId)
                      .withIssuer(ctx.request.theProtocol + "://" + ctx.request.host)
                      .withSubject(apiKey.clientId)
                      .withAudience("otoroshi")
                      .applyOnIf(signWithKeyPair) { builder =>
                        builder.withKeyId(keyPairId.get)
                      }
                      .sign(algo)
                    // no refresh token possible because of https://tools.ietf.org/html/rfc6749#section-4.4.3

                    val pass = scope.forall { s =>
                      val scopes     = s.split(" ").toSeq
                      val scopeInter =
                        apiKey.metadata.get("scope").exists(_.split(" ").toSeq.intersect(scopes).nonEmpty)
                      scopeInter && apiKey.metadata
                        .get("scope")
                        .map(_.split(" ").toSeq.intersect(scopes).size)
                        .getOrElse(scopes.size) == scopes.size
                    }
                    if (pass) {
                      val scopeObj = scope
                        .orElse(apiKey.metadata.get("scope"))
                        .map(v => Json.obj("scope" -> v))
                        .getOrElse(Json.obj())
                      Results
                        .Ok(
                          Json.obj(
                            "access_token" -> accessToken,
                            "token_type"   -> "Bearer",
                            "expires_in"   -> expiration.toSeconds
                          ) ++ scopeObj
                        )
                        .leftf
                    } else {
                      Results
                        .Forbidden(
                          Json.obj(
                            "error"             -> "access_denied",
                            "error_description" -> s"Client has not been granted scopes: ${scope.get}"
                          )
                        )
                        .leftf
                    }
                  }
                  case Some(apiKey)
                      if (apiKey.clientSecret == clientSecret || apiKey.rotation.nextSecret
                        .contains(clientSecret)) && !useJwtToken => {
                    val randomToken = IdGenerator.token(64)
                    env.datastores.rawDataStore
                      .set(
                        s"${env.storageRoot}:plugins:client-credentials-flow:access-tokens:$randomToken",
                        ByteString(apiKey.clientSecret),
                        Some(expiration.toMillis)
                      )
                      .map { _ =>
                        val pass = scope.forall { s =>
                          val scopes     = s.split(" ").toSeq
                          val scopeInter =
                            apiKey.metadata.get("scope").exists(_.split(" ").toSeq.intersect(scopes).nonEmpty)
                          scopeInter && apiKey.metadata
                            .get("scope")
                            .map(_.split(" ").toSeq.intersect(scopes).size)
                            .getOrElse(scopes.size) == scopes.size
                        }
                        if (pass) {
                          val scopeObj = scope
                            .orElse(apiKey.metadata.get("scope"))
                            .map(v => Json.obj("scope" -> v))
                            .getOrElse(Json.obj())
                          Results
                            .Ok(
                              Json.obj(
                                "access_token" -> randomToken,
                                "token_type"   -> "Bearer",
                                "expires_in"   -> expiration.toSeconds
                              ) ++ scopeObj
                            )
                            .left
                        } else {
                          Results
                            .Forbidden(
                              Json.obj(
                                "error"             -> "access_denied",
                                "error_description" -> s"Client has not been granted scopes: ${scope.get}"
                              )
                            )
                            .left
                        }
                      }
                  }
                  case _ =>
                    Results
                      .Unauthorized(
                        Json.obj("error" -> "access_denied", "error_description" -> s"Bad client credentials")
                      )
                      .leftf
                }
              }
              case _                                                                                   =>
                Results
                  .BadRequest(
                    Json.obj(
                      "error"             -> "unauthorized_client",
                      "error_description" -> s"Grant type '${ccfb.grantType}' not supported !"
                    )
                  )
                  .leftf
            }

          bodySource.runFold(ByteString.empty)(_ ++ _).flatMap { bodyRaw =>
            ctx.request.headers.get("Content-Type") match {
              case Some(ctype) if ctype.toLowerCase().contains("application/x-www-form-urlencoded") => {

                val charset          = ctx.request.charset.getOrElse("UTF-8")
                val urlEncodedString = bodyRaw.utf8String
                val body             = FormUrlEncodedParser.parse(urlEncodedString, charset).mapValues(_.head)
                (
                  body.get("grant_type"),
                  body.get("client_id"),
                  body.get("client_secret"),
                  body.get("scope"),
                  body.get("bearer_kind")
                ) match {
                  case (Some(gtype), Some(clientId), Some(clientSecret), scope, kind) =>
                    handleTokenRequest(
                      ClientCredentialFlowBody(gtype, clientId, clientSecret, scope, kind.getOrElse("jwt"))
                    )
                  case _                                                              =>
                    ctx.request.headers
                      .get("Authorization")
                      .filter(_.startsWith("Basic "))
                      .map(_.replace("Basic ", ""))
                      .map(v => org.apache.commons.codec.binary.Base64.decodeBase64(v))
                      .map(v => new String(v))
                      .filter(_.contains(":"))
                      .map(_.split(":").toSeq)
                      .map(v => (v.head, v.tail.mkString(":")))
                      .map { case (clientId, clientSecret) =>
                        handleTokenRequest(
                          ClientCredentialFlowBody(
                            body.getOrElse("grant_type", "--"),
                            clientId,
                            clientSecret,
                            None,
                            body.getOrElse("bearer_kind", "jwt")
                          )
                        )
                      }
                      .getOrElse {
                        // bad credentials
                        Results
                          .Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized"))
                          .leftf
                      }
                }
              }
              case Some(ctype) if ctype.toLowerCase().contains("application/json") => {
                val json = Json.parse(bodyRaw.utf8String)
                (
                  (json \ "grant_type").asOpt[String],
                  (json \ "client_id").asOpt[String],
                  (json \ "client_secret").asOpt[String],
                  (json \ "scope").asOpt[String],
                  (json \ "bearer_kind").asOpt[String]
                ) match {
                  case (Some(gtype), Some(clientId), Some(clientSecret), scope, kind) =>
                    handleTokenRequest(
                      ClientCredentialFlowBody(gtype, clientId, clientSecret, scope, kind.getOrElse("jwt"))
                    )
                  case _                                                              =>
                    ctx.request.headers
                      .get("Authorization")
                      .filter(_.startsWith("Basic "))
                      .map(_.replace("Basic ", ""))
                      .map(v => org.apache.commons.codec.binary.Base64.decodeBase64(v))
                      .map(v => new String(v))
                      .filter(_.contains(":"))
                      .map(_.split(":").toSeq)
                      .map(v => (v.head, v.tail.mkString(":")))
                      .map { case (clientId, clientSecret) =>
                        handleTokenRequest(
                          ClientCredentialFlowBody(
                            (json \ "grant_type").asOpt[String].getOrElse("--"),
                            clientId,
                            clientSecret,
                            None,
                            (json \ "bearer_kind").asOpt[String].getOrElse("jwt")
                          )
                        )
                      }
                      .getOrElse {
                        // bad credentials
                        Results
                          .Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized"))
                          .leftf
                      }
                }
              }
              case _                                                               =>
                // bad content type
                Results.Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized")).leftf
            }
          } andThen { case _ =>
            if (!consumed.get()) bodySource.runWith(Sink.ignore)
          }
        } getOrElse {
          // no body
          Results.Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized")).leftf
        }
      }
      case _ if autonomous     =>
        Results.NotFound(Json.obj("error" -> "not_found", "error_description" -> s"Resource not found")).leftf
      case _ if !supportRevoke => ctx.otoroshiRequest.rightf
      case _                   => {

        val req            = ctx.request
        val descriptor     = ctx.descriptor
        val authByJwtToken = ctx.request.headers
          .get(
            descriptor.apiKeyConstraints.jwtAuth.headerName
              .getOrElse(env.Headers.OtoroshiBearer)
          )
          .orElse(
            req.headers.get("Authorization").filter(_.startsWith("Bearer "))
          )
          .map(_.replace("Bearer ", ""))
          .orElse(
            req.queryString
              .get(
                descriptor.apiKeyConstraints.jwtAuth.queryName
                  .getOrElse(env.Headers.OtoroshiBearerAuthorization)
              )
              .flatMap(_.lastOption)
          )
          .orElse(
            req.cookies
              .get(
                descriptor.apiKeyConstraints.jwtAuth.cookieName
                  .getOrElse(env.Headers.OtoroshiJWTAuthorization)
              )
              .map(_.value)
          )
        // .filter(_.split("\\.").length == 3)
        authByJwtToken match {
          case None                        =>
            // no token, weird, should not happen
            Results.Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized")).leftf
          case Some(token) if useJwtToken  => {
            val decoded = JWT.decode(token)
            Try(decoded.getId).toOption match {
              case None      =>
                // no jti, weird
                Results.Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized")).leftf
              case Some(jti) =>
                revokedCache
                  .getIfPresent(jti)
                  .map(_.future)
                  .getOrElse(
                    env.datastores.rawDataStore
                      .get(s"${env.storageRoot}:plugins:client-credentials-flow:revoked-tokens:$jti")
                      .map(_.map(_.utf8String.toBoolean).getOrElse(false))
                      .andThen { case Success(b) =>
                        revokedCache.put(jti, b)
                      }
                  )
                  .flatMap {
                    case true  =>
                      // revoked token
                      Results
                        .Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized"))
                        .leftf
                    case false =>
                      ctx.otoroshiRequest.rightf
                  }
            }
          }
          case Some(token) if !useJwtToken => {
            revokedCache
              .getIfPresent(token)
              .map(_.future)
              .getOrElse(
                env.datastores.rawDataStore
                  .get(s"${env.storageRoot}:plugins:client-credentials-flow:revoked-tokens:$token")
                  .map(_.map(_.utf8String.toBoolean).getOrElse(false))
                  .andThen { case Success(b) =>
                    revokedCache.put(token, b)
                  }
              )
              .flatMap {
                case true  =>
                  // revoked token
                  Results
                    .Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized"))
                    .leftf
                case false =>
                  ctx.otoroshiRequest.rightf
              }
          }
        }
      }
    }
  }

  override def transformRequestBodyWithCtx(
      ctx: TransformerRequestBodyContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Source[ByteString, _] = {
    awaitingRequests.get(ctx.snowflake).map(_.trySuccess(ctx.body))
    ctx.body
  }
}

case class BiscuitConf(
    privkey: Option[String] = None,
    checks: Seq[String] = Seq.empty,
    facts: Seq[String] = Seq.empty,
    rules: Seq[String] = Seq.empty
)

// TODO: MIGRATE !
class ClientCredentialService extends RequestSink {

  import otoroshi.utils.http.RequestImplicits._
  import otoroshi.utils.syntax.implicits._

  case class ClientCredentialServiceConfig(raw: JsValue) {
    lazy val expiration     = (raw \ "expiration").asOpt[Long].map(_.millis).getOrElse(1.hour)
    lazy val defaultKeyPair =
      (raw \ "defaultKeyPair").asOpt[String].filter(_.trim.nonEmpty).getOrElse(Cert.OtoroshiJwtSigning)
    lazy val domain         = (raw \ "domain").asOpt[String].filter(_.trim.nonEmpty).getOrElse("*")
    lazy val secure         = (raw \ "secure").asOpt[Boolean].getOrElse(true)
    lazy val biscuit        = (raw \ "biscuit")
      .asOpt[JsObject]
      .map { js =>
        BiscuitConf(
          privkey = (js \ "privkey").asOpt[String],
          checks = (js \ "checks").asOpt[Seq[String]].getOrElse(Seq.empty),
          facts = (js \ "facts").asOpt[Seq[String]].getOrElse(Seq.empty),
          rules = (js \ "rules").asOpt[Seq[String]].getOrElse(Seq.empty)
        )
      }
      .getOrElse(BiscuitConf())
  }

  override def name: String = "Client Credential Service"

  override def defaultConfig: Option[JsObject] = {
    Some(
      Json.obj(
        "ClientCredentialService" -> Json.obj(
          "domain"         -> "*",
          "expiration"     -> 1.hour.toMillis,
          "defaultKeyPair" -> Cert.OtoroshiJwtSigning,
          "secure"         -> true
        )
      )
    )
  }

  override def description: Option[String] = {
    Some(
      s"""This plugin add an an oauth client credentials service (`https://unhandleddomain/.well-known/otoroshi/oauth/token`) to create an access_token given a client id and secret.
         |
         |```json
         |${Json.prettyPrint(defaultConfig.get)}
         |```
      """.stripMargin
    )
  }

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

  override def matches(ctx: RequestSinkContext)(implicit env: Env, ec: ExecutionContext): Boolean = {
    val conf          = ClientCredentialServiceConfig(ctx.configFor("ClientCredentialService"))
    val domainMatches = conf.domain match {
      case "*"   => true
      case value => ctx.request.theDomain == value
    }
    domainMatches && ctx.origin == RequestOrigin.ReverseProxy && ctx.request.relativeUri.startsWith(
      "/.well-known/otoroshi/oauth/"
    )
  }

  private def handleBody(
      ctx: RequestSinkContext
  )(f: Map[String, String] => Future[Result])(implicit env: Env, ec: ExecutionContext): Future[Result] = {
    implicit val mat = env.otoroshiMaterializer
    val charset      = ctx.request.charset.getOrElse("UTF-8")
    ctx.body.runFold(ByteString.empty)(_ ++ _).flatMap { bodyRaw =>
      ctx.request.headers.get("Content-Type") match {
        case Some(ctype) if ctype.toLowerCase().contains("application/x-www-form-urlencoded") => {
          val urlEncodedString         = bodyRaw.utf8String
          val body                     = FormUrlEncodedParser.parse(urlEncodedString, charset).mapValues(_.head)
          val map: Map[String, String] = body ++ ctx.request.headers
            .get("Authorization")
            .filter(_.startsWith("Basic "))
            .map(_.replace("Basic ", ""))
            .map(v => org.apache.commons.codec.binary.Base64.decodeBase64(v))
            .map(v => new String(v))
            .filter(_.contains(":"))
            .map(_.split(":").toSeq)
            .map(v => Map("client_id" -> v.head, "client_secret" -> v.tail.mkString(":")))
            .getOrElse(Map.empty[String, String])
          f(map)
        }
        case Some(ctype) if ctype.toLowerCase().contains("application/json")                  => {
          val json                     = Json.parse(bodyRaw.utf8String).as[JsObject]
          val map: Map[String, String] = json.value.toSeq.collect {
            case (key, JsString(v))  => (key, v)
            case (key, JsNumber(v))  => (key, v.toString())
            case (key, JsBoolean(v)) => (key, v.toString)
          }.toMap ++ ctx.request.headers
            .get("Authorization")
            .filter(_.startsWith("Basic "))
            .map(_.replace("Basic ", ""))
            .map(v => org.apache.commons.codec.binary.Base64.decodeBase64(v))
            .map(v => new String(v))
            .filter(_.contains(":"))
            .map(_.split(":").toSeq)
            .map(v => Map("client_id" -> v.head, "client_secret" -> v.tail.mkString(":")))
            .getOrElse(Map.empty[String, String])
          f(map)
        }
        case _                                                                                =>
          // bad content type
          Results.Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized")).future
      }
    }
  }

  private def jwks(conf: ClientCredentialServiceConfig, ctx: RequestSinkContext)(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[Result] = {
    JWKSHelper.jwks(ctx.request, conf.defaultKeyPair.some.toSeq).map {
      case Left(body)  => Results.NotFound(body)
      case Right(body) => Results.Ok(body)
    }
  }

  private def introspect(conf: ClientCredentialServiceConfig, ctx: RequestSinkContext)(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[Result] = {
    handleBody(ctx) { body =>
      body.get("token") match {
        case Some(token) => {
          val decoded        = JWT.decode(token)
          val clientId       = Try(decoded.getClaim("clientId").asString()).orElse(Try(decoded.getIssuer())).getOrElse("--")
          val possibleApiKey = env.datastores.apiKeyDataStore.findById(clientId)
          possibleApiKey.flatMap {
            case Some(apiKey) => {
              val keyPairId                     = apiKey.metadata.getOrElse("jwt-sign-keypair", conf.defaultKeyPair)
              val maybeKeyPair: Option[KeyPair] =
                DynamicSSLEngineProvider.certificates.get(keyPairId).map(_.cryptoKeyPair)
              val algo: Algorithm               = maybeKeyPair.map { kp =>
                (kp.getPublic, kp.getPrivate) match {
                  case (pub: RSAPublicKey, priv: RSAPrivateKey) => Algorithm.RSA256(pub, priv)
                  case (pub: ECPublicKey, priv: ECPrivateKey)   => Algorithm.ECDSA384(pub, priv)
                  case _                                        => Algorithm.HMAC512(apiKey.clientSecret)
                }
              } getOrElse {
                Algorithm.HMAC512(apiKey.clientSecret)
              }
              Try(JWT.require(algo).acceptLeeway(10).build().verify(token)) match {
                case Failure(e) =>
                  Results
                    .Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized"))
                    .future
                case Success(_) => Results.Ok(apiKey.lightJson ++ Json.obj("access_type" -> "apikey")).future
              }
            }
            case None         =>
              // apikey not found
              Results.Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized")).future
          }
        }
        case _           =>
          // bad body
          Results.Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized")).future
      }
    }
  }

  private def handleTokenRequest(
      ccfb: ClientCredentialFlowBody,
      conf: ClientCredentialServiceConfig,
      ctx: RequestSinkContext
  )(implicit env: Env, ec: ExecutionContext): Future[Result] =
    ccfb match {
      case ClientCredentialFlowBody("client_credentials", clientId, clientSecret, scope, bearerKind) => {
        val possibleApiKey = env.datastores.apiKeyDataStore.findById(clientId)
        possibleApiKey.flatMap {
          case Some(apiKey)
              if (apiKey.clientSecret == clientSecret || apiKey.rotation.nextSecret.contains(
                clientSecret
              )) && bearerKind == "biscuit" => {

            import org.biscuitsec.biscuit.crypto.KeyPair
            import org.biscuitsec.biscuit.token.Biscuit
            import org.biscuitsec.biscuit.token.builder.Block
            import org.biscuitsec.biscuit.token.builder.Utils._

            import collection.JavaConverters._

            val biscuitConf: BiscuitConf = conf.biscuit

            val symbols           = new SymbolTable()
            val authority_builder = new Block(0, symbols)

            authority_builder.add_fact(fact("token_id", Seq(s("authority"), string(IdGenerator.uuid)).asJava))
            authority_builder.add_fact(
              fact("token_exp", Seq(s("authority"), date(DateTime.now().plus(conf.expiration.toMillis).toDate)).asJava)
            )
            authority_builder.add_fact(fact("token_iat", Seq(s("authority"), date(DateTime.now().toDate)).asJava))
            authority_builder.add_fact(fact("token_nbf", Seq(s("authority"), date(DateTime.now().toDate)).asJava))
            authority_builder.add_fact(
              fact("token_iss", Seq(s("authority"), string(ctx.request.theProtocol + "://" + ctx.request.host)).asJava)
            )
            authority_builder.add_fact(fact("token_aud", Seq(s("authority"), s("otoroshi")).asJava))
            authority_builder.add_fact(fact("client_id", Seq(s("authority"), string(apiKey.clientId)).asJava))
            authority_builder.add_fact(
              fact(
                "client_sign",
                Seq(s("authority"), string(Signatures.hmacSha256Sign(apiKey.clientId, apiKey.clientSecret))).asJava
              )
            )

            biscuitConf.checks
              .map(Parser.check)
              .filter(_.isRight)
              .map(_.get()._2)
              .foreach(r => authority_builder.add_check(r))
            biscuitConf.facts
              .map(Parser.fact)
              .filter(_.isRight)
              .map(_.get()._2)
              .foreach(r => authority_builder.add_fact(r))
            biscuitConf.rules
              .map(Parser.rule)
              .filter(_.isRight)
              .map(_.get()._2)
              .foreach(r => authority_builder.add_rule(r))

            def fromApiKey(name: String): Seq[String] =
              apiKey.metadata.get(name).map(Json.parse).map(_.asArray.value.map(_.asString)).getOrElse(Seq.empty)

            fromApiKey("biscuit_checks")
              .map(Parser.check)
              .filter(_.isRight)
              .map(_.get()._2)
              .foreach(r => authority_builder.add_check(r))
            fromApiKey("biscuit_facts")
              .map(Parser.fact)
              .filter(_.isRight)
              .map(_.get()._2)
              .foreach(r => authority_builder.add_fact(r))
            fromApiKey("biscuit_rules")
              .map(Parser.rule)
              .filter(_.isRight)
              .map(_.get()._2)
              .foreach(r => authority_builder.add_rule(r))

            val accessToken: String = {
              val privKeyValue = apiKey.metadata.get("biscuit_pubkey").orElse(biscuitConf.privkey)
              val keypair      = new KeyPair(privKeyValue.get)
              val rng          = new SecureRandom()
              Biscuit
                .make(rng, keypair, symbols, authority_builder.build())
                .serialize_b64url()
            }

            val pass = scope.forall { s =>
              val scopes     = s.split(" ").toSeq
              val scopeInter = apiKey.metadata.get("scope").exists(_.split(" ").toSeq.intersect(scopes).nonEmpty)
              scopeInter && apiKey.metadata
                .get("scope")
                .map(_.split(" ").toSeq.intersect(scopes).size)
                .getOrElse(scopes.size) == scopes.size
            }
            if (pass) {
              val scopeObj =
                scope.orElse(apiKey.metadata.get("scope")).map(v => Json.obj("scope" -> v)).getOrElse(Json.obj())
              Results
                .Ok(
                  Json.obj(
                    "access_token" -> accessToken,
                    "token_type"   -> "Bearer",
                    "expires_in"   -> conf.expiration.toSeconds
                  ) ++ scopeObj
                )
                .future
            } else {
              Results
                .Forbidden(
                  Json.obj(
                    "error"             -> "access_denied",
                    "error_description" -> s"Client has not been granted scopes: ${scope.get}"
                  )
                )
                .future
            }
          }
          case Some(apiKey)
              if apiKey.clientSecret == clientSecret || apiKey.rotation.nextSecret.contains(clientSecret) => {
            val keyPairId                     = apiKey.metadata.getOrElse("jwt-sign-keypair", conf.defaultKeyPair)
            val maybeKeyPair: Option[KeyPair] =
              DynamicSSLEngineProvider.certificates.get(keyPairId).map(_.cryptoKeyPair)
            val algo: Algorithm               = maybeKeyPair.map { kp =>
              (kp.getPublic, kp.getPrivate) match {
                case (pub: RSAPublicKey, priv: RSAPrivateKey) => Algorithm.RSA256(pub, priv)
                case (pub: ECPublicKey, priv: ECPrivateKey)   => Algorithm.ECDSA384(pub, priv)
                case _                                        => Algorithm.HMAC512(apiKey.clientSecret)
              }
            } getOrElse {
              Algorithm.HMAC512(apiKey.clientSecret)
            }

            val accessToken = JWT
              .create()
              .withJWTId(IdGenerator.uuid)
              .withExpiresAt(DateTime.now().plus(conf.expiration.toMillis).toDate)
              .withIssuedAt(DateTime.now().toDate)
              .withNotBefore(DateTime.now().toDate)
              .withClaim("cid", apiKey.clientId)
              .withIssuer(ctx.request.theProtocol + "://" + ctx.request.host)
              .withSubject(apiKey.clientId)
              .withAudience("otoroshi")
              .withKeyId(keyPairId)
              .sign(algo)
            // no refresh token possible because of https://tools.ietf.org/html/rfc6749#section-4.4.3

            val pass = scope.forall { s =>
              val scopes     = s.split(" ").toSeq
              val scopeInter = apiKey.metadata.get("scope").exists(_.split(" ").toSeq.intersect(scopes).nonEmpty)
              scopeInter && apiKey.metadata
                .get("scope")
                .map(_.split(" ").toSeq.intersect(scopes).size)
                .getOrElse(scopes.size) == scopes.size
            }
            if (pass) {
              val scopeObj =
                scope.orElse(apiKey.metadata.get("scope")).map(v => Json.obj("scope" -> v)).getOrElse(Json.obj())
              Results
                .Ok(
                  Json.obj(
                    "access_token" -> accessToken,
                    "token_type"   -> "Bearer",
                    "expires_in"   -> conf.expiration.toSeconds
                  ) ++ scopeObj
                )
                .future
            } else {
              Results
                .Forbidden(
                  Json.obj(
                    "error"             -> "access_denied",
                    "error_description" -> s"Client has not been granted scopes: ${scope.get}"
                  )
                )
                .future
            }
          }
          case _ =>
            Results
              .Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Bad client credentials"))
              .future
        }
      }
      case _                                                                                         =>
        Results
          .BadRequest(
            Json.obj(
              "error"             -> "unauthorized_client",
              "error_description" -> s"Grant type '${ccfb.grantType}' not supported !"
            )
          )
          .future
    }

  private def token(conf: ClientCredentialServiceConfig, ctx: RequestSinkContext)(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[Result] =
    handleBody(ctx) { body =>
      (
        body.get("grant_type"),
        body.get("client_id"),
        body.get("client_secret"),
        body.get("scope"),
        body.get("bearer_kind")
      ) match {
        case (Some(gtype), Some(clientId), Some(clientSecret), scope, kind) =>
          handleTokenRequest(
            ClientCredentialFlowBody(gtype, clientId, clientSecret, scope, kind.getOrElse("jwt")),
            conf,
            ctx
          )
        case _                                                              =>
          ctx.request.headers
            .get("Authorization")
            .filter(_.startsWith("Basic "))
            .map(_.replace("Basic ", ""))
            .map(v => org.apache.commons.codec.binary.Base64.decodeBase64(v))
            .map(v => new String(v))
            .filter(_.contains(":"))
            .map(_.split(":").toSeq)
            .map(v => (v.head, v.tail.mkString(":")))
            .map { case (clientId, clientSecret) =>
              handleTokenRequest(
                ClientCredentialFlowBody(
                  body.getOrElse("grant_type", "--"),
                  clientId,
                  clientSecret,
                  None,
                  body.getOrElse("bearer_kind", "jwt")
                ),
                conf,
                ctx
              )
            }
            .getOrElse {
              // bad credentials
              Results.Unauthorized(Json.obj("error" -> "access_denied", "error_description" -> s"Unauthorized")).future
            }
      }
    }

  override def handle(ctx: RequestSinkContext)(implicit env: Env, ec: ExecutionContext): Future[Result] = {
    val conf        = ClientCredentialServiceConfig(ctx.configFor("ClientCredentialService"))
    val secureMatch = if (conf.secure) ctx.request.theSecured else true
    if (secureMatch) {
      (ctx.request.method.toLowerCase(), ctx.request.relativeUri) match {
        case ("get", "/.well-known/otoroshi/oauth/jwks.json")         => jwks(conf, ctx)
        case ("post", "/.well-known/otoroshi/oauth/token/introspect") => introspect(conf, ctx)
        case ("post", "/.well-known/otoroshi/oauth/token")            => token(conf, ctx)
        case _                                                        =>
          Results.NotFound(Json.obj("error" -> "not_found", "error_description" -> s"resource not found")).future
      }
    } else {
      Results.BadRequest(Json.obj("error" -> "bad_request", "error_description" -> s"use a secure channel")).future
    }
  }
}

import scala.concurrent.{ExecutionContext, Future}

// MIGRATED
class ApikeyAuthModule extends PreRouting {

  override def name: String = "Apikey auth module"

  override def defaultConfig: Option[JsObject] = {
    Some(
      Json.obj(
        "ApikeyAuthModule" -> Json.obj(
          "realm"          -> "apikey-auth-module-realm",
          "noneTagIn"      -> Json.arr(),
          "oneTagIn"       -> Json.arr(),
          "allTagsIn"      -> Json.arr(),
          "noneMetaIn"     -> Json.arr(),
          "oneMetaIn"      -> Json.arr(),
          "allMetaIn"      -> Json.arr(),
          "noneMetaKeysIn" -> Json.arr(),
          "oneMetaKeyIn"   -> Json.arr(),
          "allMetaKeysIn"  -> Json.arr()
        )
      )
    )
  }

  override def description: Option[String] = {
    Some(
      s"""This plugin adds basic auth on service where credentials are valid apikeys on the current service.
         |
         |```json
         |${Json.prettyPrint(defaultConfig.get)}
         |```
      """.stripMargin
    )
  }

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

  def decodeBase64(encoded: String): String = new String(OtoroshiClaim.decoder.decode(encoded), Charsets.UTF_8)

  def extractUsernamePassword(header: String): Option[(String, String)] = {
    val base64 = header.replace("Basic ", "").replace("basic ", "")
    Option(base64)
      .map(decodeBase64)
      .map(_.split(":").toSeq)
      .filter(v => v.nonEmpty && v.length > 1)
      .flatMap(a => a.headOption.map(head => (head, a.tail.mkString(":"))))
  }

  def unauthorized(ctx: PreRoutingContext): Future[Unit] = {
    val realm = ctx.configFor("ApikeyAuthModule").select("realm").asOpt[String].getOrElse("apikey-auth-module-realm")
    FastFuture.failed(
      PreRoutingError(
        body = "

not authorized

".byteString, code = 401, contentType = "text/html", headers = Map("WWW-Authenticate" -> s"""Basic realm="${realm}"""") ) ) } def forbidden(ctx: PreRoutingContext): Future[Unit] = { val realm = ctx.configFor("ApikeyAuthModule").select("realm").asOpt[String].getOrElse("apikey-auth-module-realm") FastFuture.failed( PreRoutingError( body = "

forbidden

".byteString, code = 403, contentType = "text/html", headers = Map("WWW-Authenticate" -> s"""Basic realm="${realm}"""") ) ) } def validApikey(apikey: ApiKey, password: String, groups: Seq[ServiceGroupIdentifier], config: JsValue): Boolean = { import otoroshi.models.SeqImplicits._ val validSecret = apikey.clientSecret == password || (apikey.rotation.enabled && apikey.rotation.nextSecret.contains(password)) val validGroups = apikey.authorizedEntities.intersect(groups).nonEmpty val routing = ApiKeyRouteMatcher.format.reads(config).getOrElse(ApiKeyRouteMatcher()) val matchOnRole: Boolean = Option(routing.oneTagIn) .filter(_.nonEmpty) .forall(tags => apikey.tags.findOne(tags)) val matchAllRoles: Boolean = Option(routing.allTagsIn) .filter(_.nonEmpty) .forall(tags => apikey.tags.findAll(tags)) val matchNoneRole: Boolean = !Option(routing.noneTagIn) .filter(_.nonEmpty) .exists(tags => apikey.tags.findOne(tags)) val matchOneMeta: Boolean = Option(routing.oneMetaIn.toSeq) .filter(_.nonEmpty) .forall(metas => apikey.metadata.toSeq.findOne(metas)) val matchAllMeta: Boolean = Option(routing.allMetaIn.toSeq) .filter(_.nonEmpty) .forall(metas => apikey.metadata.toSeq.findAll(metas)) val matchNoneMeta: Boolean = !Option(routing.noneMetaIn.toSeq) .filter(_.nonEmpty) .exists(metas => apikey.metadata.toSeq.findOne(metas)) val matchOneMetakeys: Boolean = Option(routing.oneMetaKeyIn) .filter(_.nonEmpty) .forall(keys => apikey.metadata.toSeq.map(_._1).findOne(keys)) val matchAllMetaKeys: Boolean = Option(routing.allMetaKeysIn) .filter(_.nonEmpty) .forall(keys => apikey.metadata.toSeq.map(_._1).findAll(keys)) val matchNoneMetaKeys: Boolean = !Option(routing.noneMetaKeysIn) .filter(_.nonEmpty) .exists(keys => apikey.metadata.toSeq.map(_._1).findOne(keys)) val result = Seq( matchOnRole, matchAllRoles, matchNoneRole, matchOneMeta, matchAllMeta, matchNoneMeta, matchOneMetakeys, matchAllMetaKeys, matchNoneMetaKeys ) .forall(bool => bool) result } override def preRoute(ctx: PreRoutingContext)(implicit env: Env, ec: ExecutionContext): Future[Unit] = { ctx.request.headers.get("Authorization") match { case Some(auth) if auth.startsWith("Basic ") => extractUsernamePassword(auth) match { case None => forbidden(ctx) case Some((username, password)) => { val groups = ctx.descriptor.groups.map(g => ServiceGroupIdentifier(g)) env.datastores.apiKeyDataStore.findById(username).flatMap { case Some(apikey) if validApikey(apikey, password, groups, ctx.configFor("ApikeyAuthModule")) => { ctx.attrs.put(otoroshi.plugins.Keys.ApiKeyKey -> apikey) ().future } case _ => unauthorized(ctx) } } } case _ => unauthorized(ctx) } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy