close icon
Scala

Build and Secure APIs with Scala and the Play Framework

Learn how to build APIs with Scala and the Play Framework, and secure them using access tokens.

November 19, 2018

TL;DR: In this article we're using to use Scala and the Play Framework to build an API that serves blog posts and comments, and then secure the API using access tokens. We will build the API from scratch over the course of this article, but if you'd like to see the final result, you can find the sample code on GitHub.com.


Let's build an API using Scala and the Play Framework! This API is going to be fairly simple. It will have two endpoints — one for serving blog posts, and one for serving comments — that we can then secure using access tokens. That is, clients without a valid access token will not be able to access the blog data.

We'll start by building out the API, and then later we'll use Auth0 to issue our access tokens. We'll then validate these tokens and decide whether or not the connected client should access the requested resource or not.

Prerequisites

To follow along with this article, you will want to have the following software installed:

  • SBT
    • built using Scala for Scala projects, this is the CLI that enables you to compile and run Scala programs
  • An appropriate editor or IDE. I used IntelliJ IDEA to build this tutorial. They have a free community edition if you want to do the same, and don't already have an IDE

Note IntelliJ requires the Scala plugin to be installed in order to work with Scala projects. To install the plugin, read the first section "Install Scala plugin" on the IntelliJ website. If you are using a different IDE such as Eclipse or Netbeans, please check to see if you need to install any plugins for Scala development.

In addition, you should have a basic working knowledge of Scala and some of its basic types and control flows, as we'll not be covering them in detail here. If you're new to Scala, you might be better taking the tour of Scala before returning to complete this tutorial.

Setting Up the Project

To begin, find a place on your hard-drive using the terminal to start a new project. We're going to be initializing our project using SBT and Giter8, which is a templating CLI that is used from the SBT console and can load project templates from GitHub. Play Framework has a set of Giter8 templates that we can use to kick-start our project without using an IDE.

Scaffold the project in the terminal now, using the following command. As part of the running of this command, you will be asked for a project name. A folder will be created with this name, and your project code will be placed inside:

sbt new playframework/play-scala-seed.g8

You will also be asked for an organization name. I left mine as the default of com.example.

Next, open the project in your editor of choice, or use the 'import' feature if you're using IntelliJ. If you are given a choice as to which build tool you'd like to use, make sure you select 'sbt'.

Before we dive into the project code, we're going to clean up the default project a little bit and remove the stuff we don't need. You can do this by removing the controllers, views, and public assets that we're not going to use:

# Remove unused folders
rm -rf app/controllers/HomeController.scala app/views public test

# Clear out the routes, as they'll no longer compile
echo "" > conf/routes

Now you should have a nice, clean, blank template on which to build our API.

"Learn how to build a Scala Play Framework API and secure it with access tokens"

Tweet

Tweet This

Building the API

We're going to start by getting a basic controller up and running that will host our API endpoints. Then, we'll create some models specific to the domain we're working with, before going back to the controller and creating our blog and comments endpoints. That way we can test the working of the controller incrementally as we go.

Starting our API controller

To start, create a new file in the app/controllers folder called ApiController.scala:

touch app/controllers/ApiController.scala

Next, populate ApiController.scala with the following:

// app/controllers/ApiController.scala

// Make sure it's in the 'controllers' package
package controllers

import javax.inject.{Inject, Singleton}
import play.api.mvc.{AbstractController, ControllerComponents}

@Singleton
class ApiController @Inject()(cc: ControllerComponents)
  extends AbstractController(cc) {

  // Create a simple 'ping' endpoint for now, so that we
  // can get up and running with a basic implementation
  def ping = Action { implicit request =>
    Ok("Hello, Scala!")
  }

}

Next, open the conf/routes file and enter in a route that enables requests to be made to our ping endpoint:

GET /api/ping controllers.ApiController.ping

With this basic implementation in place, we should be able to run the project and make a request to our simple ping endpoint. Switch back to your terminal and start the project using the SBT CLI.

sbt run 9000

Note: I've chosen to start mine on port 9000, but which port number you choose does not matter. Also, as it's the first time we're running this command, it might take a few minutes for all the dependencies to be downloaded. However, running the project after this initial step should be much faster.

To give it a test run, open another terminal window and use the curl command to issue a request to our ping endpoint:

curl localhost:9000/api/ping

If everything is working correctly, you should see the words "Hello, Scala!" appear in the terminal!

If you like, you can leave the application running, as SBT will pick up file changes and recompile automatically.

Adding models and data

Let's add some types and a data repository more relevant to our application — an API that serves blog posts and comments.

We'll add:

  • a Post model, representing a single blog post
  • a Comment model, representing a single comment on a blog post
  • a DataRepository type, which statically provides blogs and comments for the purposes of testing the API

Start by creating a new folder inside app called models, and create a new file in there called Post.scala:

# You can use this in your terminal to create the folder and add the file
mkdir app/models && touch app/models/Post.scala

Then, open Post.scala and populate it with the following:

// app/models/Post.scala

// Make sure it goes in the models package
package models

import play.api.libs.json.Json

// Create our Post type as a standard case class
case class Post(id: Int, content: String)

object Post {
  // We're going to be serving this type as JSON, so specify a
  // default Json formatter for our Post type here
  implicit val format = Json.format[Post]
}

Here we have a case class to represent our Post data. For simplicity's sake, it only has two properties: id, and content. You'll see that we've specified a JSON formatter. This is used by the Play Framework when it comes to sending our Post object back to the client as JSON. It is also able to go the other way and rehydrate our Post model from JSON. This is convenient, as rather than manually specify what the exact shape of the JSON is, we can use this built-in formatter which generates the JSON based on the properties and types that are present on the model. Play Framework has good documentation on working with JSON if you would like to read further into how JSON formatting works.

Now that we've got our Post model implemented, we can implement our Comment model in a very similar way. Create a new file inside app/models called Comment.scala:

touch app/models/Comment.scala

Next, open Comment.scala and populate it with the following:

// app/models/Comment.scala

package models

import play.api.libs.json.Json

// Represents a comment on a blog post
case class Comment(id: Int, postId: Int, text: String, authorName: String)

object Comment {
  // Use a default JSON formatter for the Comment type
  implicit val format = Json.format[Comment]
}

To complete our data story, create a new folder app/repositories and a new file within called DataRepository.scala:

mkdir app/repositories && touch app/repositories/DataRepository.scala

Open DataRepository.scala and add in the following:

// app/repositories/DataRepository.scala

package repositories

import javax.inject.Singleton
import models.{Comment, Post}

@Singleton
class DataRepository {

  // Specify a couple of posts for our API to serve up
  private val posts = Seq(
    Post(1, "This is a blog post"),
    Post(2, "Another blog post with awesome content")
  )

  // Specify some comments for our API to serve up
  private val comments = Seq(
    Comment(1, 1, "This is an awesome blog post", "Fantastic Mr Fox"),
    Comment(2, 1, "Thanks for the insights", "Jane Doe"),
    Comment(3, 2, "Great, thanks for this post", "Joe Bloggs")
  )

  /*
   * Returns a blog post that matches the specified id, or None if no
   * post was found (collectFirst returns None if the function is undefined for the
   * given post id)
   */
  def getPost(postId: Int): Option[Post] = posts.collectFirst {
    case p if p.id == postId => p
  }

  /*
   * Returns the comments for a blog post
   * If no comments exist for the specified post id, an empty sequence is returned
   * by virtue of the fact that we're using 'collect'
   */
  def getComments(postId: Int): Seq[Comment] = comments.collect {
    case c if c.postId == postId => c
  }
}

This is a simple implementation of an in-memory data store that our API can make use of in order to return some useful data to the client.

Revisiting our API controller

Let's go back to our ApiController.scala file and pull in the repository we just created. We can do this by using Play Framework's Dependency Injection feature. All we have to do is specify DataRepository as a constructor argument on our controller, and Play will create an instance of the repository and inject it into the constructor for us.

We'll also take this opportunity to add the methods that allow us to retrieve blog posts and comments.

Open ApiController.scala and modify it to look like the following:

// app/controllers/ApiController.scala

// ... other imports

// NEW - import JSON functionality and our data repository
import play.api.libs.json.Json
import repositories.DataRepository

@Singleton
class ApiController @Inject()(cc: ControllerComponents,
                              dataRepository: DataRepository // NEW
                             )
  extends AbstractController(cc) {

  // Create a simple 'ping' endpoint for now, so that we
  // can get up and running with a basic implementation
  def ping = Action { implicit request =>
    Ok("Hello, Scala!")
  }

  // NEW - Get a single post
  def getPost(postId: Int) = Action { implicit request =>
    dataRepository.getPost(postId) map { post =>
      // If the post was found, return a 200 with the post data as JSON
      Ok(Json.toJson(post))
    } getOrElse NotFound    // otherwise, return Not Found
  }

  // NEW - Get comments for a post
  def getComments(postId: Int) = Action { implicit request =>
    // Simply return 200 OK with the comment data as JSON.
    Ok(Json.toJson(dataRepository.getComments(postId)))
  }
}

Here we have created two new endpoints: one for retrieving a post by ID, and one for retrieving comments for a post given the post ID. To support that, we've brought in two new imports, and also added our DataRepository type to the class constructor.

So that we can test out our new functionality, we need to add in a couple of routes that point to these two new methods. Open conf/routes and add these two new entries in addition to the /api/ping route we had earlier:

GET /api/ping controllers.ApiController.ping

# NEW
GET /api/post/:postId           controllers.ApiController.getPost(postId: Int)
GET /api/post/:postId/comments  controllers.ApiController.getComments(postId: Int)

Finally, let's test the changes to see if we can access our blog data via an HTTP call. If you have stopped your application from running, you can restart it in the terminal by running:

sbt run 9000

From another terminal window, issue requests to our API using curl:

# Request a blog post
curl localhost:9000/api/post/1

# Request comments for a post
curl localhost:9000/api/post/1/comments

You should find that the server will correctly return the post and comment data as JSON!

Adding Authentication

Now that we have a working API, let's set about securing it using access tokens. This will mean that only clients that have a valid access token for this API will be able to call our endpoints and retrieve data. In a real-world scenario, access tokens would be retrieved as part of an OAuth flow originating from some front-end application (or perhaps a CLI in the case of a machine-to-machine application). We don't have a front-end for this example, so we're going to be using Auth0 to issue our access tokens that are then validated and accepted or rejected by our API.

On a technical level, we're going to implement this by creating a custom action that processes requests as they come into our API. A kind of middleware, if you like. Inside this custom action, we'll extract the bearer token, decode it, and then validate it to make sure that it is indeed a valid token and that the caller can access our API. We can then apply this custom action to any endpoints where the caller needs to be authenticated in order for the call to succeed.

Using JSON Web Tokens

For our API, we expect the token to be a JSON Web Token (JWT), sent to us in the Authorization header. The JWT is made up of three parts: the header, the claims, and the signature. The signature is calculated using the header and the claims, so we can be sure that the data inside the token hasn't been tampered with. Furthermore, since the JWT has been signed using Auth0's private key, if we're able to verify the signature using their corresponding public key, then we know that the token has originated from the place we expect.

Given that the JWTs that we receive will be cryptographically signed using a public/private key algorithm, we need to have the public key available so that we can verify the signature. We can get the public key from the JSON Web Key Set (JWKS) endpoint. This is essentially a set of public keys that allow us to verify RS256 signatures on JWTs. The endpoint exists as part of your Auth0 domain and can be accessed through the browser. An example of the data that it returns is as follows (the keys have been shortened for brevity):

{
  "keys": [
    {
      "alg": "RS256",
      "kty": "RSA",
      "use": "sig",
      "x5c": [
        "MIIDCzCCAfOgAwIBAgIJAd7LS...zo6379"
      ],
      "n": "3GlXBGJQlgJh...I6nGE6PzSvb5ZAw",
      "e": "AQAB",
      "kid": "NTgwMENENDFF...2RDJDMkEyNzYyRkY3MA",
      "x5t": "NTgwMENENDFF...2RDJDMkEyNzYyRkY3MA"
    }
  ]
}

