Skip to content

Instantly share code, notes, and snippets.

@daniel-shuy
Last active February 11, 2022 17:56
Show Gist options
  • Save daniel-shuy/62ef61a3e64bd1764931a8d3dda36e3e to your computer and use it in GitHub Desktop.
Save daniel-shuy/62ef61a3e64bd1764931a8d3dda36e3e to your computer and use it in GitHub Desktop.
Akka HTTP (Scala) Keycloak token verifier
val akkaVersion = "2.5.23"
val akkaHttpVersion = "10.1.8"
val keycloakVersion = "6.0.1"
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-http" % akkaHttpVersion,
"com.typesafe.akka" %% "akka-stream" % akkaVersion,
"org.keycloak" % "keycloak-adapter-core" % keycloakVersion,
"org.keycloak" % "keycloak-core" % keycloakVersion,
)
import akka.actor.ActorSystem
import akka.dispatch.MessageDispatcher
import akka.event.LoggingAdapter
import akka.http.scaladsl.model.headers.{Authorization, OAuth2BearerToken}
import akka.http.scaladsl.server.Directives.{optionalCookie, optionalHeaderValueByType, provide, reject}
import akka.http.scaladsl.server.{AuthorizationFailedRejection, Directive1, Directives}
import org.keycloak.adapters.KeycloakDeployment
import org.keycloak.adapters.rotation.AdapterTokenVerifier
import org.keycloak.representations.AccessToken
import scala.concurrent.Future
class KeycloakAuthorization(logger: LoggingAdapter,
keycloakDeployment: KeycloakDeployment,
realm: String,
implicit val system: ActorSystem) {
private implicit val executionContext: MessageDispatcher =
system.dispatchers.lookup("auth-dispatcher")
def authorized: Directive1[AccessToken] = {
bearerToken.flatMap {
case Some(token) =>
Directives
.onComplete(Future {
AdapterTokenVerifier.verifyToken(token, keycloakDeployment)
})
.flatMap {
provide(AdapterTokenVerifier.verifyToken(token, keycloakDeployment))
_.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)
}
}
@el-dom
Copy link

el-dom commented Jun 21, 2019

@arw357 If you use AdapterTokenVerifier.verifyToken you'll have a blocking call to keycloak (via PublicKeyLocator). Considering performance, i don't think that's a good idea.

@daniel-shuy
Copy link
Author

How about :

  def verifyToken(token: String): AccessToken = {
     AdapterTokenVerifier.verifyToken(token, keycloakDeployment)
  }

Does the key check for you.

@arw357 Ah I wasn't aware of AdapterTokenVerifier, it handles all of that (key rotation) automatically. I've updated this Gist to use it instead.

@daniel-shuy
Copy link
Author

@arw357 If you use AdapterTokenVerifier.verifyToken you'll have a blocking call to keycloak (via PublicKeyLocator). Considering performance, i don't think that's a good idea.

@el-dom The blocking call can be wrapped in a Future to run on a separate dispatcher from the Akka HTTP routing dispatcher. I've updated this Gist to run it on a new dispatcher called auth-dispatcher, which needs to be configured in application.conf (see https://doc.akka.io/docs/akka-http/current/handling-blocking-operations-in-akka-http-routes.html#solution-dedicated-dispatcher-for-blocking-operations).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment