Instantly share code, notes, and snippets.
Forked from daniel-shuy/KeycloakAuthorization.scala
Last active
February 11, 2022 17:56
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(1)
1
You must be signed in to fork a gist
-
Save hoangong/b55ca362bb7a29b99cf0f49720c1dd75 to your computer and use it in GitHub Desktop.
Akka HTTP (Scala) Keycloak token verifier #scala #akka-http #keycloak
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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