Skip to content

Commit

Permalink
Scope jwks by issuer uri
Browse files Browse the repository at this point in the history
  • Loading branch information
eikek committed May 22, 2024
1 parent b8866b1 commit d19921a
Show file tree
Hide file tree
Showing 8 changed files with 448 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package io.renku.openid.keycloak

import scala.concurrent.duration.*

import cats.Monad
import cats.data.EitherT
import cats.effect.*
import cats.syntax.all.*
Expand All @@ -44,8 +45,8 @@ final class DefaultJwtVerify[F[_]: Async](

private val logger = scribe.cats.effect[F]

def tryDecode(token: String) =
EitherT(state.get.flatMap(_.jwks.validate(clock)(token)))
def tryDecode(issuer: Uri, token: String): EitherT[F, JwtError, JwtClaim] =
EitherT(state.get.flatMap(_.validate(issuer, clock, token)))

def tryDecodeOnly(token: String): F[Either[JwtError, JwtClaim]] =
JwtBorer.create[F](using clock).map { jwtb =>
Expand All @@ -56,10 +57,24 @@ final class DefaultJwtVerify[F[_]: Async](
}

def verify(token: String): F[Either[JwtError, JwtClaim]] =
if (!config.enableSignatureValidation) tryDecodeOnly(token)
else tryDecode(token).foldF(updateCache(token), _.asRight.pure[F])
tryDecodeOnly(token).flatMap {
case Left(err) => Left(err).pure[F]
case Right(c) if !config.enableSignatureValidation => Right(c).pure[F]
case Right(c) =>
(for
issuer <- EitherT.fromEither(readIssuer(c))
res <- EitherT(
tryDecode(issuer, token).foldF(
updateCache(issuer, token),
_.asRight.pure[F]
)
)
yield res).value
}

def updateCache(token: String)(jwtError: JwtError): F[Either[JwtError, JwtClaim]] =
def updateCache(issuer: Uri, token: String)(
jwtError: JwtError
): F[Either[JwtError, JwtClaim]] =
jwtError match
case JwtError.JwtValidationError(_, _, Some(claim), _) =>
(for
Expand All @@ -68,37 +83,38 @@ final class DefaultJwtVerify[F[_]: Async](
s"Token validation failed, fetch JWKS from keycloak and try again: ${jwtError.getMessage()}"
)
)
jwks <- fetchJWKSGuarded(claim)
jwks <- fetchJWKSGuarded(issuer, claim)
result <- EitherT(jwks.validate(clock)(token))
yield result).value
case e => Left(e).pure[F]

def fetchJWKSGuarded(claim: JwtClaim): EitherT[F, JwtError, Jwks] =
def readIssuer(claim: JwtClaim): Either[JwtError, Uri] =
for
_ <- checkLastUpdateDelay(config.minRequestDelay)
result <- fetchJWKS(claim)
issuerUri <- Uri
.fromString(claim.issuer.getOrElse(""))
.leftMap(ex => JwtError.InvalidIssuerUrl(claim.issuer.getOrElse(""), ex))
_ <- config.checkIssuerUrl(issuerUri)
yield issuerUri

def fetchJWKSGuarded(issuer: Uri, claim: JwtClaim): EitherT[F, JwtError, Jwks] =
for
_ <- checkLastUpdateDelay(issuer, config.minRequestDelay)
result <- fetchJWKS(issuer, claim)
yield result

def checkLastUpdateDelay(min: FiniteDuration): EitherT[F, JwtError, Unit] =
def checkLastUpdateDelay(issuer: Uri, min: FiniteDuration): EitherT[F, JwtError, Unit] =
EitherT(
clock.monotonic.flatMap(ct => state.modify(_.lastUpdateDelay(ct))).map {
clock.monotonic.flatMap(ct => state.modify(_.setLastUpdateDelay(issuer, ct))).map {
case delay if delay > min => Right(())
case _ => Left(JwtError.TooManyValidationRequests(min))
}
)

def fetchJWKS(claim: JwtClaim): EitherT[F, JwtError, Jwks] =
def fetchJWKS(issuerUri: Uri, claim: JwtClaim): EitherT[F, JwtError, Jwks] =
for
_ <- EitherT.right(
clock.monotonic.flatMap(t => state.update(_.copy(lastUpdate = t)))
clock.monotonic.flatMap(t => state.update(_.setLastUpdate(issuerUri, t)))
)
issuerUri <- EitherT.fromEither(
Uri
.fromString(claim.issuer.getOrElse(""))
.leftMap(ex => JwtError.InvalidIssuerUrl(claim.issuer.getOrElse(""), ex))
)
_ <- EitherT.fromEither(config.checkIssuerUrl(issuerUri))

configUri = issuerUri.addPath(config.openIdConfigPath)

_ <- EitherT.right(logger.debug(s"Fetch openid config from $configUri"))
Expand All @@ -110,19 +126,56 @@ final class DefaultJwtVerify[F[_]: Async](
jwks <- EitherT(client.expect[Jwks](GET(openIdCfg.jwksUri)).attempt)
.leftMap(ex => JwtError.JwksError(openIdCfg.jwksUri, ex))

_ <- EitherT.right(state.update(_.copy(jwks = jwks)))
_ <- EitherT.right(state.update(_.setJwks(issuerUri, jwks)))
_ <- EitherT.right(
logger.debug(s"Updated JWKS with keys: ${jwks.keys.map(_.keyId)}")
)
yield jwks

object DefaultJwtVerify:
final case class State(
final case class State(jwks: Map[String, JwksState] = Map.empty):
def get(issuer: Uri): JwksState =
jwks.getOrElse(issuer.renderString, JwksState())

def modify(issuer: Uri, f: JwksState => JwksState): State =
copy(jwks = jwks.updatedWith(issuer.renderString) {
case Some(v) => Some(f(v))
case None => Some(f(JwksState()))
})

def validate[F[_]: Monad](
issuer: Uri,
clock: Clock[F],
token: String
): F[Either[JwtError, JwtClaim]] =
get(issuer).jwks.validate(clock)(token)

def setLastUpdate(issuer: Uri, time: FiniteDuration): State =
modify(issuer, _.copy(lastUpdate = time))

def setJwks(issuer: Uri, data: Jwks): State =
modify(issuer, _.copy(jwks = data))

def setLastUpdateDelay(issuer: Uri, now: FiniteDuration): (State, FiniteDuration) =
val issuerUri = issuer.renderString
val (ns, time) = get(issuer).lastUpdateDelay(now)
(copy(jwks = jwks.updated(issuerUri, ns)), time)

object State:
def of(
issuer: Uri,
jwks: Jwks = Jwks.empty,
lastUpdate: FiniteDuration = Duration.Zero,
lastAccess: FiniteDuration = Duration.Zero
): State =
State(Map(issuer.renderString -> JwksState(jwks, lastUpdate, lastAccess)))

final case class JwksState(
jwks: Jwks = Jwks.empty,
lastUpdate: FiniteDuration = Duration.Zero,
lastAccess: FiniteDuration = Duration.Zero
):
def lastUpdateDelay(now: FiniteDuration): (State, FiniteDuration) =
def lastUpdateDelay(now: FiniteDuration): (JwksState, FiniteDuration) =
(copy(lastAccess = now), now - lastUpdate)

def apply[F[_]: Async](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ package io.renku.openid.keycloak

import scala.concurrent.duration.FiniteDuration

import io.renku.search.common.UrlPattern
import org.http4s.Uri
import pdi.jwt.JwtClaim
import pdi.jwt.JwtHeader
import io.renku.search.common.UrlPattern

sealed trait JwtError extends Throwable

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package io.renku.openid.keycloak

import scala.concurrent.duration.*

import io.renku.search.common.UrlPattern
import org.http4s.Uri

Expand Down
30 changes: 30 additions & 0 deletions modules/openid-keycloak/src/test/resources/jwks2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"keys": [
{
"kid": "rCfowtYHV4LiMvtye-vBX9FReP9_4LQ3HkeNmROzio8",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "q9wq1MDfZILYQHwmWxkYQAU_DvU01QQBlfaOvgT8I20V4ZAkBx8J0YU-bxvMhgm0bKdQHklOVPcW3YeGvst0n63QrSwyCs1J09DUat0nEK3USJvlcN8GqCOQ4hIEaI2KdIXMjVyMKGdwK5ru797jAj51VJTpDtAryP98cc1OsY45HlPTBQNeyuJ8slhY3Fc4I3pMVvk3_jH3_J6DgZPOMP_vDDa_XkIxc1K4vmMpy9oimOTUse4d0ZovYfrYmD1ev770chf52_oL_dDCQstyP4-_rzpojzESf7KUHc5c28cpbJg_O66oDWUelHtypk8xBbbFHJsqBdtGiEOP6QqXcw",
"e": "AQAB",
"x5c": [
"MIICmTCCAYECBgGOw5qkjzANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVSZW5rdTAeFw0yNDA0MDkxNjAyNTVaFw0zNDA0MDkxNjA0MzVaMBAxDjAMBgNVBAMMBVJlbmt1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq9wq1MDfZILYQHwmWxkYQAU/DvU01QQBlfaOvgT8I20V4ZAkBx8J0YU+bxvMhgm0bKdQHklOVPcW3YeGvst0n63QrSwyCs1J09DUat0nEK3USJvlcN8GqCOQ4hIEaI2KdIXMjVyMKGdwK5ru797jAj51VJTpDtAryP98cc1OsY45HlPTBQNeyuJ8slhY3Fc4I3pMVvk3/jH3/J6DgZPOMP/vDDa/XkIxc1K4vmMpy9oimOTUse4d0ZovYfrYmD1ev770chf52/oL/dDCQstyP4+/rzpojzESf7KUHc5c28cpbJg/O66oDWUelHtypk8xBbbFHJsqBdtGiEOP6QqXcwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB6temoA4exVfdIodGpmY+feda89+9cXF/gJrUaIZd5vGby52YB9cGi1xJgmLGaEeDbhooD1A5+q3A1wYUlFk2RFpmf8gNulOjeI4Y5u8S+/Xh1iPpS9VJh9xbveP+Y5QErF4SSQhoqPGQjsPi1Mzf2pmG3Dwi90dwZkQkBcSBSzwrDbWhE72x/oWTVkrG50rYQ6T0WBrE+QtKRaTWtljYIwEa5QRbBri9GNKimejlI9SPy/dz9UlASOmyAaOrKOJdjB5azm2Fovlja/oNgn81RZCW1/OO3IUIn1U/dK0C4ZsEMo4ziAoAOdtIvaCLyB8Ye0TRE/T8RTM0EY2Rmghkh"
],
"x5t": "BR7pyd4Ju_myV8p4T0bXTazAEWk",
"x5t#S256": "9qVv5HdJupxl4hMcBPlBqun8lImmvvVzuI3ECaoVYC0"
},
{
"kid": "pGXWoq0eN3yina5xq6x11R1AXF3F5b_DgRdU2QR3p7s",
"kty": "RSA",
"alg": "RSA-OAEP",
"use": "enc",
"n": "tFCMNgom4585iJxpT4LERAATtQ5y_4XHuPNIRcHtjcFgN3Q4x1-XtApomlz898w_sWEUdCMi-yGcFJe3d6sBe5uTCvrh1XjwGoTqRn7TUOvyNeclKjWnS3H75fU2eVfKNaiUsa-DUgCymvBzbhYu4_5f0Y_WxB81lv4VgtowsXfT8D3GfbyfOwB-na_GtM78_f_0iBMOtEy0uYvZtX8sMKCvyBNavjw4opJSo3nkf3LCz73j9eNxrGS2NscmqSJnxfZcr0dWqlV7zYqeh-uVlZkHHIjJ87816jVs_UeH8TkHiPgjdDFpAXG3VM56z51bPZqUH7_eFj7BX7pkfVAZdQ",
"e": "AQAB",
"x5c": [
"MIICmTCCAYECBgGOw5qlLzANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVSZW5rdTAeFw0yNDA0MDkxNjAyNTVaFw0zNDA0MDkxNjA0MzVaMBAxDjAMBgNVBAMMBVJlbmt1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtFCMNgom4585iJxpT4LERAATtQ5y/4XHuPNIRcHtjcFgN3Q4x1+XtApomlz898w/sWEUdCMi+yGcFJe3d6sBe5uTCvrh1XjwGoTqRn7TUOvyNeclKjWnS3H75fU2eVfKNaiUsa+DUgCymvBzbhYu4/5f0Y/WxB81lv4VgtowsXfT8D3GfbyfOwB+na/GtM78/f/0iBMOtEy0uYvZtX8sMKCvyBNavjw4opJSo3nkf3LCz73j9eNxrGS2NscmqSJnxfZcr0dWqlV7zYqeh+uVlZkHHIjJ87816jVs/UeH8TkHiPgjdDFpAXG3VM56z51bPZqUH7/eFj7BX7pkfVAZdQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB20E7ifqcJwYpzq0KcJnvy744hMFLh21Bvfti7pWn3Bnm/jmORJp2DufiKl8RCMTeG9qqEFqNLIKsxWo9Kh+/2sc58XwwG98Ou8ELCZQAPbMZIuXuorRUAiKWBCTRO1XsJRneptDlLufiJnsO0vaK65C0IhKOjDf2tyvWCnlOBYXc3C+B/xsu4xSot+1bcww3G7KtpnodJTLBOlyXNjnAhIlnBHiIg7iz1pxih1nGO4MWp2DX7ptNytangklGu5vLL0lZObmqK6MXCPSMRGZemphMPVjA3aSsXbp0YViNfwZApbwKx0+ycEF8Xhj7emCWtkfNcZpFkAMaPVQGAIz7E"
],
"x5t": "sPoIN9Q3mXgrGqqWu4i0KDPCWHE",
"x5t#S256": "4B0RQI_l-nNEVSK4pVIfUfXKTQrmAaEVZ-wXBGsTBh8"
}
]
}
1 change: 1 addition & 0 deletions modules/openid-keycloak/src/test/resources/jwt-token2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJyQ2Zvd3RZSFY0TGlNdnR5ZS12Qlg5RlJlUDlfNExRM0hrZU5tUk96aW84In0.eyJleHAiOjE3MTYzMDE4MDYsImlhdCI6MTcxNjMwMDAwNiwianRpIjoiOGE1MGJkNjItYzI1NC00YWIwLWIwOTktOTA0MmYwYzdmNmZhIiwiaXNzIjoiaHR0cHM6Ly9jaS1yZW5rdS0zNTgxLmRldi5yZW5rdS5jaC9hdXRoL3JlYWxtcy9SZW5rdSIsInN1YiI6ImMyMjE4ODFhLTgxZWEtNGU3ZC05YjJjLTJjODg3Y2E5MzJhNyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFkbWluLWNsaSIsInNlc3Npb25fc3RhdGUiOiIxZGM1NGY4Ni1jNjViLTQ3OWItYjg1OC0yNDgzMmRhMmM5ZGMiLCJhY3IiOiIxIiwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiMWRjNTRmODYtYzY1Yi00NzliLWI4NTgtMjQ4MzJkYTJjOWRjIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiRWlrZSBLZXR0bmVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZWlrZS5rZXR0bmVyQHNkc2MuZXRoei5jaCIsImdpdmVuX25hbWUiOiJFaWtlIiwiZmFtaWx5X25hbWUiOiJLZXR0bmVyIiwiZW1haWwiOiJlaWtlLmtldHRuZXJAc2RzYy5ldGh6LmNoIn0.CrbsVwmZDNj-HjNO8ZWbxp1Lo_UchlE23F6zRM1ApmkDRlI3q5eGkuAGd6W17-2mkcSa6hmiKLkEhG-IcuGJOn3wlQNcYptTk0STNwsAm0Hg3HzV2PJ19TsWX-clwu_CLH9vNkVVJDsfNhh2LdhjWCeGyOAIBeOG7TgJEcr8A2JsxhPZs8a-Y-OHDjXIQHjyganvA_fSXuQnjKTv6fvPUhy2N53FV4fHzcLcM7XRVm3MGwzWjq1_jBYRtbtKlbmDlWDCZpmflokdgp2-2DMCC1yStHrc9w338LVUqN9QNISAVJAt4uQCzd2z3EXB-Lncz-9t43WCk6uxEZ1dNBe-Mg
Loading

0 comments on commit d19921a

Please sign in to comment.