Adding dependencies

In our sample application, we're going to use jwks-rsa, an open-source library to download and retrieve the public key for us. We're also going to use jwt-scala, which will take this public key and decode the token for us, as well as perform some basic validation.

Let's add these dependencies now. Open your build.sbt file that is in the root of the project folder, and add the dependencies in underneath the dependencies that have already been registered:

// ... other dependencies

libraryDependencies ++= Seq(
  "com.pauldijou" %% "jwt-play" % "0.19.0",
  "com.pauldijou" %% "jwt-core" % "0.19.0",
  "com.auth0" % "jwks-rsa" % "0.6.1"
)

Processing the token

Next, let's add a class we can use to process and validate a given JWT. Create a new folder inside app called auth and a new file in there called AuthService.scala:

mkdir app/auth && touch app/auth/AuthService.scala

Open AuthService.scala. We'll start by adding a class with a method that will take a token and return the JWT claims:

// app/auth/AuthService.scala

package auth

import com.auth0.jwk.UrlJwkProvider
import javax.inject.Inject
import pdi.jwt.{JwtAlgorithm, JwtBase64, JwtClaim, JwtJson}
import play.api.Configuration
import scala.util.{Failure, Success, Try}

class AuthService @Inject()(config: Configuration) {

  // A regex that defines the JWT pattern and allows us to
  // extract the header, claims and signature
  private val jwtRegex = """(.+?)\.(.+?)\.(.+?)""".r

  // Your Auth0 domain, read from configuration
  private def domain = config.get[String]("auth0.domain")

  // Your Auth0 audience, read from configuration
  private def audience = config.get[String]("auth0.audience")

  // The issuer of the token. For Auth0, this is just your Auth0
  // domain including the URI scheme and a trailing slash.
  private def issuer = s"https://$domain/"

  // Validates a JWT and potentially returns the claims if the token was
  // successfully parsed and validated
  def validateJwt(token: String): Try[JwtClaim] = for {
    jwk <- getJwk(token)           // Get the secret key for this token
    claims <- JwtJson.decode(token, jwk.getPublicKey, Seq(JwtAlgorithm.RS256)) // Decode the token using the secret key
    _ <- validateClaims(claims)     // validate the data stored inside the token
  } yield claims

}

The core of this is the validateJwt method, which takes a string representing the token, and returns Try[JwtClaim], which will succeed if the token is able to be parsed and validated, or fail if there was a problem. JwtClaim is a type from the jwt-core dependency we added earlier. Despite what the type name suggests, this type holds all the claims that were retrieved from the token.

In order to fully process and validate the token, this method relies on a few other private methods in order to do its job. Let's implement getJwk, which will extract the JSON web key from the JWKS data. These functions can go in the same class, underneath validateJwt:

// app/auth/AuthService.scala

// .. leave untouched ..
def validateJwt(token: String): Try[JwtClaim] = for { ... }

// .. add the new methods below 'validateJwt'

// Splits a JWT into it's 3 component parts
private val splitToken = (jwt: String) => jwt match {
  case jwtRegex(header, body, sig) => Success((header, body, sig))
  case _ => Failure(new Exception("Token does not match the correct pattern"))
}

// As the header and claims data are base64-encoded, this function
// decodes those elements
private val decodeElements = (data: Try[(String, String, String)]) => data map {
  case (header, body, sig) =>
    (JwtBase64.decodeString(header), JwtBase64.decodeString(body), sig)
}

// Gets the JWK from the JWKS endpoint using the jwks-rsa library
private val getJwk = (token: String) =>
    (splitToken andThen decodeElements) (token) flatMap {
      case (header, _, _) =>
        val jwtHeader = JwtJson.parseHeader(header)     // extract the header
        val jwkProvider = new UrlJwkProvider(s"https://$domain")

        // Use jwkProvider to load the JWKS data and return the JWK
        jwtHeader.keyId.map { k =>
          Try(jwkProvider.get(k))
        } getOrElse Failure(new Exception("Unable to retrieve kid"))
    }

Here, getJwk composes a couple of utility functions that split the token into its 3 component parts (header, claims, and signature), decodes the header and claims from base64 strings, and then extracts the KID from the header of the token. This KID is then used to look up the correct JSON Web Key (JWK) from the JWKS data. If it is found, the JWK is returned.

Finally, we need to implement validateClaims. In general, when validating a token you want to make sure that the current date is between the token's "not before" date and its expiry date (if the JWT contains such claims), as well checking that the token issuer and audience value are what you expect. Luckily for us, the JwtClaim.isValid function takes care of these validation requirements for us.

Let's put a thin wrapper around this function so that it supports returning Try[JwtClaim]. Again, this function can just go underneath the other methods that we've already put in:

// app/auth/AuthService.scala

// Validates the claims inside the token. 'isValid' checks the issuedAt, expiresAt,
// issuer and audience fields.
private val validateClaims = (claims: JwtClaim) =>
  if (claims.isValid(issuer, audience)) {
  Success(claims)
} else {
  Failure(new Exception("The JWT did not pass validation"))
}

The last thing to do here is to modify our configuration file so that the values for auth0.domain and auth0.audience exist and can be read. Open the conf/application.conf file now and populate it with the following:

auth0 {
  domain = ${?AUTH0_DOMAIN}
  audience = ${?AUTH0_AUDIENCE}
}

With these settings, we can set AUTH0_DOMAIN and AUTH0_AUDIENCE as environment variables that can subsequently be read by Play's configuration system.

Creating our custom action

The next thing to do is create the custom action that will process our requests and validate the bearer tokens on them. Create a new file in the app/auth folder called AuthAction.scala:

touch app/auth/AuthAction.scala

Then populate it with the following code:

package auth

import javax.inject.Inject
import pdi.jwt._
import play.api.http.HeaderNames
import play.api.mvc._

import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}

// A custom request type to hold our JWT claims, we can pass these on to the
// handling action
case class UserRequest[A](jwt: JwtClaim, token: String, request: Request[A]) extends WrappedRequest[A](request)

// Our custom action implementation
class AuthAction @Inject()(bodyParser: BodyParsers.Default, authService: AuthService)(implicit ec: ExecutionContext)
  extends ActionBuilder[UserRequest, AnyContent] {

  override def parser: BodyParser[AnyContent] = bodyParser
  override protected def executionContext: ExecutionContext = ec

  // A regex for parsing the Authorization header value
  private val headerTokenRegex = """Bearer (.+?)""".r

  // Called when a request is invoked. We should validate the bearer token here
  // and allow the request to proceed if it is valid.
  override def invokeBlock[A](request: Request[A], block: UserRequest[A] => Future[Result]): Future[Result] =
    extractBearerToken(request) map { token =>
      authService.validateJwt(token) match {
        case Success(claim) => block(UserRequest(claim, token, request))      // token was valid - proceed!
        case Failure(t) => Future.successful(Results.Unauthorized(t.getMessage))  // token was invalid - return 401
      }
    } getOrElse Future.successful(Results.Unauthorized)     // no token was sent - return 401

  // Helper for extracting the token value
  private def extractBearerToken[A](request: Request[A]): Option[String] =
    request.headers.get(HeaderNames.AUTHORIZATION) collect {
      case headerTokenRegex(token) => token
    }
}

The key part here is the invokeBlock method, which is called by Play as part of fulfilling a request to an endpoint. The original request is passed as an argument along with a function called block that represents the next step in the request. If the request is valid, you would call block in order to signal that the request should continue through the pipeline. Otherwise, a different result can be returned if it's decided that the request should not continue for whatever reason.

Our implementation is fairly simple. We first extract the Authorization header from the request and retrieve the token from the header value. We then use our AuthService.valiateJwt method to validate the token and extract the claims. If we successfully manage to get the claims, then the token has been validated and we should continue with the request. Otherwise, if there was a failure or some other reason why we could not retrieve the claims, then an Unauthorized result is returned. Similarly, if no token was present in the request or the header was missing, then we likewise return Unauthorized.

