In our previous post, we dived into JWT generation and validation with RSA in Spring. Now let’s see how to do the same in Scala with Akka.
Just a quick recap: we have an “auth server” that signs tokens for us and a “resource server” where we store sensitive data. We trust the auth server and we want to validate that the JWT we get comes from the trusted auth server.
We’ll generate our certs the same way we did last time. The only difference is that instead of saved files we’ll read them from configuration files.
ssh-keygen -t rsa -m PEM
-- enter filename, it was <rsa-key> in my example
ssh-keygen -m PKCS8 -e
-- save the output into <rsa-key.x509.public>
openssl pkcs8 -topk8 -inform pem -in rsa-key -outform pem -nocrypt -out rsa-key.pkcs8.private
-- again rsa-key is my filename, don't forget to change it :)
After executing these commands, we recommend keeping rsa-key.pkcs8.private (for auth server) and rsa-key.x509.public (for resource server). Feel free to get rid of the other files if you want.
First, we need to read our key so we can pass it to the JWT library. Let’s add the private key to our application.conf file:
jwt {
privateKey = """
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCsfacm7I03Hfm8
U+OnZ4HmLdfhUljVtExWzQZc6hpYwyXR2qujBPKB6Ic6YmF23ZEKli5TwbenTaWa
n5hTUA0M6RHuDGG+pETzm/DX54ixngXC/XaV8CX3Oszd4BUCrel/wP8AJ3SUZ99D
LiXQ58MEoSFcpzIkWpHCWhnwFM0mkMUFW8x6eqHWt5Clp5k54kx0DkIEHl0GUNg6
d2oM2NEFh1hStFMRprKZreMCvJveKK270MHRAxfzZQah587p88hlHD6mq/QAmXZ4
ZVN585DEcrtwGqdWE31v7I9eFx4aL1rX8xYXLauJJkW0Mhu0+3WzJY4vP2gJRt6D
9adDMtLDAgMBAAECggEBAKZUVEa4fEPV5+eujSv0J9KqCi4AliEcxzA8bBJUvCsz
otiFoFSGhMK4Uw39qDZS2XX385xYhJwTx8kedGiCHNOCPAPsdKS9CrBOgyPu5GVC
GBQ7DYrwE+wfC0Y4uonm4e6LUFn5sfUZZLUHXvffRLLGHcGWiEd9/mgHMlPL+zdf
dMJUuEqmy+ozcDhWhZSqxD2JLNcrNsfict2L7Zcib7Lvwk19/R7h4lSh0e9VqjDT
ZcJR/7s468lxgTNiulS9s/K1Xwg/zK9I8zAXKZVvHHlSkNfvzTVpQZioRK9myRrc
8mhOm4YtfUi1e5LQ319TazvJjW353BbQOH+UmvDbicECgYEA2t5l8MuH5NOLBjYX
4uoWHH+4CTIG6PCiyTQR5/SjcOsPvzATzkNd7YGdSrgk/dhMB+PZxZwudFxgdVCE
3KvWRD+lrJ9tcSh8QPeQdac4nzMweT1nqnd2F4PO6FcmoGI8Z9fzgJ/tk4ROWvKF
hLdTj6971zNoJRUTMc6NH3LU7ekCgYEAycEItV1RgKZtbRu2i1fZcCr8VMlftqLn
t0yHyD5aAAsLKLGW/yfDq0Fd+QF3m+wsPtSk35omGStnoB/WGRWv8ehWh2iNIIIu
FUUpFfdQqCGSU+ojFE+sW6hgnca17evnz+4vwflLx11qxMpXXc7EhRJXFoFxK7cc
+etcmDCW88sCgYBVjsjE15tY3UUkeXLe9mkMXPUBSzgeSSspghxZ020s0AbI0y96
2yTVmmx1cASt4qbeErjnocUbIZ1nXsGBTf8lkMff8jajHJNuBhjHlUXyHd2eF131
6lsUmCcC9kaYPa6lXWrH5jzGBNtofBOrrMqSiaPcnTDiBhoJx1etaoNIOQKBgA5H
KPSc3A28uXXFRk/qMassf5sIfUuRj9B7DAjx0LC8F1gT6Vm5WLGf+KSMpAhW2HLB
3cEtSZDyb2z3k9FGpaL7DFSc44/vZo9+y3+QdxbO+WoS4dSoJsx9yAiibXGfBlLC
yoJxwBkl1U6D+1baMTIxsBQZqQas+NH/BBiJJ8WtAoGAWi0FiH3eLjcpc+MSzna8
BplVeWAolJnPiaZe5OLo3RhjXKg38fZ2WOZvOL5WI0BeXRib/4zres+CD2/nOdpe
k9RSuI3vDyOV2WTSLBDgAFLWmD+BYBm5BYCZuc8Kbs97UFKHv4cUON/aRvS3h3/s
gFjKFVyVEpMccyZdhpXSKKE=
-----END PRIVATE KEY-----
"""
}
Now we need to include pureconfig and read the privateKey.
package com.wanari.jwt.example.authserver.config
import pureconfig.generic.ProductHint
import pureconfig.{CamelCase, ConfigFieldMapping, ConfigSource}
class Config {
import Config._
import ConfigSource.default.at
import pureconfig.generic.auto._
implicit def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(CamelCase, CamelCase))
implicit val jwtConf: JwtConf = at("jwt").loadOrThrow[JwtConf]
}
object Config {
case class JwtConf(
privateKey: String,
)
}
Next step: generating the JWT.
package com.wanari.jwt.example.authserver.api
import com.wanari.jwt.example.authserver.api.JwtAuthentication.JwtPayload
import com.wanari.jwt.example.authserver.config.Config.JwtConf
import pdi.jwt.{JwtAlgorithm, JwtSprayJson}
import spray.json.DefaultJsonProtocol._
import spray.json._
class JwtAuthentication(jwtConf: JwtConf) {
def generateToken(username: String): String = {
val claim = JwtPayload(username).toJson.asJsObject
JwtSprayJson.encode(claim, jwtConf.privateKey, JwtAlgorithm.RS512)
}
}
object JwtAuthentication {
implicit val jwtPayloadFormatter: RootJsonFormat[JwtPayload] = jsonFormat1(JwtPayload)
final case class JwtPayload(
username: String,
)
}
All there’s left to do is implement an API to send back the JWT as a response. Looking at the full source code, you might notice that we haven’t protected this API with any authentication. Don’t try this at home, kids.
Now comes the part where we implement the JWT validation. We will use the previously generated publicKey as a configuration value in the resource server.
jwt {
publicKey = """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArH2nJuyNNx35vFPjp2eB
5i3X4VJY1bRMVs0GXOoaWMMl0dqrowTygeiHOmJhdt2RCpYuU8G3p02lmp+YU1AN
DOkR7gxhvqRE85vw1+eIsZ4Fwv12lfAl9zrM3eAVAq3pf8D/ACd0lGffQy4l0OfD
BKEhXKcyJFqRwloZ8BTNJpDFBVvMenqh1reQpaeZOeJMdA5CBB5dBlDYOndqDNjR
BYdYUrRTEaayma3jAryb3iitu9DB0QMX82UGoefO6fPIZRw+pqv0AJl2eGVTefOQ
xHK7cBqnVhN9b+yPXhceGi9a1/MWFy2riSZFtDIbtPt1syWOLz9oCUbeg/WnQzLS
wwIDAQAB
-----END PUBLIC KEY-----
"""
}
We’ll read this config value the same way as before. (smile)
Now the only things left to do are to decode and validate the token.
package com.wanari.jwt.example.resourceserver.api
import akka.http.scaladsl.server.Directive1
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.directives.Credentials
import com.wanari.jwt.example.resourceserver.api.JwtAuthentication.JwtPayload
import com.wanari.jwt.example.resourceserver.config.Config.JwtConf
import pdi.jwt.{JwtAlgorithm, JwtSprayJson}
import spray.json.DefaultJsonProtocol._
import spray.json.RootJsonFormat
class JwtAuthentication(jwtConf: JwtConf) {
def authenticate: Directive1[JwtPayload] = {
authenticateOAuth2[JwtPayload](realm = "", validateCredentials)
}
private def validateCredentials(credentials: Credentials): Option[JwtPayload] = {
credentials match {
case Credentials.Provided(token) =>
decode(token)
case Credentials.Missing =>
None
}
}
private def decode(jwt: String): Option[JwtPayload] = {
JwtSprayJson
.decodeJson(jwt, jwtConf.publicKey, Seq(JwtAlgorithm.RS256))
.map(_.convertTo[JwtPayload])
.toOption
}
}
object JwtAuthentication {
implicit val jwtPayloadFormatter: RootJsonFormat[JwtPayload] = jsonFormat1(JwtPayload)
final case class JwtPayload(
username: String,
)
}
As you can see, we’re using the same JwtPayload class as before. (smile)
Once we’ve written this little piece of code, we can use ‘authenticate’ the same way as any other directive that Akka Http defines. Let’s see an example API for this:
package com.wanari.jwt.example.resourceserver.api
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
class SecretApi(jwtAuthentication: JwtAuthentication) {
import jwtAuthentication.authenticate
val getSecret: Route = path("secret") {
authenticate { jwtPayload =>
complete(s"Welcome ${jwtPayload.username}!")
}
}
}
All we need to do is call the auth server’s API, prefix the given token with “Bearer” and put it in the Authorization header in case we want to call the API on the resource server.
Intrigued? Download the whole source code from our GitHub page and give it a try yourself. Remember to call the resource server’s /secret API with the JWT generated by the auth server – and there you have it!
We hope you’ve enjoyed this tutorial. Stay tuned for more! In the meantime, check out our previous post about the same drill in Spring.
Have questions or ideas on what we should post about next? Leave a comment below – and don’t hold back.