From c02de9e795454da57fcdb90dcacbfd2b8df9e775 Mon Sep 17 00:00:00 2001 From: Mathieu Ancelin Date: Wed, 6 Nov 2024 10:21:58 +0100 Subject: [PATCH] fix #2022 --- otoroshi/app/models/apikey.scala | 31 +++++++++---------- .../app/next/plugins/clientcredentials.scala | 20 +++++++----- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/otoroshi/app/models/apikey.scala b/otoroshi/app/models/apikey.scala index cd041edb1..b58cf8542 100644 --- a/otoroshi/app/models/apikey.scala +++ b/otoroshi/app/models/apikey.scala @@ -10,16 +10,7 @@ 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.events.{Alerts, ApiKeyQuotasAlmostExceededAlert, ApiKeyQuotasAlmostExceededReason, ApiKeyQuotasExceededAlert, ApiKeyQuotasExceededReason, ApiKeySecretHasRotated, ApiKeySecretWillRotate, RevokedApiKeyUsageAlert} import otoroshi.gateway.Errors import org.joda.time.DateTime import otoroshi.next.plugins.api.NgAccess @@ -31,16 +22,11 @@ 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 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( @@ -1835,6 +1821,8 @@ object ApiKeyHelper { 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 aud = jwt.getAudience.asScala.headOption.filter(v => v.startsWith("http://") || v.startsWith("https://")) + println(s"audience is: ${aud}") val kid = Option(jwt.getKeyId) .orElse(possibleKeyPairId) .filter(_ => constraints.jwtAuth.keyPairSigned) @@ -1904,6 +1892,15 @@ object ApiKeyHelper { .acceptLeeway(10) .build Try(verifier.verify(jwt)) + .filter { token => + 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") diff --git a/otoroshi/app/next/plugins/clientcredentials.scala b/otoroshi/app/next/plugins/clientcredentials.scala index 329f099c2..1d5058241 100644 --- a/otoroshi/app/next/plugins/clientcredentials.scala +++ b/otoroshi/app/next/plugins/clientcredentials.scala @@ -8,6 +8,7 @@ import org.biscuitsec.biscuit.datalog.SymbolTable import org.biscuitsec.biscuit.token.builder.parser.Parser import org.joda.time.DateTime import otoroshi.env.Env +import otoroshi.models.ApiKeyHelper import otoroshi.next.plugins.api._ import otoroshi.next.proxy.NgProxyEngineError import otoroshi.plugins.apikeys.ClientCredentialFlowBody @@ -476,7 +477,8 @@ case class NgClientCredentialTokenEndpointBody( clientId: String, clientSecret: String, scope: Option[String], - bearerKind: String + bearerKind: String, + aud: Option[String], ) case class NgClientCredentialTokenEndpointConfig(expiration: FiniteDuration, defaultKeyPair: String) extends NgPluginConfig { @@ -508,7 +510,7 @@ class NgClientCredentialTokenEndpoint extends NgBackendCall { override def name: String = "Client credential token endpoint" override def description: Option[String] = - "This plugin provide the endpoint for the client_credential flow token endpoint".some + "This plugin provide the endpoint for the client_credential flow".some override def useDelegates: Boolean = false @@ -579,7 +581,7 @@ class NgClientCredentialTokenEndpoint extends NgBackendCall { ctx: NgbBackendCallContext )(implicit env: Env, ec: ExecutionContext): Future[Result] = ccfb match { - case NgClientCredentialTokenEndpointBody("client_credentials", clientId, clientSecret, scope, bearerKind) => { + case NgClientCredentialTokenEndpointBody("client_credentials", clientId, clientSecret, scope, bearerKind, aud) => { val possibleApiKey = env.datastores.apiKeyDataStore.findById(clientId) possibleApiKey.flatMap { case Some(apiKey) if apiKey.isValid(clientSecret) && apiKey.isActive() => { @@ -604,7 +606,7 @@ class NgClientCredentialTokenEndpoint extends NgBackendCall { .withClaim("cid", apiKey.clientId) .withIssuer(ctx.rawRequest.theProtocol + "://" + ctx.rawRequest.host) .withSubject(apiKey.clientId) - .withAudience("otoroshi") + .withAudience(aud.getOrElse("otoroshi")) .withKeyId(keyPairId) .sign(algo) // no refresh token possible because of https://tools.ietf.org/html/rfc6749#section-4.4.3 @@ -675,11 +677,12 @@ class NgClientCredentialTokenEndpoint extends NgBackendCall { body.get("client_id"), body.get("client_secret"), body.get("scope"), - body.get("bearer_kind") + body.get("bearer_kind"), + body.get("aud"), ) match { - case (Some(gtype), Some(clientId), Some(clientSecret), scope, kind) => + case (Some(gtype), Some(clientId), Some(clientSecret), scope, kind, aud) => handleTokenRequest( - NgClientCredentialTokenEndpointBody(gtype, clientId, clientSecret, scope, kind.getOrElse("jwt")), + NgClientCredentialTokenEndpointBody(gtype, clientId, clientSecret, scope, kind.getOrElse("jwt"), aud), config, ctx ) @@ -700,7 +703,8 @@ class NgClientCredentialTokenEndpoint extends NgBackendCall { clientId, clientSecret, None, - body.getOrElse("bearer_kind", "jwt") + body.getOrElse("bearer_kind", "jwt"), + body.get("aud") ), config, ctx