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)
}
}
@tsmgeek
Copy link

tsmgeek commented Nov 11, 2018

Did you write this for a specific Scala version?
ive tried to copy getPublicKey into our project and just get lots of errors even after ive added the dependencies.
In my IDE at 102, it does not resolve .hcursor

I modded it slightly to try and use TokenVerifier instead as RSATokenVerifier is deprecated now.

@daniel-shuy
Copy link
Author

Did you write this for a specific Scala version?
ive tried to copy getPublicKey into our project and just get lots of errors even after ive added the dependencies.
In my IDE at 102, it does not resolve .hcursor

I modded it slightly to try and use TokenVerifier instead as RSATokenVerifier is deprecated now.

@tsmgeek It turns out that since I wrote this Gist, there have been significant changes in Circe (used by akka-http-circe) and some minor changes in akka-http and keycloak-core. I have updated the dependencies and the code to match.

Sorry for not replying sooner, unfortunately GitHub does not send notifications for Gists (isaacs/github#21).

@daniel-shuy
Copy link
Author

@arw357
Copy link

arw357 commented Jun 20, 2019

How about :

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

Does the key check for you.

@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