Securing our endpoints

The last thing we need to do is apply this custom action to our endpoints that we want to secure. Open up our ApiController once again and make the following changes to the controller class:

// app/controllers/ApiController.scala

import auth.AuthAction    // NEW - import our custom AuthAction
// .. other imports

@Singleton
class ApiController @Inject()(
  cc: ControllerComponents,
  dataRepository: DataRepository,
  authAction: AuthAction  // NEW - add the action as a constructor argument
  )
  extends AbstractController(cc) {

  def ping = Action { implicit request =>
    // .. unchanged code
  }

  // Get a single post
  // NEW - change the action type to 'authAction'
  def getPost(postId: Int) = authAction { implicit request =>
    // .. unchanged code
  }

  // Get comments for a post
  // NEW - change the action type to 'authAction'
  def getComments(postId: Int) = authAction { implicit request =>
    // .. unchanged code
  }
}

To make use of our custom action, we first need to import it. Then we add the action into the constructor of the class so that it is injected by Play. Finally, on each endpoint we want to secure, we change the action type to our authAction instance instead of the default Action. That is:

  // old
  def getPost(postId: Int) = Action { implicit request =>
    // .. unchanged code
  }

  // new 
  def getPost(postId: Int) = authAction { implicit request =>
    // .. unchanged code
  }

Now whenever these endpoints are called, they will filter through our custom action and require that a valid access token is supplied in the request in order for the call to work.

In order for us to test this, we need to create an API application within Auth0 so that it can issue us some valid tokens. Let's do that now.

Creating the Auth0 API

Sign-in to your Auth0 account (or register for a free account if you don't already have one). Once you are on the dashboard, select "APIs" on the left to enter the API dashboard screen.

The Auth0 API dashboard

Next, click the red "Create API" button. Here we're asked to enter a name for our API and an identifier. For these, I've used "Scala API demo" and "https://scala-api.example.com" respectively. Leave the "Signing Algorithm" field as the default of "RS256".

Note: The value for identifier here will be used as your API audience when you come to run the application in the next step.

Adding an API

Then hit the "Create" button. On the page that follows, click on the "Test" link. This is where we can retrieve an access token to test our API's authentication mechanism. To retrieve a valid token, click the 'copy token' link that is underneath the request example.

Auth0 API test panel

Next, start your application running by using the following command, which will set the proper environment variables. If your application is already running, you should stop it and run the following instead:

export AUTH0_DOMAIN=<your Auth0 domain>   # e.g. your-tenant.auth0.com
export AUTH0_AUDIENCE=<your Auth0 API identifier>   # e.g. https://scala-api.example.com
sbt run 9000

Note You should substitute in your own values for AUTH0_DOMAIN and AUTH0_AUDIENCE.

To test the API's authentication mechanism, let's first make an unauthenticated request to the server:

curl -i localhost:9000/api/post/1

Example response:

HTTP/1.1 401 Unauthorized
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'self'
X-Permitted-Cross-Domain-Policies: master-only
Date: Tue, 13 Nov 2018 14:25:06 GMT
Content-Length: 0

We should have correctly received a 401 Unauthorized response from the app! Now, let's make sure the same call works with a valid access token. You'll need to copy and paste your token from Auth0 here where it says <your access token>:

curl -i \
-H 'Authorization: Bearer <your access token>' \
localhost:9000/api/post/1

# Response
HTTP/1.1 200 OK
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'self'
X-Permitted-Cross-Domain-Policies: master-only
Date: Tue, 13 Nov 2018 14:29:18 GMT
Content-Type: application/json
Content-Length: 40

{"id":1,"content":"This is a blog post"}%

This time we should have received a 200 OK response with the JSON data that we expect!

"I successfully built a Scala API and secured it using JWTs!"

Tweet

Tweet This

Summary

In this tutorial, we've learned how to create a basic API using Scala and the Play Framework. We also saw the steps needed to parse and validate JSON Web Tokens as requests are made into our app. We've then gone on to set up an API within Auth0 and used it to issue access tokens that we can then validate using our API.

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon