models.apikey.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otoroshi_2.12 Show documentation
Show all versions of otoroshi_2.12 Show documentation
Lightweight api management on top of a modern http reverse proxy
The newest version!
package otoroshi.models
import java.security.interfaces.{ECPrivateKey, ECPublicKey, RSAPrivateKey, RSAPublicKey}
import akka.http.scaladsl.util.FastFuture
import akka.stream.scaladsl.Flow
import akka.util.ByteString
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.interfaces.DecodedJWT
import com.google.common.base.Charsets
import com.google.common.hash.Hashing
import otoroshi.env.Env
import otoroshi.events.{
Alerts,
ApiKeyQuotasAlmostExceededAlert,
ApiKeyQuotasAlmostExceededReason,
ApiKeyQuotasExceededAlert,
ApiKeyQuotasExceededReason,
ApiKeySecretHasRotated,
ApiKeySecretWillRotate,
RevokedApiKeyUsageAlert
}
import otoroshi.gateway.Errors
import org.joda.time.DateTime
import otoroshi.next.plugins.api.NgAccess
import play.api.Logger
import play.api.libs.json._
import play.api.mvc.Results.{BadGateway, BadRequest, NotFound, TooManyRequests, Unauthorized}
import play.api.mvc.{RequestHeader, Result, Results}
import otoroshi.security.{IdGenerator, OtoroshiClaim}
import otoroshi.storage.BasicStore
import otoroshi.utils.TypedMap
import otoroshi.ssl.DynamicSSLEngineProvider
import otoroshi.utils.syntax.implicits.{
BetterDecodedJWT,
BetterJsLookupResult,
BetterJsReadable,
BetterJsValue,
BetterSyntax
}
import java.security.Signature
import scala.concurrent.{ExecutionContext, Future}
import scala.jdk.CollectionConverters.asScalaBufferConverter
import scala.util.{Failure, Success, Try}
case class RemainingQuotas(
// secCalls: Long = RemainingQuotas.MaxValue,
// secCallsRemaining: Long = RemainingQuotas.MaxValue,
// dailyCalls: Long = RemainingQuotas.MaxValue,
// dailyCallsRemaining: Long = RemainingQuotas.MaxValue,
// monthlyCalls: Long = RemainingQuotas.MaxValue,
// monthlyCallsRemaining: Long = RemainingQuotas.MaxValue
authorizedCallsPerSec: Long = RemainingQuotas.MaxValue,
currentCallsPerSec: Long = RemainingQuotas.MaxValue,
remainingCallsPerSec: Long = RemainingQuotas.MaxValue,
authorizedCallsPerDay: Long = RemainingQuotas.MaxValue,
currentCallsPerDay: Long = RemainingQuotas.MaxValue,
remainingCallsPerDay: Long = RemainingQuotas.MaxValue,
authorizedCallsPerMonth: Long = RemainingQuotas.MaxValue,
currentCallsPerMonth: Long = RemainingQuotas.MaxValue,
remainingCallsPerMonth: Long = RemainingQuotas.MaxValue
) {
def toJson: JsObject = RemainingQuotas.fmt.writes(this)
}
object RemainingQuotas {
val MaxValue: Long = 10000000L
implicit val fmt = Json.format[RemainingQuotas]
}
case class ApiKeyRotation(
enabled: Boolean = false,
rotationEvery: Long = 31 * 24,
gracePeriod: Long = 7 * 24,
nextSecret: Option[String] = None
) {
def json: JsValue = ApiKeyRotation.fmt.writes(this)
}
case class ApiKeyRotationInfo(rotationAt: DateTime, remaining: Long) {
def json: JsValue = Json.obj(
"remaining" -> remaining,
"rotation_at" -> rotationAt.toString()
)
}
object ApiKeyRotation {
val fmt = new Format[ApiKeyRotation] {
override def writes(o: ApiKeyRotation): JsValue =
Json.obj(
"enabled" -> o.enabled,
"rotationEvery" -> o.rotationEvery,
"gracePeriod" -> o.gracePeriod,
"nextSecret" -> o.nextSecret.map(JsString.apply).getOrElse(JsNull).as[JsValue]
)
override def reads(json: JsValue): JsResult[ApiKeyRotation] =
Try {
ApiKeyRotation(
enabled = (json \ "enabled").asOpt[Boolean].getOrElse(false),
rotationEvery = (json \ "rotationEvery").asOpt[Long].getOrElse(31 * 24),
gracePeriod = (json \ "gracePeriod").asOpt[Long].getOrElse(7 * 24),
nextSecret = (json \ "nextSecret").asOpt[String]
)
} match {
case Failure(e) => JsError(e.getMessage)
case Success(apkr) => JsSuccess(apkr)
}
}
}
object EntityIdentifier {
def applyModern(json: JsObject): Option[EntityIdentifier] = {
val id = json.select("id").asString
json.select("kind").asOpt[String] match {
case Some("group") => ServiceGroupIdentifier(id).some
case Some("service") => ServiceDescriptorIdentifier(id).some
case Some("route-composition") => RouteCompositionIdentifier(id).some
case Some("route") => RouteIdentifier(id).some
case _ => ServiceGroupIdentifier(id).some // should be None but could be useful for backward compatibility
}
}
def apply(prefixedId: String): Option[EntityIdentifier] = {
prefixedId match {
case id if id.startsWith("group_") => Some(ServiceGroupIdentifier(id.replaceFirst("group_", "")))
case id if id.startsWith("service_") => Some(ServiceDescriptorIdentifier(id.replaceFirst("service_", "")))
case id if id.startsWith("route-composition_") =>
Some(RouteCompositionIdentifier(id.replaceFirst("route-composition_", "")))
case id if id.startsWith("route") => Some(RouteIdentifier(id.replaceFirst("route_", "")))
case id => Some(ServiceGroupIdentifier(id)) // should be None but could be useful for backward compatibility
}
}
}
sealed trait EntityIdentifier {
def prefix: String
def id: String
def str: String = s"${prefix}_${id}"
def json: JsValue = JsString(str)
def modernJson: JsValue = Json.obj("kind" -> prefix, "id" -> id)
}
case class ServiceGroupIdentifier(id: String) extends EntityIdentifier {
def prefix: String = "group"
}
case class ServiceDescriptorIdentifier(id: String) extends EntityIdentifier {
def prefix: String = "service"
}
case class RouteIdentifier(id: String) extends EntityIdentifier {
def prefix: String = "route"
}
case class RouteCompositionIdentifier(id: String) extends EntityIdentifier {
def prefix: String = "route-composition"
}
case class ApiKey(
clientId: String, // = IdGenerator.token(16),
clientSecret: String, // = IdGenerator.token(64),
clientName: String,
description: String = "",
authorizedEntities: Seq[EntityIdentifier],
enabled: Boolean = true,
readOnly: Boolean = false,
allowClientIdOnly: Boolean = false,
throttlingQuota: Long = RemainingQuotas.MaxValue,
dailyQuota: Long = RemainingQuotas.MaxValue,
monthlyQuota: Long = RemainingQuotas.MaxValue,
constrainedServicesOnly: Boolean = false,
restrictions: Restrictions = Restrictions(),
validUntil: Option[DateTime] = None,
rotation: ApiKeyRotation = ApiKeyRotation(),
tags: Seq[String] = Seq.empty[String],
metadata: Map[String, String] = Map.empty[String, String],
location: otoroshi.models.EntityLocation = otoroshi.models.EntityLocation()
) extends otoroshi.models.EntityLocationSupport {
def json: JsValue = toJson
def internalId: String = clientId
def theDescription: String = description
def theMetadata: Map[String, String] = metadata
def theName: String = clientName
def theTags: Seq[String] = tags
def save()(implicit ec: ExecutionContext, env: Env) = env.datastores.apiKeyDataStore.set(this)
def delete()(implicit ec: ExecutionContext, env: Env) = env.datastores.apiKeyDataStore.delete(this)
def exists()(implicit ec: ExecutionContext, env: Env) = env.datastores.apiKeyDataStore.exists(this)
def toJson = ApiKey.toJson(this)
def isActive(): Boolean = enabled && validUntil.map(date => date.isBeforeNow).getOrElse(true)
def isInactive(): Boolean = !isActive()
def isValid(value: String): Boolean =
enabled && ((value == clientSecret) || (rotation.enabled && rotation.nextSecret.contains(value)))
def isInvalid(value: String): Boolean = !isValid(value)
def authorizedOn(identifier: EntityIdentifier): Boolean = authorizedEntities.contains(identifier)
def authorizedOnService(id: String): Boolean = authorizedEntities.contains(ServiceDescriptorIdentifier(id))
def authorizedOnGroup(id: String): Boolean = authorizedEntities.contains(ServiceGroupIdentifier(id))
def authorizedOnRoute(id: String): Boolean = authorizedEntities.contains(RouteIdentifier(id))
def authorizedOnRouteComposition(id: String): Boolean = authorizedEntities.contains(RouteCompositionIdentifier(id))
def authorizedOnOneGroupFrom(ids: Seq[String]): Boolean = {
val identifiers = ids.map(ServiceGroupIdentifier.apply)
authorizedEntities.exists(e => identifiers.contains(e))
}
def authorizedOnServiceOrGroups(service: String, groups: Seq[String]): Boolean = {
authorizedOnService(service) || authorizedOnRoute(service) || authorizedOnRouteComposition(service) || {
val identifiers = groups.map(ServiceGroupIdentifier.apply)
authorizedEntities.exists(e => identifiers.contains(e))
}
}
// def services(implicit ec: ExecutionContext, env: Env): Future[Seq[ServiceDescriptor]] = {
// FastFuture
// .sequence(authorizedEntities.map {
// case ServiceDescriptorIdentifier(id) => env.datastores.serviceDescriptorDataStore.findById(id).map(_.toSeq)
// case ServiceGroupIdentifier(id) =>
// env.datastores.serviceGroupDataStore.findById(id).flatMap {
// case Some(group) => group.services
// case None => FastFuture.successful(Seq.empty[ServiceDescriptor])
// }
// })
// .map(_.flatten)
// }
def updateQuotas()(implicit ec: ExecutionContext, env: Env): Future[RemainingQuotas] =
env.datastores.apiKeyDataStore.updateQuotas(this)
def remainingQuotas()(implicit ec: ExecutionContext, env: Env): Future[RemainingQuotas] =
env.datastores.apiKeyDataStore.remainingQuotas(this)
def withinThrottlingQuota()(implicit ec: ExecutionContext, env: Env): Future[Boolean] =
env.datastores.apiKeyDataStore.withinThrottlingQuota(this)
def withinDailyQuota()(implicit ec: ExecutionContext, env: Env): Future[Boolean] =
env.datastores.apiKeyDataStore.withinDailyQuota(this)
def withinMonthlyQuota()(implicit ec: ExecutionContext, env: Env): Future[Boolean] =
env.datastores.apiKeyDataStore.withinMonthlyQuota(this)
def withinQuotas()(implicit ec: ExecutionContext, env: Env): Future[Boolean] =
env.datastores.apiKeyDataStore.withingQuotas(this)
def withinQuotasAndRotation()(implicit
ec: ExecutionContext,
env: Env
): Future[(Boolean, Option[ApiKeyRotationInfo])] = {
for {
within <- env.datastores.apiKeyDataStore.withingQuotas(this)
rotation <- env.datastores.apiKeyDataStore.keyRotation(this)
} yield (within, rotation)
}
def withinQuotasAndRotationQuotas()(implicit
ec: ExecutionContext,
env: Env
): Future[(Boolean, Option[ApiKeyRotationInfo], RemainingQuotas)] = {
for {
quotas <- env.datastores.apiKeyDataStore.remainingQuotas(this)
rotation <- env.datastores.apiKeyDataStore.keyRotation(this)
} yield {
val within = (quotas.currentCallsPerSec <= (throttlingQuota * env.throttlingWindow)) &&
(quotas.currentCallsPerDay < dailyQuota) &&
(quotas.currentCallsPerMonth < monthlyQuota)
(within, rotation, quotas)
}
}
def metadataJson: JsValue = JsObject(metadata.mapValues(JsString.apply))
def lightJson: JsObject =
Json.obj(
"clientId" -> clientId,
"clientName" -> clientName,
"metadata" -> metadata,
"tags" -> tags
)
def matchRouting(sr: ServiceDescriptor): Boolean = matchRouting(sr.apiKeyConstraints)
def matchRouting(apiKeyConstraints: ApiKeyConstraints): Boolean = matchRouting(apiKeyConstraints.routing)
def matchRouting(routing: ApiKeyRouteMatcher): Boolean = {
import SeqImplicits._
val shouldNotSearchForAnApiKey = routing.hasNoRoutingConstraints
if (shouldNotSearchForAnApiKey && constrainedServicesOnly) {
false
} else if (shouldNotSearchForAnApiKey) {
true
} else {
val matchOnRole: Boolean = Option(routing.oneTagIn)
.filter(_.nonEmpty)
.forall(tags => this.tags.findOne(tags))
val matchAllRoles: Boolean = Option(routing.allTagsIn)
.filter(_.nonEmpty)
.forall(tags => this.tags.findAll(tags))
val matchNoneRole: Boolean = !Option(routing.noneTagIn)
.filter(_.nonEmpty)
.exists(tags => this.tags.findOne(tags))
val matchOneMeta: Boolean = Option(routing.oneMetaIn.toSeq)
.filter(_.nonEmpty)
.forall(metas => this.metadata.toSeq.findOne(metas))
val matchAllMeta: Boolean = Option(routing.allMetaIn.toSeq)
.filter(_.nonEmpty)
.forall(metas => this.metadata.toSeq.findAll(metas))
val matchNoneMeta: Boolean = !Option(routing.noneMetaIn.toSeq)
.filter(_.nonEmpty)
.exists(metas => this.metadata.toSeq.findOne(metas))
val matchOneMetakeys: Boolean = Option(routing.oneMetaKeyIn)
.filter(_.nonEmpty)
.forall(keys => this.metadata.toSeq.map(_._1).findOne(keys))
val matchAllMetaKeys: Boolean = Option(routing.allMetaKeysIn)
.filter(_.nonEmpty)
.forall(keys => this.metadata.toSeq.map(_._1).findAll(keys))
val matchNoneMetaKeys: Boolean = !Option(routing.noneMetaKeysIn)
.filter(_.nonEmpty)
.exists(keys => this.metadata.toSeq.map(_._1).findOne(keys))
val result = Seq(
matchOnRole,
matchAllRoles,
matchNoneRole,
matchOneMeta,
matchAllMeta,
matchNoneMeta,
matchOneMetakeys,
matchAllMetaKeys,
matchNoneMetaKeys
)
.forall(bool => bool)
result
}
}
private def getSignedPrefix(): (String, String) = {
val prefix = getPrefix()
val signature = Hashing.hmacSha256(clientSecret.getBytes("utf-8")).hashBytes(prefix.getBytes("utf-8")).toString
(prefix, signature)
}
private def getNextSignedPrefix(): (String, String) = {
val prefix = getPrefix()
val signature = Hashing
.hmacSha256(rotation.nextSecret.getOrElse(clientSecret).getBytes("utf-8"))
.hashBytes(prefix.getBytes("utf-8"))
.toString
(prefix, signature)
}
private def getPrefix(): String = {
s"otoapk_${clientId}"
}
def toBearer(): String = {
val (prefix, signature) = getSignedPrefix()
s"${prefix}_${signature}"
}
def toNextBearer(): String = {
val (prefix, signature) = getNextSignedPrefix()
s"${prefix}_${signature}"
}
def checkBearer(value: String): Boolean = {
val bearer = toBearer()
lazy val bearer2 = toNextBearer()
value == bearer || value == bearer2
}
}
class ServiceNotFoundException(serviceId: String)
extends RuntimeException(s"Service descriptor with id: '$serviceId' does not exist")
class GroupNotFoundException(groupId: String)
extends RuntimeException(s"Service group with id: '$groupId' does not exist")
object ApiKey {
lazy val logger = Logger("otoroshi-apkikey")
val _fmt: Format[ApiKey] = new Format[ApiKey] {
override def writes(apk: ApiKey): JsValue = {
val enabled = apk.validUntil match {
case Some(date) if date.isBeforeNow => false
case _ => apk.enabled
}
val authGroup: JsValue = apk.authorizedEntities
.find {
case ServiceGroupIdentifier(_) => true
case _ => false
}
.map(_.id)
.map(JsString.apply)
.getOrElse(JsNull) // simulate old behavior
apk.location.jsonWithKey ++ Json.obj(
"clientId" -> apk.clientId,
"clientSecret" -> apk.clientSecret,
"clientName" -> apk.clientName,
"description" -> apk.description,
"authorizedGroup" -> authGroup,
"authorizedEntities" -> JsArray(apk.authorizedEntities.map(_.json)),
"authorizations" -> JsArray(apk.authorizedEntities.map(_.modernJson)),
"enabled" -> enabled, //apk.enabled,
"readOnly" -> apk.readOnly,
"allowClientIdOnly" -> apk.allowClientIdOnly,
"throttlingQuota" -> apk.throttlingQuota,
"dailyQuota" -> apk.dailyQuota,
"monthlyQuota" -> apk.monthlyQuota,
"constrainedServicesOnly" -> apk.constrainedServicesOnly,
"restrictions" -> apk.restrictions.json,
"rotation" -> apk.rotation.json,
"validUntil" -> apk.validUntil.map(v => JsNumber(v.toDate.getTime)).getOrElse(JsNull).as[JsValue],
"tags" -> JsArray(apk.tags.map(JsString.apply)),
"metadata" -> JsObject(apk.metadata.filter(_._1.nonEmpty).mapValues(JsString.apply))
)
}
override def reads(json: JsValue): JsResult[ApiKey] =
Try {
val rawEnabled = (json \ "enabled").asOpt[Boolean].getOrElse(true)
val rawValidUntil = (json \ "validUntil").asOpt[Long].map(l => new DateTime(l))
val enabled = rawValidUntil match {
case Some(date) if date.isBeforeNow => false
case _ => rawEnabled
}
for (
clientId <- (json \ "clientId").asOpt[String].filterNot(_.isBlank);
clientSecret <- (json \ "clientSecret").asOpt[String].filterNot(_.isBlank);
clientName <- (json \ "clientName").asOpt[String].filterNot(_.isBlank)
)
yield ApiKey(
location = otoroshi.models.EntityLocation.readFromKey(json),
clientId = clientId,
clientSecret = clientSecret,
clientName = clientName,
description = (json \ "description").asOpt[String].getOrElse(""),
authorizedEntities = {
val authorizations: Seq[EntityIdentifier] = json
.select("authorizations")
.asOpt[Seq[JsValue]]
.map { values =>
values
.collect {
case JsString(value) => EntityIdentifier.apply(value)
case value @ JsObject(_) => EntityIdentifier.applyModern(value)
}
.collect { case Some(id) =>
id
}
}
.getOrElse(Seq.empty[EntityIdentifier])
val authorizedGroup: Seq[EntityIdentifier] =
(json \ "authorizedGroup").asOpt[String].map(ServiceGroupIdentifier.apply).toSeq
val authorizedEntities: Seq[EntityIdentifier] =
(json \ "authorizedEntities")
.asOpt[Seq[String]]
.map { identifiers =>
identifiers.map(EntityIdentifier.apply).collect { case Some(id) =>
id
}
}
.getOrElse(Seq.empty[EntityIdentifier])
(authorizations ++ authorizedEntities ++ authorizedGroup).distinct
},
enabled = enabled,
readOnly = (json \ "readOnly").asOpt[Boolean].getOrElse(false),
allowClientIdOnly = (json \ "allowClientIdOnly").asOpt[Boolean].getOrElse(false),
throttlingQuota = (json \ "throttlingQuota").asOpt[Long].getOrElse(RemainingQuotas.MaxValue),
dailyQuota = (json \ "dailyQuota").asOpt[Long].getOrElse(RemainingQuotas.MaxValue),
monthlyQuota = (json \ "monthlyQuota").asOpt[Long].getOrElse(RemainingQuotas.MaxValue),
constrainedServicesOnly = (json \ "constrainedServicesOnly").asOpt[Boolean].getOrElse(false),
restrictions = Restrictions.format
.reads((json \ "restrictions").asOpt[JsValue].getOrElse(JsNull))
.getOrElse(Restrictions()),
rotation = ApiKeyRotation.fmt
.reads((json \ "rotation").asOpt[JsValue].getOrElse(JsNull))
.getOrElse(ApiKeyRotation()),
validUntil = rawValidUntil,
tags = (json \ "tags").asOpt[Seq[String]].getOrElse(Seq.empty[String]),
metadata = (json \ "metadata")
.asOpt[Map[String, String]]
.map(m => m.filter(_._1.nonEmpty))
.getOrElse(Map.empty[String, String])
)
} map { case sd =>
JsSuccess(sd.get)
} recover { case t =>
logger.error("Error while reading ApiKey", t)
JsError(t.getMessage)
} get
}
def toJson(value: ApiKey): JsValue = _fmt.writes(value)
def fromJsons(value: JsValue): ApiKey =
try {
_fmt.reads(value).get
} catch {
case e: Throwable => {
logger.error(s"Try to deserialize ${Json.prettyPrint(value)}")
throw e
}
}
def fromJsonSafe(value: JsValue): JsResult[ApiKey] = _fmt.reads(value)
}
trait ApiKeyDataStore extends BasicStore[ApiKey] {
def initiateNewApiKey(groupId: String, env: Env): ApiKey = {
val defaultApikey = ApiKey(
clientId = IdGenerator.lowerCaseToken(16),
clientSecret = IdGenerator.lowerCaseToken(64),
clientName = "client-name-apikey",
authorizedEntities = Seq(ServiceGroupIdentifier(groupId))
)
env.datastores.globalConfigDataStore
.latest()(env.otoroshiExecutionContext, env)
.templates
.apikey
.map { template =>
ApiKey._fmt.reads(defaultApikey.json.asObject.deepMerge(template)).get
}
.getOrElse {
defaultApikey
}
}
def template(env: Env): ApiKey = {
val defaultApikey = ApiKey(
clientId = IdGenerator.lowerCaseToken(16),
clientSecret = IdGenerator.lowerCaseToken(64),
clientName = "client-name-apikey",
authorizedEntities = Seq.empty
)
env.datastores.globalConfigDataStore
.latest()(env.otoroshiExecutionContext, env)
.templates
.apikey
.map { template =>
ApiKey._fmt.reads(defaultApikey.json.asObject.deepMerge(template)).get
}
.getOrElse {
defaultApikey
}
}
def remainingQuotas(apiKey: ApiKey)(implicit ec: ExecutionContext, env: Env): Future[RemainingQuotas]
def resetQuotas(apiKey: ApiKey)(implicit ec: ExecutionContext, env: Env): Future[RemainingQuotas]
def updateQuotas(apiKey: ApiKey, increment: Long = 1L)(implicit
ec: ExecutionContext,
env: Env
): Future[RemainingQuotas]
def withingQuotas(apiKey: ApiKey)(implicit ec: ExecutionContext, env: Env): Future[Boolean]
def withinThrottlingQuota(apiKey: ApiKey)(implicit ec: ExecutionContext, env: Env): Future[Boolean]
def withinDailyQuota(apiKey: ApiKey)(implicit ec: ExecutionContext, env: Env): Future[Boolean]
def withinMonthlyQuota(apiKey: ApiKey)(implicit ec: ExecutionContext, env: Env): Future[Boolean]
def findByService(serviceId: String)(implicit ec: ExecutionContext, env: Env): Future[Seq[ApiKey]]
def findByGroup(groupId: String)(implicit ec: ExecutionContext, env: Env): Future[Seq[ApiKey]]
def findAuthorizeKeyFor(clientId: String, serviceId: String)(implicit
ec: ExecutionContext,
env: Env
): Future[Option[ApiKey]]
def findAuthorizeKeyForBearer(bearer: String, serviceId: String)(implicit
ec: ExecutionContext,
env: Env
): Future[Option[ApiKey]]
def findAuthorizeKeyForFromCache(clientId: String, serviceId: String)(implicit env: Env): Option[ApiKey]
def deleteFastLookupByGroup(groupId: String, apiKey: ApiKey)(implicit ec: ExecutionContext, env: Env): Future[Long]
def deleteFastLookupByService(serviceId: String, apiKey: ApiKey)(implicit
ec: ExecutionContext,
env: Env
): Future[Long]
def addFastLookupByGroup(groupId: String, apiKey: ApiKey)(implicit ec: ExecutionContext, env: Env): Future[Long]
def addFastLookupByService(serviceId: String, apiKey: ApiKey)(implicit ec: ExecutionContext, env: Env): Future[Long]
def clearFastLookupByGroup(groupId: String)(implicit ec: ExecutionContext, env: Env): Future[Long]
def clearFastLookupByService(serviceId: String)(implicit ec: ExecutionContext, env: Env): Future[Long]
// def willBeRotatedAt(apiKey: ApiKey)(implicit ec: ExecutionContext, env: Env): Future[Option[(DateTime, Long)]] = {
// if (apiKey.rotation.enabled) {
// val key = s"${env.storageRoot}:apikeys-rotation:${apiKey.clientId}"
// env.datastores.rawDataStore.get(key).map {
// case None => None
// case Some(body) =>
// val json = Json.parse(body.utf8String)
// val start = new DateTime((json \ "start").as[Long])
// val end = start.plus(apiKey.rotation.rotationEvery * 3600000L)
// Some((end, new org.joda.time.Period(DateTime.now(), end).toStandardSeconds.getSeconds * 1000))
// }
// } else {
// FastFuture.successful(None)
// }
// }
def keyRotation(apiKey: ApiKey)(implicit ec: ExecutionContext, env: Env): Future[Option[ApiKeyRotationInfo]] = {
if (apiKey.rotation.enabled) {
val key = s"${env.storageRoot}:apikeys-rotation:${apiKey.clientId}"
env.datastores.rawDataStore.get(key).flatMap {
case None =>
val newApk = apiKey.copy(rotation = apiKey.rotation.copy(nextSecret = None))
val start = DateTime.now()
val end = start.plus(apiKey.rotation.rotationEvery * 3600000L)
val remaining = end.getMillis - DateTime.now().getMillis
val res: Option[ApiKeyRotationInfo] = Some(
ApiKeyRotationInfo(
end,
remaining
) // new org.joda.time.Period(DateTime.now(), end).toStandardSeconds.getSeconds * 1000)
)
env.datastores.rawDataStore
.set(
key,
ByteString(
Json.stringify(
Json.obj(
"start" -> start.toDate.getTime
)
)
),
Some((newApk.rotation.rotationEvery * 60 * 60 * 1000) + (5 * 60 * 1000))
)
.flatMap { _ =>
newApk.save().map(_ => res)
}
case Some(body) => {
val json = Json.parse(body.utf8String)
val start = new DateTime((json \ "start").as[Long])
val end = start.plus(apiKey.rotation.rotationEvery * 3600000L)
val startGrace = end.minus(apiKey.rotation.gracePeriod * 3600000L)
val now = DateTime.now()
val beforeGracePeriod = now.isAfter(start) && now.isBefore(startGrace) && now.isBefore(end)
val inGracePeriod = now.isAfter(start) && now.isAfter(startGrace) && now.isBefore(end)
val afterGracePeriod = now.isAfter(start) && now.isAfter(startGrace) && now.isAfter(end)
val remaining = end.getMillis - DateTime.now().getMillis
val res: Option[ApiKeyRotationInfo] = Some(
ApiKeyRotationInfo(
end,
remaining
) //new org.joda.time.Period(DateTime.now(), end).toStandardSeconds.getSeconds * 1000)
)
if (beforeGracePeriod) {
FastFuture.successful(res)
} else if (inGracePeriod) {
val newApk = apiKey.copy(
rotation =
apiKey.rotation.copy(nextSecret = apiKey.rotation.nextSecret.orElse(Some(IdGenerator.token(64))))
)
Alerts.send(
ApiKeySecretWillRotate(
`@id` = env.snowflakeGenerator.nextIdStr(),
`@env` = env.env,
apikey = newApk
)
)
newApk.save().map(_ => res)
} else if (afterGracePeriod) {
val newApk = apiKey.copy(
clientSecret = apiKey.rotation.nextSecret.getOrElse(IdGenerator.token(64)),
rotation = apiKey.rotation.copy(nextSecret = None)
)
Alerts.send(
ApiKeySecretHasRotated(
`@id` = env.snowflakeGenerator.nextIdStr(),
`@env` = env.env,
oldApikey = apiKey,
apikey = newApk
)
)
env.datastores.rawDataStore
.set(
key,
ByteString(
Json.stringify(
Json.obj(
"start" -> System.currentTimeMillis()
)
)
),
Some((newApk.rotation.rotationEvery * 60 * 60 * 1000) + (5 * 60 * 1000))
)
.flatMap { _ =>
newApk.save().map(_ => res)
}
} else {
FastFuture.successful(res)
}
}
}
} else {
FastFuture.successful(None)
}
}
}
sealed trait ApikeyLocationKind {
def name: String
}
object ApikeyLocationKind {
case object Header extends ApikeyLocationKind { def name: String = "Header" }
case object Cookie extends ApikeyLocationKind { def name: String = "Cookie" }
case object Query extends ApikeyLocationKind { def name: String = "Query" }
}
case class ApikeyLocation(kind: ApikeyLocationKind, name: String) {
def json: JsValue = Json.obj(
"name" -> name,
"kind" -> kind.name
)
}
case class ApikeyTuple(
clientId: String,
clientSecret: Option[String] = None,
jwtToken: Option[DecodedJWT] = None,
location: Option[ApikeyLocation],
otoBearer: Option[String]
) {
def json: JsValue = Json.obj(
"client_id" -> clientId,
"client_secret" -> clientSecret.map(JsString.apply).getOrElse(JsNull).as[JsValue],
"jwt_token" -> jwtToken.map(_.json).getOrElse(JsNull).as[JsValue],
"oto_bearer" -> otoBearer.map(JsString.apply).getOrElse(JsNull).as[JsValue],
"location" -> location.map(_.json).getOrElse(JsNull).as[JsValue]
)
}
object ApikeyTuple {
def fromJson(json: JsValue): Either[Throwable, ApikeyTuple] = {
Try {
ApikeyTuple(
clientId = json.select("client_id").asString,
clientSecret = json.select("client_secret").asOpt[String],
jwtToken = json.select("jwt_token").asOpt[String].map(JWT.decode),
otoBearer = json.select("oto_bearer").asOpt[String],
location = json.select("location").asOpt[JsObject].map { loc =>
ApikeyLocation(
name = loc.select("name").asString,
kind = loc.select("kind").asString match {
case "Query" => ApikeyLocationKind.Query
case "Cookie" => ApikeyLocationKind.Cookie
case _ => ApikeyLocationKind.Header
}
)
}
)
}.toEither
}
}
object ApiKeyHelper {
import otoroshi.utils.http.RequestImplicits._
import otoroshi.utils.syntax.implicits._
def decodeBase64(encoded: String): String = new String(OtoroshiClaim.decoder.decode(encoded), Charsets.UTF_8)
def extractApiKey(req: RequestHeader, descriptor: ServiceDescriptor, attrs: TypedMap)(implicit
ec: ExecutionContext,
env: Env
): Future[Option[ApiKey]] = {
val authByOtoBearerToken = OtoroshiBearerToken.extractTokenFromRequest(req, descriptor)
val authByJwtToken = req.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)
val authBasic = req.headers
.get(
descriptor.apiKeyConstraints.basicAuth.headerName
.getOrElse(env.Headers.OtoroshiAuthorization)
)
.orElse(
req.headers.get("Authorization").filter(_.startsWith("Basic "))
)
.map(_.replace("Basic ", ""))
.flatMap(e => Try(decodeBase64(e)).toOption)
.orElse(
req.queryString
.get(
descriptor.apiKeyConstraints.basicAuth.queryName
.getOrElse(env.Headers.OtoroshiBasicAuthorization)
)
.flatMap(_.lastOption)
.flatMap(e => Try(decodeBase64(e)).toOption)
)
val authByCustomHeaders = req.headers
.get(
descriptor.apiKeyConstraints.customHeadersAuth.clientIdHeaderName
.getOrElse(env.Headers.OtoroshiClientId)
)
.flatMap(id =>
req.headers
.get(
descriptor.apiKeyConstraints.customHeadersAuth.clientSecretHeaderName
.getOrElse(env.Headers.OtoroshiClientSecret)
)
.map(s => (id, s))
)
val authBySimpleApiKeyClientId = req.headers
.get(
descriptor.apiKeyConstraints.clientIdAuth.headerName
.getOrElse(env.Headers.OtoroshiSimpleApiKeyClientId)
)
.orElse(
req.queryString
.get(
descriptor.apiKeyConstraints.clientIdAuth.queryName
.getOrElse(env.Headers.OtoroshiSimpleApiKeyClientId)
)
.flatMap(_.lastOption)
)
val preExtractedApiKey = attrs.get(otoroshi.plugins.Keys.ApiKeyKey)
if (preExtractedApiKey.isDefined) {
FastFuture.successful(preExtractedApiKey)
} else if (authByOtoBearerToken.isDefined && descriptor.apiKeyConstraints.otoBearerAuth.enabled) {
val bearer = authByOtoBearerToken.get
env.datastores.apiKeyDataStore
.findAuthorizeKeyForBearer(bearer, descriptor.id)
.flatMap {
case None => FastFuture.successful(None)
case Some(key) => FastFuture.successful(Some(key))
}
} else if (authBySimpleApiKeyClientId.isDefined && descriptor.apiKeyConstraints.clientIdAuth.enabled) {
val clientId = authBySimpleApiKeyClientId.get
env.datastores.apiKeyDataStore
.findAuthorizeKeyFor(clientId, descriptor.id)
.flatMap {
case None => FastFuture.successful(None)
case Some(key) if !key.allowClientIdOnly => FastFuture.successful(None)
case Some(key) if key.allowClientIdOnly => FastFuture.successful(Some(key))
}
} else if (authByCustomHeaders.isDefined && descriptor.apiKeyConstraints.customHeadersAuth.enabled) {
val (clientId, clientSecret) = authByCustomHeaders.get
env.datastores.apiKeyDataStore
.findAuthorizeKeyFor(clientId, descriptor.id)
.flatMap {
case None => FastFuture.successful(None)
case Some(key) if key.isInvalid(clientSecret) => FastFuture.successful(None)
case Some(key) if key.isValid(clientSecret) => FastFuture.successful(Some(key))
}
} else if (authByJwtToken.isDefined && descriptor.apiKeyConstraints.jwtAuth.enabled) {
val jwtTokenValue = authByJwtToken.get
Try {
JWT.decode(jwtTokenValue)
} map { jwt =>
jwt
.claimStr("clientId")
.orElse(jwt.claimStr("client_id"))
.orElse(jwt.claimStr("cid"))
.orElse(jwt.claimStr("iss")) match {
case Some(clientId) =>
env.datastores.apiKeyDataStore
.findAuthorizeKeyFor(clientId, descriptor.id)
.flatMap {
case Some(apiKey) => {
val possibleKeyPairId = apiKey.metadata.get("jwt-sign-keypair")
val kid = Option(jwt.getKeyId)
.orElse(possibleKeyPairId)
.filter(_ => descriptor.apiKeyConstraints.jwtAuth.keyPairSigned)
.filter(id => if (possibleKeyPairId.isDefined) possibleKeyPairId.get == id else true)
.flatMap(id => DynamicSSLEngineProvider.certificates.get(id))
val kp = kid.map(_.cryptoKeyPair)
val algorithmOpt: Option[Algorithm] = Option(jwt.getAlgorithm).collect {
case "HS256" if descriptor.apiKeyConstraints.jwtAuth.secretSigned =>
Algorithm.HMAC256(apiKey.clientSecret)
case "HS384" if descriptor.apiKeyConstraints.jwtAuth.secretSigned =>
Algorithm.HMAC384(apiKey.clientSecret)
case "HS512" if descriptor.apiKeyConstraints.jwtAuth.secretSigned =>
Algorithm.HMAC512(apiKey.clientSecret)
case "ES256" if kid.isDefined =>
Algorithm.ECDSA256(
kp.get.getPublic.asInstanceOf[ECPublicKey],
kp.get.getPrivate.asInstanceOf[ECPrivateKey]
)
case "ES384" if kid.isDefined =>
Algorithm.ECDSA384(
kp.get.getPublic.asInstanceOf[ECPublicKey],
kp.get.getPrivate.asInstanceOf[ECPrivateKey]
)
case "ES512" if kid.isDefined =>
Algorithm.ECDSA512(
kp.get.getPublic.asInstanceOf[ECPublicKey],
kp.get.getPrivate.asInstanceOf[ECPrivateKey]
)
case "RS256" if kid.isDefined =>
Algorithm.RSA256(
kp.get.getPublic.asInstanceOf[RSAPublicKey],
kp.get.getPrivate.asInstanceOf[RSAPrivateKey]
)
case "RS384" if kid.isDefined =>
Algorithm.RSA384(
kp.get.getPublic.asInstanceOf[RSAPublicKey],
kp.get.getPrivate.asInstanceOf[RSAPrivateKey]
)
case "RS512" if kid.isDefined =>
Algorithm.RSA512(
kp.get.getPublic.asInstanceOf[RSAPublicKey],
kp.get.getPrivate.asInstanceOf[RSAPrivateKey]
)
} // getOrElse Algorithm.HMAC512(apiKey.clientSecret)
val exp =
Option(jwt.getClaim("exp")).filterNot(_.isNull).filterNot(_.isMissing).map(_.asLong())
val iat =
Option(jwt.getClaim("iat")).filterNot(_.isNull).filterNot(_.isMissing).map(_.asLong())
val httpPath = Option(jwt.getClaim("httpPath"))
.filterNot(_.isNull)
.filterNot(_.isMissing)
.map(_.asString())
val httpVerb = Option(jwt.getClaim("httpVerb"))
.filterNot(_.isNull)
.filterNot(_.isMissing)
.map(_.asString())
val httpHost = Option(jwt.getClaim("httpHost"))
.filterNot(_.isNull)
.filterNot(_.isMissing)
.map(_.asString())
algorithmOpt match {
case Some(algorithm) => {
val verifier =
JWT
.require(algorithm)
//.withIssuer(clientId)
.acceptLeeway(10)
.build
Try(verifier.verify(jwtTokenValue))
.filter { token =>
val xsrfToken = token.getClaim("xsrfToken")
val xsrfTokenHeader = req.headers.get("X-XSRF-TOKEN")
if (!xsrfToken.isNull && !xsrfToken.isMissing && xsrfTokenHeader.isDefined) {
xsrfToken.asString() == xsrfTokenHeader.get
} else if (!xsrfToken.isNull && !xsrfToken.isMissing && xsrfTokenHeader.isEmpty) {
false
} else {
true
}
}
.filter { _ =>
descriptor.apiKeyConstraints.jwtAuth.maxJwtLifespanSecs.map { maxJwtLifespanSecs =>
if (exp.isEmpty || iat.isEmpty) {
false
} else {
if ((exp.get - iat.get) <= maxJwtLifespanSecs) {
true
} else {
false
}
}
} getOrElse {
true
}
}
.filter { _ =>
if (descriptor.apiKeyConstraints.jwtAuth.includeRequestAttributes) {
val matchPath = httpPath.exists(_ == req.relativeUri)
val matchVerb =
httpVerb.exists(_.toLowerCase == req.method.toLowerCase)
val matchHost = httpHost.exists(_.toLowerCase == req.theHost)
matchPath && matchVerb && matchHost
} else {
true
}
} match {
case Success(_) =>
attrs.put(
otoroshi.plugins.Keys.ApiKeyJwtKey -> Json
.parse(jwt.getPayload.byteString.decodeBase64.utf8String)
)
FastFuture.successful(Some(apiKey))
case Failure(e) => FastFuture.successful(None)
}
}
case None => FastFuture.successful(None)
}
}
case None => FastFuture.successful(None)
}
case None => FastFuture.successful(None)
}
} getOrElse FastFuture.successful(None)
} else if (authBasic.isDefined && descriptor.apiKeyConstraints.basicAuth.enabled) {
val auth = authBasic.get
val parts = auth.split(":")
val id = parts.headOption.map(_.trim)
val secret = if (parts.length > 1) parts.tail.mkString(":").trim.some else None
(id, secret) match {
case (Some(apiKeyClientId), Some(apiKeySecret)) => {
env.datastores.apiKeyDataStore
.findAuthorizeKeyFor(apiKeyClientId, descriptor.id)
.flatMap {
case None => FastFuture.successful(None)
case Some(key) if key.isInvalid(apiKeySecret) => FastFuture.successful(None)
case Some(key) if key.isValid(apiKeySecret) => FastFuture.successful(Some(key))
}
}
case _ => FastFuture.successful(None)
}
} else {
FastFuture.successful(None)
}
}
def detectApiKey(req: RequestHeader, descriptor: ServiceDescriptor, attrs: TypedMap)(implicit env: Env): Boolean = {
val authByOtoBearerToken = OtoroshiBearerToken.extractTokenFromRequest(req, descriptor)
val authByJwtToken = req.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)
val authBasic = req.headers
.get(
descriptor.apiKeyConstraints.basicAuth.headerName
.getOrElse(env.Headers.OtoroshiAuthorization)
)
.orElse(
req.headers.get("Authorization").filter(_.startsWith("Basic "))
)
.map(_.replace("Basic ", ""))
.flatMap(e => Try(decodeBase64(e)).toOption)
.orElse(
req.queryString
.get(
descriptor.apiKeyConstraints.basicAuth.queryName
.getOrElse(env.Headers.OtoroshiBasicAuthorization)
)
.flatMap(_.lastOption)
.flatMap(e => Try(decodeBase64(e)).toOption)
)
val authByCustomHeaders = req.headers
.get(
descriptor.apiKeyConstraints.customHeadersAuth.clientIdHeaderName
.getOrElse(env.Headers.OtoroshiClientId)
)
.flatMap(id =>
req.headers
.get(
descriptor.apiKeyConstraints.customHeadersAuth.clientSecretHeaderName
.getOrElse(env.Headers.OtoroshiClientSecret)
)
.map(s => (id, s))
)
val authBySimpleApiKeyClientId = req.headers
.get(
descriptor.apiKeyConstraints.clientIdAuth.headerName
.getOrElse(env.Headers.OtoroshiSimpleApiKeyClientId)
)
.orElse(
req.queryString
.get(
descriptor.apiKeyConstraints.clientIdAuth.queryName
.getOrElse(env.Headers.OtoroshiSimpleApiKeyClientId)
)
.flatMap(_.lastOption)
)
val preExtractedApiKey = attrs.get(otoroshi.plugins.Keys.ApiKeyKey)
if (preExtractedApiKey.isDefined) {
true
} else if (authBySimpleApiKeyClientId.isDefined && descriptor.apiKeyConstraints.clientIdAuth.enabled) {
true
} else if (authByCustomHeaders.isDefined && descriptor.apiKeyConstraints.customHeadersAuth.enabled) {
true
} else if (authByJwtToken.isDefined && descriptor.apiKeyConstraints.jwtAuth.enabled) {
true
} else if (authBasic.isDefined && descriptor.apiKeyConstraints.basicAuth.enabled) {
true
} else if (authByOtoBearerToken.isDefined && descriptor.apiKeyConstraints.otoBearerAuth.enabled) {
true
} else {
false
}
}
case class PassWithApiKeyContext(
req: RequestHeader,
descriptor: ServiceDescriptor,
attrs: TypedMap,
config: GlobalConfig
)
def passWithApiKey[T](
ctx: PassWithApiKeyContext,
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 PassWithApiKeyContext(req, descriptor, attrs, config) = ctx
def sendRevokedApiKeyAlert(key: ApiKey) = {
Alerts.send(
RevokedApiKeyUsageAlert(
env.snowflakeGenerator.nextIdStr(),
DateTime.now(),
env.env,
req,
key,
descriptor.some,
env
)
)
}
def sendQuotasAlmostExceededError(key: ApiKey, quotas: RemainingQuotas): Unit = {
if (ctx.config.quotasSettings.enabled) {
if (
quotas.currentCallsPerDay >= (ctx.config.quotasSettings.dailyQuotasThreshold * quotas.authorizedCallsPerDay)
) {
ApiKeyQuotasAlmostExceededAlert(
`@id` = env.snowflakeGenerator.nextIdStr(),
`@env` = env.env,
apikey = key,
remainingQuotas = quotas,
settings = ctx.config.quotasSettings,
reason = ApiKeyQuotasAlmostExceededReason.DailyQuotasAlmostExceeded
).toAnalytics()
}
if (
quotas.currentCallsPerMonth >= (ctx.config.quotasSettings.monthlyQuotasThreshold * quotas.authorizedCallsPerMonth)
) {
ApiKeyQuotasAlmostExceededAlert(
`@id` = env.snowflakeGenerator.nextIdStr(),
`@env` = env.env,
apikey = key,
remainingQuotas = quotas,
settings = ctx.config.quotasSettings,
reason = ApiKeyQuotasAlmostExceededReason.MonthlyQuotasAlmostExceeded
).toAnalytics()
}
}
}
def sendQuotasExceededError(key: ApiKey, quotas: RemainingQuotas): Unit = {
if (
quotas.currentCallsPerDay >= (ctx.config.quotasSettings.dailyQuotasThreshold * quotas.authorizedCallsPerDay)
) {
ApiKeyQuotasExceededAlert(
`@id` = env.snowflakeGenerator.nextIdStr(),
`@env` = env.env,
apikey = key,
remainingQuotas = quotas,
reason = ApiKeyQuotasExceededReason.DailyQuotasExceeded
).toAnalytics()
}
if (
quotas.currentCallsPerMonth >= (ctx.config.quotasSettings.monthlyQuotasThreshold * quotas.authorizedCallsPerMonth)
) {
ApiKeyQuotasExceededAlert(
`@id` = env.snowflakeGenerator.nextIdStr(),
`@env` = env.env,
apikey = key,
remainingQuotas = quotas,
reason = ApiKeyQuotasExceededReason.MonthlyQuotasExceeded
).toAnalytics()
}
}
// if (req.headers.get("Otoroshi-Client-Id").isEmpty) {
// println("no apikey", req.method, req.path)
// } else {
// println("with apikey", req.method, req.path)
// }
// if (descriptor.thirdPartyApiKey.enabled) {
// descriptor.thirdPartyApiKey.handleGen[T](req, descriptor, config, attrs) { key =>
// callDownstream(config, key, None)
// }
// } else {
val authByOtoBearerToken = OtoroshiBearerToken.extractTokenFromRequest(req, descriptor)
val authByJwtToken = req.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)
val authBasic = req.headers
.get(
descriptor.apiKeyConstraints.basicAuth.headerName
.getOrElse(env.Headers.OtoroshiAuthorization)
)
.orElse(
req.headers.get("Authorization").filter(_.startsWith("Basic "))
)
.map(_.replace("Basic ", ""))
.flatMap(e => Try(decodeBase64(e)).toOption)
.orElse(
req.queryString
.get(
descriptor.apiKeyConstraints.basicAuth.queryName
.getOrElse(env.Headers.OtoroshiBasicAuthorization)
)
.flatMap(_.lastOption)
.flatMap(e => Try(decodeBase64(e)).toOption)
)
val authByCustomHeaders = req.headers
.get(
descriptor.apiKeyConstraints.customHeadersAuth.clientIdHeaderName
.getOrElse(env.Headers.OtoroshiClientId)
)
.flatMap(id =>
req.headers
.get(
descriptor.apiKeyConstraints.customHeadersAuth.clientSecretHeaderName
.getOrElse(env.Headers.OtoroshiClientSecret)
)
.map(s => (id, s))
)
val authBySimpleApiKeyClientId = req.headers
.get(
descriptor.apiKeyConstraints.clientIdAuth.headerName
.getOrElse(env.Headers.OtoroshiSimpleApiKeyClientId)
)
.orElse(
req.queryString
.get(
descriptor.apiKeyConstraints.clientIdAuth.queryName
.getOrElse(env.Headers.OtoroshiSimpleApiKeyClientId)
)
.flatMap(_.lastOption)
)
val preExtractedApiKey = attrs.get(otoroshi.plugins.Keys.ApiKeyKey)
if (preExtractedApiKey.isDefined) {
preExtractedApiKey match {
case None => errorResult(Unauthorized, "Invalid API key", "errors.invalid.api.key")
case Some(key) if key.isInvalid(key.clientSecret) => {
sendRevokedApiKeyAlert(key)
errorResult(Unauthorized, "Bad API key", "errors.bad.api.key")
}
case Some(key) if !key.matchRouting(descriptor) => {
errorResult(Unauthorized, "Invalid API key", "errors.bad.api.key")
}
case Some(key)
if key.restrictions
.handleRestrictions(descriptor.id, descriptor.some, Some(key), req, attrs)
._1 => {
key.restrictions
.handleRestrictions(descriptor.id, descriptor.some, Some(key), req, attrs)
._2
.map(v => Left(v))
}
case Some(key) if key.isValid(key.clientSecret) =>
key.withinQuotasAndRotationQuotas().flatMap {
case (true, rotationInfos, quotas) =>
rotationInfos.foreach { i =>
attrs.put(otoroshi.plugins.Keys.ApiKeyRotationKey -> i)
}
attrs.put(otoroshi.plugins.Keys.ApiKeyRemainingQuotasKey -> quotas)
sendQuotasAlmostExceededError(key, quotas)
callDownstream(config, Some(key), None)
case (false, _, quotas) =>
sendQuotasExceededError(key, quotas)
errorResult(TooManyRequests, "You performed too much requests", "errors.too.much.requests")
}
}
} else if (authBySimpleApiKeyClientId.isDefined && descriptor.apiKeyConstraints.clientIdAuth.enabled) {
val clientId = authBySimpleApiKeyClientId.get
env.datastores.apiKeyDataStore
.findAuthorizeKeyFor(clientId, descriptor.id)
.flatMap {
case None =>
errorResult(Unauthorized, "Invalid API key", "errors.invalid.api.key")
case Some(key) if !key.allowClientIdOnly =>
errorResult(BadRequest, "Bad API key", "errors.bad.api.key")
case Some(key) if !key.matchRouting(descriptor) =>
errorResult(Unauthorized, "Invalid API key", "errors.bad.api.key")
case Some(key)
if key.restrictions.enabled && key.restrictions
.isNotFound(req.method, req.theDomain, req.relativeUri) => {
errorResult(NotFound, "Not Found", "errors.not.found")
}
case Some(key)
if key.restrictions
.handleRestrictions(descriptor.id, descriptor.some, Some(key), req, attrs)
._1 => {
key.restrictions
.handleRestrictions(descriptor.id, descriptor.some, Some(key), req, attrs)
._2
.map(v => Left(v))
}
case Some(key) if key.allowClientIdOnly =>
key.withinQuotasAndRotationQuotas().flatMap {
case (true, rotationInfos, quotas) =>
rotationInfos.foreach { i =>
attrs.put(otoroshi.plugins.Keys.ApiKeyRotationKey -> i)
}
attrs.put(otoroshi.plugins.Keys.ApiKeyRemainingQuotasKey -> quotas)
sendQuotasAlmostExceededError(key, quotas)
callDownstream(config, Some(key), None)
case (false, _, quotas) =>
sendQuotasExceededError(key, quotas)
errorResult(TooManyRequests, "You performed too much requests", "errors.too.much.requests")
}
}
} else if (authByCustomHeaders.isDefined && descriptor.apiKeyConstraints.customHeadersAuth.enabled) {
val (clientId, clientSecret) = authByCustomHeaders.get
env.datastores.apiKeyDataStore
.findAuthorizeKeyFor(clientId, descriptor.id)
.flatMap {
case None =>
errorResult(Unauthorized, "Invalid API key", "errors.invalid.api.key")
case Some(key) if key.isInvalid(clientSecret) => {
sendRevokedApiKeyAlert(key)
errorResult(Unauthorized, "Bad API key", "errors.bad.api.key")
}
case Some(key) if !key.matchRouting(descriptor) =>
errorResult(Unauthorized, "Bad API key", "errors.bad.api.key")
case Some(key)
if key.restrictions
.handleRestrictions(descriptor.id, descriptor.some, Some(key), req, attrs)
._1 => {
key.restrictions
.handleRestrictions(descriptor.id, descriptor.some, Some(key), req, attrs)
._2
.map(v => Left(v))
}
case Some(key) if key.isValid(clientSecret) =>
key.withinQuotasAndRotationQuotas().flatMap {
case (true, rotationInfos, quotas) =>
rotationInfos.foreach { i =>
attrs.put(otoroshi.plugins.Keys.ApiKeyRotationKey -> i)
}
attrs.put(otoroshi.plugins.Keys.ApiKeyRemainingQuotasKey -> quotas)
sendQuotasAlmostExceededError(key, quotas)
callDownstream(config, Some(key), None)
case (false, _, quotas) =>
sendQuotasExceededError(key, quotas)
errorResult(TooManyRequests, "You performed too much requests", "errors.too.much.requests")
}
}
} else if (authByOtoBearerToken.isDefined && descriptor.apiKeyConstraints.otoBearerAuth.enabled) {
val bearer = authByOtoBearerToken.get
env.datastores.apiKeyDataStore
.findAuthorizeKeyForBearer(bearer, descriptor.id)
.flatMap {
case None =>
errorResult(Unauthorized, "Invalid API key", "errors.invalid.api.key")
case Some(key) if !key.matchRouting(descriptor) =>
errorResult(Unauthorized, "Bad API key", "errors.bad.api.key")
case Some(key)
if key.restrictions
.handleRestrictions(descriptor.id, descriptor.some, Some(key), req, attrs)
._1 => {
key.restrictions
.handleRestrictions(descriptor.id, descriptor.some, Some(key), req, attrs)
._2
.map(v => Left(v))
}
case Some(key) =>
key.withinQuotasAndRotationQuotas().flatMap {
case (true, rotationInfos, quotas) =>
rotationInfos.foreach { i =>
attrs.put(otoroshi.plugins.Keys.ApiKeyRotationKey -> i)
}
attrs.put(otoroshi.plugins.Keys.ApiKeyRemainingQuotasKey -> quotas)
sendQuotasAlmostExceededError(key, quotas)
callDownstream(config, Some(key), None)
case (false, _, quotas) =>
sendQuotasExceededError(key, quotas)
errorResult(TooManyRequests, "You performed too much requests", "errors.too.much.requests")
}
}
} else if (authByJwtToken.isDefined && descriptor.apiKeyConstraints.jwtAuth.enabled) {
val jwtTokenValue = authByJwtToken.get
Try {
JWT.decode(jwtTokenValue)
} map { jwt =>
jwt
.claimStr("clientId")
.orElse(jwt.claimStr("client_id"))
.orElse(jwt.claimStr("cid"))
.orElse(jwt.claimStr("iss")) match {
case Some(clientId) =>
env.datastores.apiKeyDataStore
.findAuthorizeKeyFor(clientId, descriptor.id)
.flatMap {
case Some(apiKey) => {
val possibleKeyPairId = apiKey.metadata.get("jwt-sign-keypair")
val kid = Option(jwt.getKeyId)
.orElse(possibleKeyPairId)
.filter(_ => descriptor.apiKeyConstraints.jwtAuth.keyPairSigned)
.filter(id => if (possibleKeyPairId.isDefined) possibleKeyPairId.get == id else true)
.flatMap(id => DynamicSSLEngineProvider.certificates.get(id))
val kp = kid.map(_.cryptoKeyPair)
val algorithmOpt: Option[Algorithm] = Option(jwt.getAlgorithm).collect {
case "HS256" if descriptor.apiKeyConstraints.jwtAuth.secretSigned =>
Algorithm.HMAC256(apiKey.clientSecret)
case "HS384" if descriptor.apiKeyConstraints.jwtAuth.secretSigned =>
Algorithm.HMAC384(apiKey.clientSecret)
case "HS512" if descriptor.apiKeyConstraints.jwtAuth.secretSigned =>
Algorithm.HMAC512(apiKey.clientSecret)
case "ES256" if kid.isDefined =>
Algorithm.ECDSA256(
kp.get.getPublic.asInstanceOf[ECPublicKey],
kp.get.getPrivate.asInstanceOf[ECPrivateKey]
)
case "ES384" if kid.isDefined =>
Algorithm.ECDSA384(
kp.get.getPublic.asInstanceOf[ECPublicKey],
kp.get.getPrivate.asInstanceOf[ECPrivateKey]
)
case "ES512" if kid.isDefined =>
Algorithm.ECDSA512(
kp.get.getPublic.asInstanceOf[ECPublicKey],
kp.get.getPrivate.asInstanceOf[ECPrivateKey]
)
case "RS256" if kid.isDefined =>
Algorithm.RSA256(
kp.get.getPublic.asInstanceOf[RSAPublicKey],
kp.get.getPrivate.asInstanceOf[RSAPrivateKey]
)
case "RS384" if kid.isDefined =>
Algorithm.RSA384(
kp.get.getPublic.asInstanceOf[RSAPublicKey],
kp.get.getPrivate.asInstanceOf[RSAPrivateKey]
)
case "RS512" if kid.isDefined =>
Algorithm.RSA512(
kp.get.getPublic.asInstanceOf[RSAPublicKey],
kp.get.getPrivate.asInstanceOf[RSAPrivateKey]
)
} //getOrElse Algorithm.HMAC512(apiKey.clientSecret)
val exp =
Option(jwt.getClaim("exp"))
.filterNot(_.isNull)
.filterNot(_.isMissing)
.map(_.asLong())
val iat =
Option(jwt.getClaim("iat"))
.filterNot(_.isNull)
.filterNot(_.isMissing)
.map(_.asLong())
val httpPath = Option(jwt.getClaim("httpPath"))
.filterNot(_.isNull)
.filterNot(_.isMissing)
.map(_.asString())
val httpVerb = Option(jwt.getClaim("httpVerb"))
.filterNot(_.isNull)
.filterNot(_.isMissing)
.map(_.asString())
val httpHost = Option(jwt.getClaim("httpHost"))
.filterNot(_.isNull)
.filterNot(_.isMissing)
.map(_.asString())
algorithmOpt match {
case Some(algorithm) => {
val verifier =
JWT
.require(algorithm)
// .withIssuer(clientId)
.acceptLeeway(10) // TODO: customize ???
.build
Try(verifier.verify(jwtTokenValue))
.filter { token =>
val xsrfToken = token.getClaim("xsrfToken")
val xsrfTokenHeader = req.headers.get("X-XSRF-TOKEN")
if (!xsrfToken.isNull && !xsrfToken.isMissing && xsrfTokenHeader.isDefined) {
xsrfToken.asString() == xsrfTokenHeader.get
} else !(!xsrfToken.isNull && !xsrfToken.isMissing && xsrfTokenHeader.isEmpty)
}
.filter { _ =>
descriptor.apiKeyConstraints.jwtAuth.maxJwtLifespanSecs.map { maxJwtLifespanSecs =>
if (exp.isEmpty || iat.isEmpty) {
false
} else {
(exp.get - iat.get) <= maxJwtLifespanSecs
}
} getOrElse {
true
}
}
.filter { _ =>
if (descriptor.apiKeyConstraints.jwtAuth.includeRequestAttributes) {
val matchPath = httpPath.exists(_ == req.relativeUri)
val matchVerb =
httpVerb
.exists(_.toLowerCase == req.method.toLowerCase)
val matchHost =
httpHost.exists(_.toLowerCase == req.theHost)
matchPath && matchVerb && matchHost
} else {
true
}
} match {
case Success(_) if !apiKey.matchRouting(descriptor) =>
errorResult(Unauthorized, "Invalid API key", "errors.bad.api.key")
case Success(_)
if apiKey.restrictions
.handleRestrictions(descriptor.id, descriptor.some, Some(apiKey), req, attrs)
._1 => {
apiKey.restrictions
.handleRestrictions(descriptor.id, descriptor.some, Some(apiKey), req, attrs)
._2
.map(v => Left(v))
}
case Success(_) =>
apiKey.withinQuotasAndRotationQuotas().flatMap {
case (true, rotationInfos, quotas) =>
rotationInfos.foreach { i =>
attrs.put(otoroshi.plugins.Keys.ApiKeyRotationKey -> i)
}
attrs.put(
otoroshi.plugins.Keys.ApiKeyJwtKey -> Json
.parse(jwt.getPayload.byteString.decodeBase64.utf8String)
)
attrs.put(otoroshi.plugins.Keys.ApiKeyRemainingQuotasKey -> quotas)
sendQuotasAlmostExceededError(apiKey, quotas)
callDownstream(config, Some(apiKey), None)
case (false, _, quotas) =>
sendQuotasExceededError(apiKey, quotas)
errorResult(
TooManyRequests,
"You performed too much requests",
"errors.too.much.requests"
)
}
case Failure(e) => {
e.printStackTrace()
sendRevokedApiKeyAlert(apiKey)
errorResult(BadRequest, s"Bad API key 1", "errors.bad.api.key")
}
}
}
case None =>
errorResult(Unauthorized, "Invalid ApiKey provided", "errors.invalid.api.key")
}
}
case None =>
errorResult(Unauthorized, "Invalid ApiKey provided", "errors.invalid.api.key")
}
case None =>
errorResult(Unauthorized, "Invalid ApiKey provided", "errors.invalid.api.key")
}
} getOrElse errorResult(Unauthorized, s"Invalid ApiKey provided", "errors.invalid.api.key")
} else if (authBasic.isDefined && descriptor.apiKeyConstraints.basicAuth.enabled) {
val auth = authBasic.get
val parts = auth.split(":")
val id = parts.headOption.map(_.trim)
val secret = if (parts.length > 1) parts.tail.mkString(":").trim.some else None
(id, secret) match {
case (Some(apiKeyClientId), Some(apiKeySecret)) => {
env.datastores.apiKeyDataStore
.findAuthorizeKeyFor(apiKeyClientId, descriptor.id)
.flatMap {
case None =>
errorResult(Unauthorized, "Invalid API key", "errors.invalid.api.key")
case Some(key) if key.isInvalid(apiKeySecret) => {
sendRevokedApiKeyAlert(key)
errorResult(Unauthorized, "Bad API key", "errors.bad.api.key")
}
case Some(key) if !key.matchRouting(descriptor) =>
errorResult(Unauthorized, "Invalid API key", "errors.bad.api.key")
case Some(key)
if key.restrictions
.handleRestrictions(descriptor.id, descriptor.some, Some(key), req, attrs)
._1 => {
key.restrictions
.handleRestrictions(descriptor.id, descriptor.some, Some(key), req, attrs)
._2
.map(v => Left(v))
}
case Some(key) if key.isValid(apiKeySecret) =>
key.withinQuotasAndRotationQuotas().flatMap {
case (true, rotationInfos, quotas) =>
rotationInfos.foreach { i =>
attrs.put(otoroshi.plugins.Keys.ApiKeyRotationKey -> i)
}
attrs.put(otoroshi.plugins.Keys.ApiKeyRemainingQuotasKey -> quotas)
sendQuotasAlmostExceededError(key, quotas)
callDownstream(config, Some(key), None)
case (false, _, quotas) =>
sendQuotasExceededError(key, quotas)
errorResult(TooManyRequests, "You performed too much requests", "errors.too.much.requests")
}
}
}
case _ =>
errorResult(BadRequest, "No ApiKey provided", "errors.no.api.key")
}
} else {
errorResult(BadRequest, "No ApiKey provided", "errors.no.api.key")
}
//}
}
def detectApikeyTuple(req: RequestHeader, constraints: ApiKeyConstraints, attrs: TypedMap)(implicit
env: Env
): Option[ApikeyTuple] = {
attrs.get(otoroshi.next.plugins.Keys.PreExtractedApikeyTupleKey) match {
case s @ Some(_) => s
case None => {
var location = ApikeyLocation(ApikeyLocationKind.Header, "--")
val authByOtoBearerToken: Option[ApikeyTuple] =
OtoroshiBearerToken.extractTokenFromRequest(req, constraints).map { bearer =>
val clientId = OtoroshiBearerToken.extractClientId(bearer)
ApikeyTuple(
clientId = clientId,
clientSecret = None,
jwtToken = None,
location = Some(
ApikeyLocation(
ApikeyLocationKind.Header,
"Authorization" // TODO: do it better ;)
)
),
otoBearer = Some(bearer)
)
}
val authByJwtToken: Option[ApikeyTuple] = req.headers
.get(
constraints.jwtAuth.headerName
.getOrElse(env.Headers.OtoroshiBearer)
)
.seffectOnWithPredicate(_.isDefined)(_ =>
location = ApikeyLocation(
ApikeyLocationKind.Header,
constraints.jwtAuth.headerName.getOrElse(env.Headers.OtoroshiBearer)
)
)
.orElse(
req.headers.get("Authorization").filter(_.startsWith("Bearer "))
)
.seffectOnWithPredicate(_.isDefined)(_ =>
location = ApikeyLocation(ApikeyLocationKind.Header, "Authorization")
)
.map(_.replace("Bearer ", ""))
.orElse(
req.queryString
.get(
constraints.jwtAuth.queryName
.getOrElse(env.Headers.OtoroshiBearerAuthorization)
)
.flatMap(_.lastOption)
.seffectOnWithPredicate(_.isDefined)(_ =>
location = ApikeyLocation(
ApikeyLocationKind.Query,
constraints.jwtAuth.queryName.getOrElse(env.Headers.OtoroshiBearerAuthorization)
)
)
)
.orElse(
req.cookies
.get(
constraints.jwtAuth.cookieName
.getOrElse(env.Headers.OtoroshiJWTAuthorization)
)
.map(_.value)
.seffectOnWithPredicate(_.isDefined)(_ =>
location = ApikeyLocation(
ApikeyLocationKind.Cookie,
constraints.jwtAuth.cookieName.getOrElse(env.Headers.OtoroshiJWTAuthorization)
)
)
)
.filter(_.split("\\.").length == 3)
.flatMap(v => Try(JWT.decode(v)).toOption)
.flatMap { jwt =>
jwt
.claimStr("clientId")
.orElse(jwt.claimStr("client_id"))
.orElse(jwt.claimStr("cid"))
.orElse(jwt.claimStr("iss"))
.map(cid => (cid, jwt))
}
.map(t => ApikeyTuple(t._1, jwtToken = t._2.some, location = location.some, otoBearer = None))
val authBasic: Option[ApikeyTuple] = req.headers
.get(
constraints.basicAuth.headerName
.getOrElse(env.Headers.OtoroshiAuthorization)
)
.seffectOnWithPredicate(_.isDefined)(_ =>
location = ApikeyLocation(
ApikeyLocationKind.Header,
constraints.basicAuth.headerName.getOrElse(env.Headers.OtoroshiAuthorization)
)
)
.orElse(
req.headers
.get("Authorization")
.filter(_.startsWith("Basic "))
.seffectOnWithPredicate(_.isDefined)(_ =>
location = ApikeyLocation(ApikeyLocationKind.Header, "Authorization")
)
)
.map(_.replace("Basic ", ""))
.flatMap(e => Try(decodeBase64(e)).toOption)
.orElse(
req.queryString
.get(
constraints.basicAuth.queryName
.getOrElse(env.Headers.OtoroshiBasicAuthorization)
)
.flatMap(_.lastOption)
.flatMap(e => Try(decodeBase64(e)).toOption)
.seffectOnWithPredicate(_.isDefined)(_ =>
location = ApikeyLocation(
ApikeyLocationKind.Query,
constraints.basicAuth.queryName.getOrElse(env.Headers.OtoroshiBasicAuthorization)
)
)
)
.map(_.split(":"))
.collect {
case arr if arr.length == 2 => arr
case arr if arr.length > 2 => Array(arr.head, arr.tail.mkString(":"))
}
.map(parts => ApikeyTuple(parts.head, parts.lastOption, location = location.some, otoBearer = None))
val authByCustomHeaders: Option[ApikeyTuple] = req.headers
.get(
constraints.customHeadersAuth.clientIdHeaderName
.getOrElse(env.Headers.OtoroshiClientId)
)
.seffectOnWithPredicate(_.isDefined)(_ =>
location = ApikeyLocation(
ApikeyLocationKind.Header,
constraints.customHeadersAuth.clientIdHeaderName.getOrElse(env.Headers.OtoroshiClientId)
)
)
.flatMap(id =>
req.headers
.get(
constraints.customHeadersAuth.clientSecretHeaderName
.getOrElse(env.Headers.OtoroshiClientSecret)
)
.map(s => ApikeyTuple(id, s.some, location = location.some, otoBearer = None))
)
val authBySimpleApiKeyClientId: Option[ApikeyTuple] = req.headers
.get(
constraints.clientIdAuth.headerName
.getOrElse(env.Headers.OtoroshiSimpleApiKeyClientId)
)
.seffectOnWithPredicate(_.isDefined)(_ =>
location = ApikeyLocation(
ApikeyLocationKind.Header,
constraints.clientIdAuth.headerName.getOrElse(env.Headers.OtoroshiSimpleApiKeyClientId)
)
)
.orElse(
req.queryString
.get(
constraints.clientIdAuth.queryName
.getOrElse(env.Headers.OtoroshiSimpleApiKeyClientId)
)
.flatMap(_.lastOption)
.seffectOnWithPredicate(_.isDefined)(_ =>
location = ApikeyLocation(
ApikeyLocationKind.Query,
constraints.clientIdAuth.queryName.getOrElse(env.Headers.OtoroshiSimpleApiKeyClientId)
)
)
)
.map(v => ApikeyTuple(v, location = location.some, otoBearer = None))
val preExtractedApiKey = attrs
.get(otoroshi.plugins.Keys.ApiKeyKey)
.map(a => ApikeyTuple(a.clientId, a.clientSecret.some, location = None, otoBearer = None))
.orElse(attrs.get(otoroshi.next.plugins.Keys.PreExtractedApikeyTupleKey))
if (preExtractedApiKey.isDefined) {
preExtractedApiKey
} else if (authBySimpleApiKeyClientId.isDefined && constraints.clientIdAuth.enabled) {
authBySimpleApiKeyClientId
} else if (authByCustomHeaders.isDefined && constraints.customHeadersAuth.enabled) {
authByCustomHeaders
} else if (authByJwtToken.isDefined && constraints.jwtAuth.enabled) {
authByJwtToken
} else if (authBasic.isDefined && constraints.basicAuth.enabled) {
authBasic
} else if (authByOtoBearerToken.isDefined && constraints.otoBearerAuth.enabled) {
authByOtoBearerToken
} else {
None
}
}
}
}
def validateApikeyTuple(
req: RequestHeader,
apikeyTuple: ApikeyTuple,
constraints: ApiKeyConstraints,
service: String,
attrs: TypedMap
)(implicit env: Env): Either[Option[ApiKey], ApiKey] = {
attrs.get(otoroshi.next.plugins.Keys.PreExtractedApikeyKey) match {
case Some(either) => either
case None => {
env.datastores.apiKeyDataStore.findAuthorizeKeyForFromCache(apikeyTuple.clientId, service) match {
case None => None.left
case Some(apikey) =>
apikeyTuple match {
case ApikeyTuple(_, None, None, _, _) if apikey.allowClientIdOnly => apikey.right
case ApikeyTuple(_, Some(secret), None, _, _) if apikey.isValid(secret) => apikey.right
case ApikeyTuple(_, Some(secret), None, _, _) if apikey.isInvalid(secret) => apikey.some.left
case ApikeyTuple(_, None, _, _, Some(otoBearer)) if apikey.checkBearer(otoBearer) => apikey.right
case ApikeyTuple(_, None, _, _, Some(otoBearer)) if !apikey.checkBearer(otoBearer) => apikey.some.left
case ApikeyTuple(_, None, Some(jwt), _, _) => {
val possibleKeyPairId = apikey.metadata.get("jwt-sign-keypair")
val kid = Option(jwt.getKeyId)
.orElse(possibleKeyPairId)
.filter(_ => constraints.jwtAuth.keyPairSigned)
.filter(id => if (possibleKeyPairId.isDefined) possibleKeyPairId.get == id else true)
.flatMap(id => DynamicSSLEngineProvider.certificates.get(id))
val kp = kid.map(_.cryptoKeyPair)
val algorithmOpt: Option[Algorithm] = Option(jwt.getAlgorithm).collect {
case "HS256" if constraints.jwtAuth.secretSigned =>
Algorithm.HMAC256(apikey.clientSecret)
case "HS384" if constraints.jwtAuth.secretSigned =>
Algorithm.HMAC384(apikey.clientSecret)
case "HS512" if constraints.jwtAuth.secretSigned =>
Algorithm.HMAC512(apikey.clientSecret)
case "ES256" if kid.isDefined =>
Algorithm.ECDSA256(
kp.get.getPublic.asInstanceOf[ECPublicKey],
kp.get.getPrivate.asInstanceOf[ECPrivateKey]
)
case "ES384" if kid.isDefined =>
Algorithm.ECDSA384(
kp.get.getPublic.asInstanceOf[ECPublicKey],
kp.get.getPrivate.asInstanceOf[ECPrivateKey]
)
case "ES512" if kid.isDefined =>
Algorithm.ECDSA512(
kp.get.getPublic.asInstanceOf[ECPublicKey],
kp.get.getPrivate.asInstanceOf[ECPrivateKey]
)
case "RS256" if kid.isDefined =>
Algorithm.RSA256(
kp.get.getPublic.asInstanceOf[RSAPublicKey],
kp.get.getPrivate.asInstanceOf[RSAPrivateKey]
)
case "RS384" if kid.isDefined =>
Algorithm.RSA384(
kp.get.getPublic.asInstanceOf[RSAPublicKey],
kp.get.getPrivate.asInstanceOf[RSAPrivateKey]
)
case "RS512" if kid.isDefined =>
Algorithm.RSA512(
kp.get.getPublic.asInstanceOf[RSAPublicKey],
kp.get.getPrivate.asInstanceOf[RSAPrivateKey]
)
}
val exp =
Option(jwt.getClaim("exp")).filterNot(_.isNull).filterNot(_.isMissing).map(_.asLong())
val iat =
Option(jwt.getClaim("iat")).filterNot(_.isNull).filterNot(_.isMissing).map(_.asLong())
val httpPath = Option(jwt.getClaim("httpPath"))
.filterNot(_.isNull)
.filterNot(_.isMissing)
.map(_.asString())
val httpVerb = Option(jwt.getClaim("httpVerb"))
.filterNot(_.isNull)
.filterNot(_.isMissing)
.map(_.asString())
val httpHost = Option(jwt.getClaim("httpHost"))
.filterNot(_.isNull)
.filterNot(_.isMissing)
.map(_.asString())
algorithmOpt match {
case Some(algorithm) => {
val verifier =
JWT
.require(algorithm)
//.withIssuer(clientId)
.acceptLeeway(10)
.build
Try(verifier.verify(jwt))
.filter { token =>
val aud = token.getAudience.asScala.headOption.filter(v =>
v.startsWith("http://") || v.startsWith("https://")
)
if (aud.isDefined) {
val currentUrl = req.theUrl
val audience = aud.get
currentUrl.startsWith(audience)
} else {
true
}
}
.filter { token =>
val xsrfToken = token.getClaim("xsrfToken")
val xsrfTokenHeader = req.headers.get("X-XSRF-TOKEN")
if (!xsrfToken.isNull && !xsrfToken.isMissing && xsrfTokenHeader.isDefined) {
xsrfToken.asString() == xsrfTokenHeader.get
} else if (!xsrfToken.isNull && !xsrfToken.isMissing && xsrfTokenHeader.isEmpty) {
false
} else {
true
}
}
.filter { _ =>
constraints.jwtAuth.maxJwtLifespanSecs.map { maxJwtLifespanSecs =>
if (exp.isEmpty || iat.isEmpty) {
false
} else {
if ((exp.get - iat.get) <= maxJwtLifespanSecs) {
true
} else {
false
}
}
} getOrElse {
true
}
}
.filter { _ =>
if (constraints.jwtAuth.includeRequestAttributes) {
val matchPath = httpPath.exists(_ == req.relativeUri)
val matchVerb =
httpVerb.exists(_.toLowerCase == req.method.toLowerCase)
val matchHost = httpHost.exists(_.toLowerCase == req.theHost)
matchPath && matchVerb && matchHost
} else {
true
}
} match {
case Success(_) =>
attrs.put(
otoroshi.plugins.Keys.ApiKeyJwtKey -> Json.parse(
jwt.getPayload.byteString.decodeBase64.utf8String
)
)
apikey.right
case Failure(e) => apikey.some.left
}
}
case None => apikey.some.left
}
}
case _ => apikey.some.left
}
}
}
}
}
def passWithApiKeyFromCache(
req: RequestHeader,
constraints: ApiKeyConstraints,
attrs: TypedMap,
service: String,
incrementQuotas: Boolean,
routingEnabled: Boolean
)(implicit
ec: ExecutionContext,
env: Env
): Future[Either[Result, ApiKey]] = {
val config = env.datastores.globalConfigDataStore.latest()
def error(status: Results.Status, message: String, code: String): Future[Either[Result, ApiKey]] = {
Errors
.craftResponseResult(message, status, req, None /* TODO: pass the service None */, code.some, attrs = attrs)
.map(Left.apply)
}
def sendRevokedApiKeyAlert(key: ApiKey) = {
Alerts.send(
RevokedApiKeyUsageAlert(env.snowflakeGenerator.nextIdStr(), DateTime.now(), env.env, req, key, None, env)
)
}
def sendQuotasAlmostExceededError(key: ApiKey, quotas: RemainingQuotas): Unit = {
val quotasSettings = config.quotasSettings
if (quotasSettings.enabled) {
if (quotas.currentCallsPerDay >= (quotasSettings.dailyQuotasThreshold * quotas.authorizedCallsPerDay)) {
ApiKeyQuotasAlmostExceededAlert(
`@id` = env.snowflakeGenerator.nextIdStr(),
`@env` = env.env,
apikey = key,
remainingQuotas = quotas,
settings = config.quotasSettings,
reason = ApiKeyQuotasAlmostExceededReason.DailyQuotasAlmostExceeded
).toAnalytics()
}
if (quotas.currentCallsPerMonth >= (quotasSettings.monthlyQuotasThreshold * quotas.authorizedCallsPerMonth)) {
ApiKeyQuotasAlmostExceededAlert(
`@id` = env.snowflakeGenerator.nextIdStr(),
`@env` = env.env,
apikey = key,
remainingQuotas = quotas,
settings = quotasSettings,
reason = ApiKeyQuotasAlmostExceededReason.MonthlyQuotasAlmostExceeded
).toAnalytics()
}
}
}
def sendQuotasExceededError(key: ApiKey, quotas: RemainingQuotas): Unit = {
val quotasSettings = config.quotasSettings
if (quotas.currentCallsPerDay >= (quotasSettings.dailyQuotasThreshold * quotas.authorizedCallsPerDay)) {
ApiKeyQuotasExceededAlert(
`@id` = env.snowflakeGenerator.nextIdStr(),
`@env` = env.env,
apikey = key,
remainingQuotas = quotas,
reason = ApiKeyQuotasExceededReason.DailyQuotasExceeded
).toAnalytics()
}
if (quotas.currentCallsPerMonth >= (quotasSettings.monthlyQuotasThreshold * quotas.authorizedCallsPerMonth)) {
ApiKeyQuotasExceededAlert(
`@id` = env.snowflakeGenerator.nextIdStr(),
`@env` = env.env,
apikey = key,
remainingQuotas = quotas,
reason = ApiKeyQuotasExceededReason.MonthlyQuotasExceeded
).toAnalytics()
}
}
detectApikeyTuple(req, constraints, attrs) match {
case None => error(Results.BadRequest, "no apikey", "errors.no.api.key")
case Some(apikeyTuple) =>
validateApikeyTuple(req, apikeyTuple, constraints, service, attrs) match {
case Left(None) => error(Results.BadRequest, "invalid apikey", "errors.invalid.api.key")
case Left(Some(apikey)) =>
sendRevokedApiKeyAlert(apikey)
error(Results.Unauthorized, "bad apikey", "errors.bad.api.key")
case Right(apikey) if routingEnabled && !apikey.matchRouting(constraints) =>
error(Results.Unauthorized, "invalid apikey", "errors.invalid.api.key")
case Right(apikey) if apikey.restrictions.handleRestrictions(service, None, Some(apikey), req, attrs)._1 => {
apikey.restrictions
.handleRestrictions(service, None, Some(apikey), req, attrs)
._2
.map(v => Left(v))
}
case Right(apikey) => {
apikey.withinQuotasAndRotationQuotas().flatMap {
case (true, rotationInfos, quotas) =>
rotationInfos.foreach { i =>
attrs.put(otoroshi.plugins.Keys.ApiKeyRotationKey -> i)
}
attrs.put(otoroshi.plugins.Keys.ApiKeyRemainingQuotasKey -> quotas)
sendQuotasAlmostExceededError(apikey, quotas)
if (incrementQuotas) {
apikey.updateQuotas().map { remainingQuotas =>
attrs.put(otoroshi.plugins.Keys.ApiKeyRemainingQuotasKey -> remainingQuotas)
apikey.right
}
} else {
apikey.rightf
}
case (false, _, quotas) =>
sendQuotasExceededError(apikey, quotas)
error(Results.TooManyRequests, "You performed too much requests", "errors.too.much.requests")
}
}
}
}
}
}
object OtoroshiBearerToken {
def extractClientId(bearer: String)(implicit env: Env): String = {
bearer
.replaceFirst(s"otoapk_${env.env}", "")
.replaceFirst("otoapk_", "")
.split("_")
.init
.mkString("_")
}
def extractTokenFromRequest(req: RequestHeader, descriptor: ServiceDescriptor)(implicit env: Env): Option[String] = {
extractTokenFromRequest(req, descriptor.apiKeyConstraints)
}
def extractTokenFromRequest(req: RequestHeader, constraints: ApiKeyConstraints)(implicit env: Env): Option[String] = {
req.headers
.get(
constraints.otoBearerAuth.headerName
.getOrElse(env.Headers.OtoroshiBearer)
)
.orElse(
req.headers.get("Authorization").filter(_.startsWith("Bearer "))
)
.map(_.replace("Bearer ", ""))
.orElse(
req.queryString
.get(
constraints.otoBearerAuth.queryName
.getOrElse(env.Headers.OtoroshiBearerAuthorization)
)
.flatMap(_.lastOption)
)
.orElse(
req.queryString
.get(
constraints.otoBearerAuth.queryName
.getOrElse(env.Headers.OtoroshiSimpleApiKeyClientId)
)
.flatMap(_.lastOption)
)
.orElse(
req.headers
.get(
constraints.otoBearerAuth.headerName
.getOrElse(env.Headers.OtoroshiSimpleApiKeyClientId)
)
)
.orElse(
req.cookies
.get(
constraints.otoBearerAuth.cookieName
.getOrElse("bearer")
)
.map(_.value)
)
.filter(v => v.startsWith("otoapk_"))
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy