next.plugins.biscuit.scala Maven / Gradle / Ivy
package otoroshi.next.plugins
import akka.Done
import org.biscuitsec.biscuit.crypto.PublicKey
import org.biscuitsec.biscuit.token.builder.Term.Str
import org.biscuitsec.biscuit.token.{Authorizer, Biscuit}
import otoroshi.env.Env
import otoroshi.gateway.Errors
import otoroshi.models.{ApiKey, PrivateAppsUser, ServiceDescriptor}
import otoroshi.next.plugins.api._
import otoroshi.plugins.biscuit._
import otoroshi.utils.crypto.Signatures
import otoroshi.utils.syntax.implicits._
import play.api.libs.json._
import play.api.mvc.{RequestHeader, Results}
import scala.concurrent.{ExecutionContext, Future}
import scala.util._
case class PreRoutingVerifierContext(ctx: NgPreRoutingContext, apk: ApiKey) extends VerificationContext {
override def request: RequestHeader = ctx.request
override def descriptor: ServiceDescriptor = ctx.route.legacy
override def apikey: Option[ApiKey] = apk.some
override def user: Option[PrivateAppsUser] = None
}
case class AccessValidatorContext(ctx: NgAccessContext) extends VerificationContext {
override def request: RequestHeader = ctx.request
override def descriptor: ServiceDescriptor = ctx.route.legacy
override def apikey: Option[ApiKey] = ctx.apikey
override def user: Option[PrivateAppsUser] = ctx.user
}
case class NgBiscuitConfig(
legacy: BiscuitConfig = BiscuitConfig(
publicKey = None,
checks = Seq.empty,
facts = Seq.empty,
resources = Seq.empty,
rules = Seq.empty,
revocation_ids = Seq.empty,
extractor = "header",
extractorName = "Authorization",
enforce = false
)
) extends NgPluginConfig {
override def json: JsValue = Json.obj(
"public_key" -> legacy.publicKey,
"checks" -> legacy.checks,
"facts" -> legacy.facts,
"resources" -> legacy.resources,
"rules" -> legacy.rules,
"revocation_ids" -> legacy.revocation_ids,
"extractor" -> Json.obj(
"name" -> legacy.extractorName,
"type" -> legacy.extractor
),
"enforce" -> legacy.enforce
)
}
object NgBiscuitConfig {
val format = new Format[NgBiscuitConfig] {
override def writes(o: NgBiscuitConfig): JsValue = o.json
override def reads(json: JsValue): JsResult[NgBiscuitConfig] = Try {
NgBiscuitConfig(
legacy = BiscuitHelper.readConfigFromJson(json)
)
} match {
case Failure(e) => JsError(e.getMessage)
case Success(c) => JsSuccess(c)
}
}
}
class NgBiscuitExtractor extends NgPreRouting {
import collection.JavaConverters._
override def name: String = "Apikey from Biscuit token extractor"
override def description: Option[String] =
"This plugin extract an from a Biscuit token where the biscuit has an #authority fact 'client_id' containing\napikey client_id and an #authority fact 'client_sign' that is the HMAC256 signature of the apikey client_id with the apikey client_secret".some
override def defaultConfigObject: Option[NgPluginConfig] = NgBiscuitConfig().some
override def multiInstance: Boolean = true
override def core: Boolean = true
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.AccessControl)
override def steps: Seq[NgStep] = Seq(NgStep.PreRoute)
// TODO: check if it's a bug, first letter is missing in parsed rule (lient_id instead of client_id)
// val ruleTuple = Parser.rule("client_id($id) <- client_id(#authority, $id) @ []").get()
private val client_id_rule = org.biscuitsec.biscuit.token.builder.Utils.rule(
"client_id_res",
Seq(org.biscuitsec.biscuit.token.builder.Utils.`var`("id")).asJava,
Seq(
org.biscuitsec.biscuit.token.builder.Utils.pred(
"client_id",
Seq(
org.biscuitsec.biscuit.token.builder.Utils.s("authority"),
org.biscuitsec.biscuit.token.builder.Utils.`var`("id")
).asJava
)
).asJava
)
private val client_sign_rule = org.biscuitsec.biscuit.token.builder.Utils.rule(
"client_sign_res",
Seq(org.biscuitsec.biscuit.token.builder.Utils.`var`("sign")).asJava,
Seq(
org.biscuitsec.biscuit.token.builder.Utils.pred(
"client_sign",
Seq(
org.biscuitsec.biscuit.token.builder.Utils.s("authority"),
org.biscuitsec.biscuit.token.builder.Utils.`var`("sign")
).asJava
)
).asJava
)
def unauthorized(error: JsObject): Future[Either[NgPreRoutingError, Done]] = {
NgPreRoutingErrorWithResult(Results.Unauthorized(error)).leftf
}
override def preRoute(
ctx: NgPreRoutingContext
)(implicit env: Env, ec: ExecutionContext): Future[Either[NgPreRoutingError, Done]] = {
val config = ctx.cachedConfig(internalName)(NgBiscuitConfig.format).getOrElse(NgBiscuitConfig())
def verification(verifier: Authorizer): Future[Either[NgPreRoutingError, Done]] = {
val client_id: Option[String] = Try(verifier.query(client_id_rule)).toOption
.map(_.asScala)
.flatMap(_.headOption)
.filter(_.name() == "client_id_res")
.map(_.terms().asScala)
.flatMap(_.headOption)
.flatMap {
case str: Str => str.getValue().some
case _ => None
}
val client_sign: Option[String] = Try(verifier.query(client_sign_rule)).toOption
.map(_.asScala)
.flatMap(_.headOption)
.filter(_.name() == "client_sign_res")
.map(_.terms().asScala)
.flatMap(_.headOption)
.flatMap {
case str: Str => str.getValue().some
case _ => None
}
(client_id, client_sign) match {
case (Some(client_id), Some(client_sign)) => {
env.datastores.apiKeyDataStore.findById(client_id).flatMap {
case Some(apikey) if apikey.isInactive() && config.legacy.enforce =>
unauthorized(Json.obj("error" -> "unauthorized", "error_description" -> "bad_apikey"))
case Some(apikey) if apikey.isInactive() => Done.rightf
case Some(apikey) => {
val nextSignedOk = apikey.rotation.nextSecret
.map(s => Signatures.hmacSha256Sign(client_id, s))
.contains(client_sign)
val signed = Signatures.hmacSha256Sign(client_id, apikey.clientSecret)
if (signed == client_sign || nextSignedOk) {
BiscuitHelper.verify(verifier, config.legacy, PreRoutingVerifierContext(ctx, apikey)) match {
case Left(err) if config.legacy.enforce =>
unauthorized(
Json.obj("error" -> "unauthorized", "error_description" -> s"verification error: $err")
)
case Left(_) => Done.rightf
case Right(_) => {
// println(biscuit.print())
// println(verifier.print_world())
ctx.attrs.put(otoroshi.plugins.Keys.ApiKeyKey -> apikey)
Done.rightf
}
}
} else if (config.legacy.enforce) {
unauthorized(Json.obj("error" -> "unauthorized", "error_description" -> "bad_apikey"))
} else {
Done.rightf
}
}
case _ => Done.rightf
}
}
case _ => Done.rightf
}
}
BiscuitHelper.extractToken(ctx.request, config.legacy) match {
case Some(PubKeyBiscuitToken(token)) => {
val pubkey =
new PublicKey(biscuit.format.schema.Schema.PublicKey.Algorithm.Ed25519, config.legacy.publicKey.get)
Try(Biscuit.from_b64url(token, pubkey)).toEither match {
case Left(err) if config.legacy.enforce =>
unauthorized(Json.obj("error" -> "unauthorized", "error_description" -> s"deserialization error: $err"))
case Left(_) => Done.rightf
case Right(biscuit) =>
Try(biscuit.verify(pubkey)).toEither match {
case Left(err) if config.legacy.enforce =>
unauthorized(Json.obj("error" -> "unauthorized", "error_description" -> s"verifier error: $err"))
case Left(_) => Done.rightf
case Right(biscuit) => verification(biscuit.authorizer())
}
}
}
case _ => Done.rightf
}
}
}
class NgBiscuitValidator extends NgAccessValidator {
override def name: String = "Biscuit token validator"
override def description: Option[String] = "This plugin validates a Biscuit token".some
override def defaultConfigObject: Option[NgPluginConfig] = NgBiscuitConfig().some
override def multiInstance: Boolean = true
override def core: Boolean = true
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.AccessControl)
override def steps: Seq[NgStep] = Seq(NgStep.ValidateAccess)
def forbidden(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
Errors
.craftResponseResult(
"forbidden",
Results.Forbidden,
ctx.request,
None,
None,
duration = ctx.report.getDurationNow(),
overhead = ctx.report.getOverheadInNow(),
attrs = ctx.attrs,
maybeRoute = ctx.route.some
)
.map(r => NgAccess.NgDenied(r))
}
override def access(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
val config = ctx.cachedConfig(internalName)(NgBiscuitConfig.format).getOrElse(NgBiscuitConfig())
BiscuitHelper.extractToken(ctx.request, config.legacy) match {
case Some(PubKeyBiscuitToken(token)) => {
val pubkey =
new PublicKey(biscuit.format.schema.Schema.PublicKey.Algorithm.Ed25519, config.legacy.publicKey.get)
Try(Biscuit.from_b64url(token, pubkey)).toEither match {
case Left(_) => forbidden(ctx)
case Right(biscuit) =>
Try(biscuit.verify(pubkey)).toEither match {
case Left(_) => forbidden(ctx)
case Right(verifier) => {
BiscuitHelper.verify(verifier.authorizer(), config.legacy, AccessValidatorContext(ctx)) match {
case Left(_) => forbidden(ctx)
case Right(_) => NgAccess.NgAllowed.vfuture
}
}
}
}
}
case _ if config.legacy.enforce => forbidden(ctx)
case _ if !config.legacy.enforce => NgAccess.NgAllowed.vfuture
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy