akka-http Bearer Token Verification with Keycloak/openid-connect

Preface

I always relied on a hand-made custom and minimal authentication scheme to authenticate and authorise users of Angular2-frontends against akka-http APIs. Sessions would be stored and handled in memory (inside a singleton Actor) after matching encryption hashes with a database, all very nicely. Cutting and pasting password-reset-, email-sending- and profile-editing- capabilities into each new API and Angular UI though became a tedious and boring task.

Already sending custom http-request headers with every call to the API (with auth mechanism), coupling the UI with additional microservices – either directly with additional HTTP-APIs or with service-2-service communication via Akka messages – would mean verifying authentication against the auth-API for every call, plus an additional Cluster Singleton in an Akka API/Service cluster.

@OleMchls renewed my interest in OAuth/openid-connect with JSON Web Tokens (JWT)

 

General Idea

OpenID Connect is basically “is a simple identity layer on top of the OAuth 2.0 protocol. It allows Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner” (source: OpenId Connect)

A Bearer Token is “a security token with the property that any party in possession of the token (a “bearer”) can use the token in any way that any other party in possession of it can” (source: RFC 6750)

JSON Web Token (JWT) is “is a compact, URL-safe means of representing claims to be transferred between two parties” (source: RFC 7519)

Using a JWT as a Bearer Token should eventually make the following scenario possible:

A user authenticates via OpenID-connect authentication an gets back a hash like this:


{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSl...",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSld...",
  "token_type": "bearer",
  "not-before-policy": 1508513756,
  "session_state": "65d5b2d5-3431-441d-ae2d-b64a2d809bae"
}

The access token is actually an encoded JWT with a plethora of information inside, cryptographically signed by the issuing authentication service

This token then can be put in a subsequent request’s headers (“Authorization: Bearer <token>”) to access a capable subsystem/microservice (or monolith, of course!)

This subsystem can decode the JWT, verify its integrity and validity by using the authentication issuers’s public key and can actively access the decoded session information (aka. if role.admin? then …) WHILE NOT NEEDING TO CONTACT THE AUTH SERVER.

It might just return something like this for test purposes:


{
  "name": "Sebastian Georgi",
  "email": "sgeorgi@sgeorgi.de",
  "username": "sgeorgi-test",
  "id": "98b1fd03-b70a-4957-a42b-54a2492df322",
  "roles": [
    "admin",
    "uma_authorization"
  ]
}

The general idea would be to make an Angular4 frontend redirect to an auth-server for authentication, get back access- and refresh-token, keep the session alive by keeping the access-token refreshed and do subsequent service requests to subsystems via the Bearer Token Authorization.

Both frontend and backend logic can work with session/user information from the decoded JWT.

An attacker would need to construct and sign a JWT with the configured authentication servers private key to gain authorization for a backend service.

 

Keycloak Server

Keycloak “is an open source Identity and Access Management solution aimed at modern applications and services. It makes it easy to secure applications and services with little to no code.” (source: Keycloak: About).

It’s easy to install, easy to maintain and quite customizable. In fact, I don’t even know what most of the security settings really are. Doesn’t change the fact that Keycloak does the job.

Installing Keycloak was pretty straight-forward (see documentation), although I had and solved some minor problems with putting it behing a nginx-ssl-proxy.

Nevertheless, describing installation and configuring of Keycloak in detail is beyond the scope of this document. The described mechanisms should work with any OpenID-connect authorization server or service.

You’d need some sort of authentication realm/scope with your own userbase, the realms RSA public key to configure the backend services and an template authentication client (to use as a resource name in backend service configuration).

This is my test client:

 

Token Verification

A hypothetical microservice running with Akka-HTTP would first need to include the Keycloak client libraries via Maven (in this case, specified as additional depencencies in build.sbt):

val keycloakDepencencies = Seq(
  "org.keycloak" % "keycloak-adapter-core" % "3.3.0.CR2",
  "org.keycloak" % "keycloak-core" % "3.3.0.CR2",
  "org.jboss.logging" % "jboss-logging" % "3.3.1.Final"
)

 

Add a configuration file (keycloak.json):

{
  "realm": "test",
  "realm-public-key": "MIIBIj...",
  "bearer-only": true,
  "auth-server-url": "https://your.sso.tld/auth",
  "ssl-required": "external",
  "resource": "test-client"
}

 

Then implement the general TokenVerifier:

trait TokenVerifier {
  def verifyToken(token: String): Future[AccessToken]
}
class KeycloakTokenVerifier(keycloakDeployment: KeycloakDeployment) extends TokenVerifier {
  def verifyToken(token: String): Future[AccessToken] = {
    Future {
      RSATokenVerifier.verifyToken(
        token,
        decodePublicKey(pemToDer("MIIBIj...")),
        keycloakDeployment.getRealmInfoUrl
      )
    }
  }

  def pemToDer(pem: String): Array[Byte] = Base64.getDecoder.decode(stripBeginEnd(pem))

  def stripBeginEnd(pem: String): String = {
    var stripped = pem.replaceAll("-----BEGIN (.*)-----", "")
    stripped = stripped.replaceAll("-----END (.*)----", "")
    stripped = stripped.replaceAll("\r\n", "")
    stripped = stripped.replaceAll("\n", "")
    stripped.trim
  }

  def decodePublicKey(der: Array[Byte]): PublicKey = {
    val spec = new X509EncodedKeySpec(der)
    val kf = KeyFactory.getInstance("RSA")

    kf.generatePublic(spec)
  }

This has a lot of Java/Scala dependencies for getting the public key, as well as the actual key again, inside the code. The public key has already been specified in the keycloak configuration, but unfortunately the KeycloakDeployment instance is not willing to return the key, although it has been initialized with the full configuration.

 

Initialize the token verifier in the application’s main class while utilizing the KeycloakDeploymentBuilder (which yields a KeycloakDeployment):

val tokenVerifier = new KeycloakTokenVerifier(
      KeycloakDeploymentBuilder.build(
        getClass.getResourceAsStream("/keycloak.json")
      )
    )

 

Akka-HTTP API

To actually make use of the token verification, add dependencies:

val apiDependencies = Seq(
  "com.typesafe.akka" %% "akka-http-spray-json" % "10.0.10",
  "com.typesafe.akka" %% "akka-http" % "10.0.10"
)

 

Create a case class for the decoded JWT:

case class VerifiedToken(token: String,
                         id: String,
                         name: String,
                         username: String,
                         email: String,
                         roles: Seq[String])

 

Construct Directives for the API:

class OAuth2Authorization(logger: Logger, tokenVerifier: TokenVerifier) {

  def authorizeTokenWithRole(role: String): Directive1[VerifiedToken] = {
    authorizeToken flatMap {
      case t if t.roles.contains(role) => provide(t)
      case _ => reject(AuthorizationFailedRejection).toDirective[Tuple1[VerifiedToken]]
    }
  }

  def authorizeToken: Directive1[VerifiedToken] = {
    bearerToken.flatMap {
      case Some(token) =>
        onComplete(tokenVerifier.verifyToken(token)).flatMap {
          _.map { t =>
            provide(VerifiedToken(token, t.getId, t.getName, t.getPreferredUsername, t.getEmail,
              t.getRealmAccess.getRoles.asScala.toSeq))
          }.recover {
            case ex: Throwable =>
              logger.error("Authorization Token could not be verified", ex)
              reject(AuthorizationFailedRejection).toDirective[Tuple1[VerifiedToken]]
          }.get
        }
      case None =>
        reject(AuthorizationFailedRejection)
    }
  }

  private def bearerToken: Directive1[Option[String]] =
    for {
      authBearerHeader <- optionalHeaderValueByType(classOf[Authorization]).map(extractBearerToken)
      xAuthCookie <- optionalCookie("X-Authorization-Token").map(_.map(_.value))
    } yield authBearerHeader.orElse(xAuthCookie)

  private def extractBearerToken(authHeader: Option[Authorization]): Option[String] =
    authHeader.collect {
      case Authorization(OAuth2BearerToken(token)) => token
    }

}

object OAuth2Authorization {
  def apply(l: Logger, tv: TokenVerifier): OAuth2Authorization = new OAuth2Authorization(l, tv)
}

The authorizeToken -directive simply looks for the Bearer Token inside the request’s HTTP-header or Cookie, verifies the Token with the tokenVerifier and maps the JWT’s decoded content into a case class.

 

For the API side, don’t forget (as I constantly do) to declare an implicit format for the decoded JWT:

trait ApiFormats extends SprayJsonSupport with DefaultJsonProtocol {
  implicit val verifiedTokenFormat: RootJsonFormat[VerifiedToken] = jsonFormat6(VerifiedToken)
}

and implement a basic Router to test the Directives:

class Router(oAuth2Authorization: OAuth2Authorization) extends ApiFormats {

  def route: Route = {
    path("") {
      oAuth2Authorization.authorizeToken { token =>
        complete(StatusCodes.OK, token)
      }
    } ~ path("adminRole") {
      oAuth2Authorization.authorizeTokenWithRole("admin") { token =>
        complete(StatusCodes.OK, token)
      }
    } ~ path("userRole") {
      oAuth2Authorization.authorizeTokenWithRole("user") { token =>
        complete(StatusCodes.OK, token)
      }
    }
  }
}

 

Wrapping it all up

Simply construct an instance of OAuth2Authorization and pass it to the Router’s route:

val oAuth2Authorization = OAuth2Authorization(
    logger, new KeycloakTokenVerifier(
      KeycloakDeploymentBuilder.build(
        getClass.getResourceAsStream("/keycloak.json")
      )
    )
  )

val router = new Router(oAuth2Authorization)

The complete application could look like this:

object Api extends App {
  val logger = Logger(LoggerFactory.getLogger("api"))

  implicit val config: Config = ConfigFactory.load
  implicit val system: ActorSystem = ActorSystem("api", config)
  implicit val materializer: ActorMaterializer = ActorMaterializer()
  implicit val ec: ExecutionContextExecutor = system.dispatcher
  implicit val timeout: Timeout = Timeout(30 seconds)
  val port = 7005

  val oAuth2Authorization = OAuth2Authorization(
    logger, new KeycloakTokenVerifier(
      KeycloakDeploymentBuilder.build(
        getClass.getResourceAsStream("/keycloak.json")
      )
    )
  )

  val router = new Router(oAuth2Authorization)
  val bindingFuture: Future[Http.ServerBinding] = Http().bindAndHandle(router.route, "127.0.0.1", port)
}

 

Testing with Postman

Testing both your authentication service and the JWT-capable service can be done with Postman and some additional scripting in the Tests tab of the initial request to the authentication service. I found this Gist by Ben Howes quite helpful. The script captures the JWT and assigns it to a global variable in Postman for easy reference in subsequent request to the service under test.

Retrieving a Token from the authentication service:

 

Connecting to the test API with the Bearer Token:

 

Find the complete application on my Github repository akka-http-keycloak

I didn’t have time for writing proper ScalaTest tests, yet.

 

Where to go from here

I will be developing an Angular4 application to retrieve, store and refresh the JWT from the Keycloak server. This application then can use the JWT to authorize access to a multitude of JWT-verifying-capable Akka-HTTP-services (like the example service in this post).

Also, customizing the login template for your authorization realm in Keycloak looks pretty doable. Keycloak also offers self-registration, which would of course need the Akka-HTTP directives of a Scala backend to locally persist new users upon first contact to the service if neccesary. Password change/retrieval can also be done with Keycloak out of the box.

That’s quite a nice set of security-related features I would not have to worry about or implement myself for future applications and services!

 

Resources