How to Create a Spray Custom Authenticator

Spray has a few standard authenticators already built in. However, what if the authentication we need is not supported? In this article we will describe how to create a custom authenticator in Spray. In particular, we will implement a custom client id/secret token-based authentication.

Spray Authentication: how it works

Spray has already some built in authentication systems. For example, it supports HTTP Basic Authentication: have a look at their documentation here. What if the authentication logic that we need is not supported? Luckily, Spray allows the implementation of custom authenticators.

The authenticate directive has two signatures:

  def authenticate[T](auth: ⇒ Future[Authentication[T]])(implicit executor: ExecutionContext): Directive1[T]
  def authenticate[T](auth: ContextAuthenticator[T])(implicit executor: ExecutionContext): Directive1[T]

where ContextAuthenticator and Authentication are aliases for the following types:

// from spray.routing.authentication package object
...
  type Authentication[T] = Either[Rejection, T]
  type ContextAuthenticator[T] = RequestContext ⇒ Future[Authentication[T]]
...

In other words, a ContextAuthenticator is a function that takes a RequestContext (i.e.: a wrapper that contains all the information about the received request) and it uses it to either authenticate or reject the request.

Cient Id/Secret Custom Authenticator

Our goal is to create a custom authenticator that looks at the query parameters of a request, it extracts some client_id and client_secret and it authorizes the request if they are correct.

For the purposes of this tutorial we have decided to focusing on the second signature of the authenticate directive, so we will define a ContextAuthenticator to use with the directive.

First, let’s create a data container for our credentials and an auxiliary method to extract the id/secret from the request context:

  case class Credentials(id: String, secret: String)

  private def extractCredentials(ctx: RequestContext): Option[Credentials] = {
    val queryParams = ctx.request.uri.query.toMap
    for {
      id <- queryParams.get("client_id")
      secret <- queryParams.get("client_secret")
    } yield Credentials(id, secret)
  }

We can now define our ContextAuthenticator as following, keeping in mind that Authentication[T] is just a type alias for Either[Rejection, T]:

 val authenticator: ContextAuthenticator[Unit] = { ctx =>
      Future {
        val maybeCredentials = extractCredentials(ctx)
          maybeCredentials.fold[authentication.Authentication[Unit]](
            Left(AuthenticationFailedRejection(CredentialsMissing, List()))
          )( credentials =>
              credentials match {
                case AppCredentials("my-client-id", "my-client-super-secret") => Right()
                case _ => Left(AuthenticationFailedRejection(CredentialsRejected, List()))
              }
          )
      }
  }

If any of the authentication query parameters are missing, the following response is returned:

Status: 401 Unauthorized
Body: The resource requires authentication, which was not supplied with the request

If the credentials are provided but they are not correct, the client will see the following message:

Status: 401 Unauthorized
Body: The supplied authentication is invalid

Finally, if the credentials are correct, the request will be authorized and satisfied if possible.

Note that in a more realistic case scenario, you would probably return a User entity instead of Unit. Moreover, rather than having the correct credentials hard-coded in the code, they should be either configurable or stored in a proper data storage.

We are now ready to use our custom authenticator!

For example, to make all our endpoints use our custom authenticator we could do something similar to the following:

 def routes: Route = 
    sealRoute {
        authenticate(authenticator) { authenticated =>
            firstResourceRoutes ~
              secondResourceRoutes
          }
      }
    }

Summary

Spray allows the implementation of custom authenticators. This article provides an example of how this feature can be used to implement a client id/secret token based authentication.