Skip to content

Instantly share code, notes, and snippets.

@hoangong
Forked from daniel-shuy/KeycloakAuthorization.scala
Last active February 11, 2022 17:56
Show Gist options
  • Save hoangong/b55ca362bb7a29b99cf0f49720c1dd75 to your computer and use it in GitHub Desktop.
Save hoangong/b55ca362bb7a29b99cf0f49720c1dd75 to your computer and use it in GitHub Desktop.
Akka HTTP (Scala) Keycloak token verifier #scala #akka-http #keycloak
import java.math.BigInteger
import java.security.spec.RSAPublicKeySpec
import java.security.{KeyFactory, PublicKey}
import java.util.Base64
import akka.actor.ActorSystem
import akka.event.LoggingAdapter
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.HttpRequest
import akka.http.scaladsl.model.headers.{Authorization, OAuth2BearerToken}
import akka.http.scaladsl.server.Directives.{onComplete, optionalCookie, optionalHeaderValueByType, provide, reject}
import akka.http.scaladsl.server.{AuthorizationFailedRejection, Directive1}
import akka.http.scaladsl.unmarshalling.{PredefinedFromEntityUnmarshallers, Unmarshal}
import akka.stream.ActorMaterializer
import de.heikoseeberger.akkahttpcirce.ErrorAccumulatingCirceSupport
import io.circe._
import org.keycloak.TokenVerifier
import org.keycloak.adapters.KeycloakDeployment
import org.keycloak.common.VerificationException
import org.keycloak.jose.jws.{AlgorithmType, JWSHeader}
import org.keycloak.representations.AccessToken
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
class KeycloakAuthorization(logger: LoggingAdapter, keycloakDeployment: KeycloakDeployment,
realm: String, implicit val system: ActorSystem) extends ErrorAccumulatingCirceSupport {
private implicit val mat: ActorMaterializer = ActorMaterializer()
def authorized: Directive1[AccessToken] = {
bearerToken.flatMap {
case Some(token) =>
onComplete(verifyToken(token)).flatMap {
_.map(accessToken => provide(accessToken))
.recover {
case e =>
logger.error(e, "Couldn't log in using provided authorization token")
reject(AuthorizationFailedRejection)
.toDirective[Tuple1[AccessToken]]
}
.get
}
case None =>
reject(AuthorizationFailedRejection)
}
}
/**
* Obtain Bearer Token from Authentication Header.
* Fallback to X-Authorization-Token Cookie on failure.
*
* @return
* The Bearer Token.
*/
private def bearerToken: Directive1[Option[String]] = {
for {
authBearerHeader <- optionalHeaderValueByType(classOf[Authorization])
.map(authHeader => authHeader.collect {
case Authorization(OAuth2BearerToken(token)) => token
})
xAuthCookie <- optionalCookie("X-Authorization-Token")
.map(_.map(cookie => cookie.value))
} yield authBearerHeader.orElse(xAuthCookie)
}
private def verifyToken(token: String): Future[AccessToken] = {
val tokenVerifier = TokenVerifier.create(token, classOf[AccessToken])
.withDefaultChecks()
getPublicKey(tokenVerifier.getHeader).map {
case Left(error) =>
throw new VerificationException("Error deserializing public key from Keycloak.", error)
case Right(option) => option match {
case Some(publicKey) =>
tokenVerifier.publicKey(publicKey).verify().getToken
case None =>
throw new VerificationException("No matching public key found.")
}
}
}
/**
* Because Keycloak supports key rotation, we cannot hardcode the public key.
* This method performs a HTTP GET request to Keycloak's OpenID Connect's
* certification endpoint to obtain the public keys, then uses the kid header
* in the JWS Header to find the public key that corresponds with the
* private key used to generate the token.
*
* This example uses akka-http-json and circe to unmarshal the JSON response
* from the GET request.
*
* @see
* https://gist.github.com/thomasdarimont/52152ed68486c65b50a04fcf7bd9bbde
*
* @param jwsHeader
* The JSON Web Signature header.
* @return
* The current public key.
*/
private def getPublicKey(jwsHeader: JWSHeader): Future[Either[DecodingFailure, Option[PublicKey]]] = {
Http().singleRequest(HttpRequest(uri = keycloakDeployment.getJwksUrl))
.flatMap(response => {PredefinedFromEntityUnmarshallers
Unmarshal(response).to[Json]
.map(json => json.hcursor
.get[List[Map[String, String]]]("keys")
.map(keys => keys
.find(key => key("kid") == jwsHeader.getKeyId)
.map(key => {
val keyFactory = KeyFactory.getInstance(AlgorithmType.RSA.toString)
val modulusBase64 = key("n")
val exponentBase64 = key("e")
val urlDecoder = Base64.getUrlDecoder
val modulus = new BigInteger(1, urlDecoder.decode(modulusBase64))
val publicExponent = new BigInteger(1, urlDecoder.decode(exponentBase64))
keyFactory.generatePublic(new RSAPublicKeySpec(modulus, publicExponent))
})
)
)
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment