Application-specific authentication

This page explains how to extend the Endpoints algebra with vocabulary specific to the authentication mechanism used by an application, and how to extend interpreters to implement this authentication mechanism for the server side and the client side.

We will be using Play framework but the same approach can be used for other HTTP libraries.

We focus on authentication but the same approach can be used for any other application-specific aspect of the communication that needs to be consistently implemented by clients and servers.

Authentication flow

In this example, the authentication information will be encoded in a JSON Web Token (JWT) attached to HTTP requests. The client will first login to the server, to get its JWT, and then will use the JWT issued by the server to access to protected resources. This can be summarized by the following diagram:

authentication-flow

We want to enrich the endpoints4s algebras with new vocabulary describing the login endpoint as well as the protected endpoints.

Login endpoint

Let’s start with the login endpoint. This endpoint takes requests containing credentials and returns responses containing the issued JWT, or an empty “Bad Request” response in case the credentials where invalid.

Algebra

The existing algebras already provides all we need to describe such an endpoint, except for two things:

  • encoding the logged in user information as a JWT in the response,
  • signalling a bad request in case the authentication failed.

A JWT contains information about the logged-in user (for instance, his name), and that information is serialized and is cryptographically signed by the server (that’s why clients can not forge an arbitrary JWT). In our case, the user information we are interested in is only its name:

case class UserInfo(name: String)

The type used to model the authentication token will be different on client-side and server-side. On server-side, we are only interested in the user info and we want to let the algebra interpreter serialize and sign it. However, on client-side we need to also keep the serialized form since clients can not compute it. Since we want to represent the same concept with different concrete types on the server and client sides, we model it in the algebra with an abstract type member AuthenticationToken.

In the end, we need to add the following members to our algebra:

import endpoints4s.algebra

/** Algebra interface for defining authenticated endpoints using JWT.
  */
trait Authentication extends algebra.Endpoints {

  /** Authentication information */
  type AuthenticationToken

  /** A response entity containing the authenticated user info
    *
    * Clients decode the JWT attached to the response.
    * Servers encode the authentication information as a JWT and attach it to their response.
    */
  def authenticationToken: Response[AuthenticationToken]

  /** A response that might signal to the client that his request was invalid using
    * a `BadRequest` status.
    * Clients map `BadRequest` statuses to `None`, and the underlying `response` into `Some`.
    * Conversely, servers build a `BadRequest` response on `None`, or the underlying `response` otherwise.
    */
  final def wheneverValid[A](responseA: Response[A]): Response[Option[A]] =
    responseA
      .orElse(response(BadRequest, emptyResponse))
      .xmap(_.fold[Option[A]](Some(_), _ => None))(_.toLeft(()))

}

We define our algebra in a trait named Authentication, which extends the main algebra, algebra.Endpoints.

Given this new algebra, we can now describe the login endpoint as follows:

import endpoints4s.algebra

trait AuthenticationEndpoints extends algebra.Endpoints with Authentication {

  /** Login endpoint: takes the API key in a query string parameter and returns either `Some(authenticationToken)`
    * if the credentials are valid, or `None` otherwise
    */
  val login = endpoint(
    get(path / "login" /? qs[String]("apiKey")),
    wheneverValid(authenticationToken)
  )

}

The login endpoint is defined in an AuthenticationTrait, which uses (by inheritance) the main algebra, algebra.Endpoints, and the Authentication algebra.

The endpoint takes request using the GET method, the /login URL and a query string parameter apiKey containing the credentials. The returned response is either a “Bad Request”, or a “Ok” with the issued authentication token.

Server interpreter

The server interpreter fixes the AuthenticationToken type member to UserInfo and implements the authenticationToken and wheneverValid methods:

import endpoints4s.play.server
import pdi.jwt.JwtSession
import pdi.jwt.JwtSession.RichResult

trait ServerAuthentication extends Authentication with server.Endpoints {

  import ClockSettings._
  import playComponents.executionContext

  protected implicit def playConfiguration: Configuration

  // On server side, we build the token ourselves so we only care about the user information
  type AuthenticationToken = UserInfo

  // Encodes the user info in the JWT session
  def authenticationToken: Response[UserInfo] =
    userInfo => Results.Ok.withJwtSession(JwtSession().+("user", userInfo))

}

The ServerAuthentication trait extends the Authentication algebra as well as a server Endpoints interpreter based on Play framework.

The authenticationToken operation is straightforwardly implemented by building an Ok response and adding it a JWT session containing a user property with the contents of our UserInfo. The management of the JWT session is delegated to the pauldijou/jwt-scala library, which attaches the issued JWT to the Authorization header of the response.

The wheneverValid operation checks whether the response value is defined or not. In case it is empty, it returns a BadRequest response, otherwise it calls the underlying response.

With this interpreter, the implementation of the login endpoint looks like the following:

import endpoints4s.play.server

class Server(
    val playComponents: PlayComponents,
    val playConfiguration: Configuration
) extends AuthenticationEndpoints
    with server.Endpoints
    with ServerAuthentication {

    login.implementedBy { apiKey =>
      if (apiKey == "foobar") Some(UserInfo("Alice"))
      else None
    }

}

Our Server class extends the traits that defines the login endpoint, namely the AuthenticationEndpoints, and mixes the Play-based server interpreter as well as our ServerAuthentication interpreter.

In this simplified example, we only have one valid API key, "foobar", belonging to Alice. The login endpoint is implemented by a function that checks whether the supplied apiKey is equal to "foobar", in which case we return a UserInfo object wrapped in a Some. Otherwise we return None to signal that the API key is invalid.

Mid-way summary

What have we learnt so far?

We are only halfway trough this document but the first sections already showed the key aspects of enriching endpoints4s for application-specific needs:

  1. We have enriched the existing algebras with another algebra, by defining a trait extending the existing algebras;
  2. We have introduced new concepts as abstract type members (in our case, AuthenticationToken);
  3. We have introduced new operations defining how to build or combine concepts together;
  4. We have used our algebra to define descriptions of endpoints, by defining a trait extending the algebra;
  5. We have implemented an interpreter for our algebra, by defining a trait extending the algebra, mixing an existing base interpreter and implementing the remaining abstract members;
  6. We have applied our interpreter to our descriptions of endpoints, by defining a class (or an object) extending the endpoint descriptions and mixing the interpreter trait.

These relationships are illustrated by the following diagram:

interactions

The traits provided by endpoints4s are shown in gray.

Client interpreter

The implementation of the client interpreter repeats the same recipe: we define a trait ClientAuthentication, which extends Authentication and mixes a client.Endpoints base interpreter:

import endpoints4s.play.client

/** Interpreter for the [[Authentication]] algebra interface that produces
  * a Play client (using `play.api.libs.ws.WSClient`).
  */
trait ClientAuthentication extends client.Endpoints with Authentication {

  implicit protected def playConfiguration: Configuration

  // The constructor is private so that users can not
  // forge instances themselves
  class AuthenticationToken private[ClientAuthentication] (
      private[ClientAuthentication] val token: String,
      val decoded: UserInfo
  )

  // Decodes the user info from an OK response
  def authenticationToken: Response[AuthenticationToken] = { (status, headers) =>
    if (status == OK) {
      headers.get(HeaderNames.AUTHORIZATION) match {
        case Some(Seq(headerValue)) =>
          val token = headerValue.stripPrefix("Bearer ")
          // Note: the default implementation of `JwtSession.deserialize`
          // returns an “empty” JwtSession object when it is invalid.
          // You might want to tweak the logic to return an error in such a case.
          UserInfo.decodeToken(token) match {
            case Some(user) =>
              Some(_ => Right(new AuthenticationToken(token, user)))
            case None => Some(_ => Left(new Exception("Invalid JWT session")))
          }
        case _ => Some(_ => Left(new Exception("Missing JWT session")))
      }
    } else None
  }

}

The AuthenticationToken type is implemented as a class whose constructor is private. If it was public, clients could build a fake authentication token which would then fail at runtime because the server would reject it when seeing that it is not correctly signed. By making the constructor private, we simply make it impossible to reach such a runtime error.

The AuthenticationToken class contains the serialized token as well as the decoded UserInfo.

The authenticationToken operation is implemented as the dual of the server interpreter: it checks that there is an Authorization response header, and that it contains a valid UserInfo object. In case of failure, this method returns an exception that will be eventually thrown by the base client interpreter. One could argue that we should model the fact that decoding the response can fail by returning an Option instead of throwing an exception. However, the philosophy of endpoints4s is that client and server interpreters implement a same HTTP protocol, therefore we expect (and assume) the interpreters to be consistent together. Thus, we assume that don’t need to surface that kind of failures (hence the use of exceptions).

This contrasts with the wheneverValid operation, which models the fact that the API key supplied by the user can be invalid. In such a case, we really want the failure to surface to the end-user, hence the usage of Option. The implementation checks whether the status is 400, in which case it returns None, otherwise it returns the underlying response wrapped in a Some.

Putting things together

If we create an instance of our Client an run our Server, we can test that the following scenarios work as expected:

"wrong login using client" in {
  for {
    loginResult <- client.login("unknown")
  } yield assert(loginResult.isEmpty)
}
"valid login using client" in {
  for {
    loginResult <- client.login("foobar")
  } yield assert(loginResult.nonEmpty)
}

These tests check that if we login with an unknown API key we get no authentication token, but if we login with the "foobar" API key then we get some authentication token.

Protected endpoints

Now that we are able to issue an authentication token, let’s see how we can define endpoints that require such an authentication token to be present (and valid) in incoming requests.

Such protected endpoints take requests containing the serialized token in their Authorization HTTP header, and return a 401 (Unauthorized) response in case the token is not found or is invalid.

Algebra

To define protected endpoints, we need to enrich the Authentication algebra with additional vocabulary. First, we need a way to define that requests that must contain the authentication token. Second, we need a way to define that responses might be Unauthorized. Last, we need a convenient Endpoint constructor that puts all the pieces together.

/** A request with the given `method`, `url` and `entity`, and which is rejected by the server if it
  * doesn’t contain a valid JWT.
  */
private[authentication] def authenticatedRequest[U, E, UE, UET](
    method: Method,
    url: Url[U],
    entity: RequestEntity[E]
)(implicit
    tuplerUE: Tupler.Aux[U, E, UE],
    tuplerUET: Tupler.Aux[UE, AuthenticationToken, UET]
): Request[UET]

/** A response that might signal to the client that his request was not authenticated.
  * Clients throw an exception if the response status is `Unauthorized`.
  * Servers build an `Unauthorized` response in case the incoming request was not correctly authenticated.
  */
private[authentication] def wheneverAuthenticated[A](
    response: Response[A]
): Response[A]

/** User-facing constructor for endpoints requiring authentication.
  *
  * @return An endpoint requiring a authentication information to be provided
  *         in the `Authorization` request header. It returns `response`
  *         if the request is correctly authenticated, otherwise it returns
  *         an empty `Unauthorized` response.
  *
  * @param method        HTTP method
  * @param url           Request URL
  * @param response      HTTP response
  * @param requestEntity HTTP request entity
  * @tparam U Information carried by the URL
  * @tparam E Information carried by the request entity
  * @tparam R Information carried by the response
  */
final def authenticatedEndpoint[U, E, R, UE, UET](
    method: Method,
    url: Url[U],
    requestEntity: RequestEntity[E],
    response: Response[R]
)(implicit
    tuplerUE: Tupler.Aux[U, E, UE],
    tuplerUET: Tupler.Aux[UE, AuthenticationToken, UET]
): Endpoint[UET, R] =
  endpoint(
    authenticatedRequest(method, url, requestEntity),
    wheneverAuthenticated(response)
  )

The authenticatedRequest method defines a request expecting an authentication token to be provided in the Authorization header. The wheneverAuthenticated method transforms a given Response[A] into another Response[A] that can be an Unauthorized HTTP response in case the client was not authenticated. Note that, in contrast with the previously defined wheneverValid method, we return a Response[A] rather than a Response[Option[A]]. This is because we assume that requests will be built by using the same algebra, which will make them correctly authenticated by construction.

The last operation we have introduced is authenticatedEndpoint, which takes a request and a response and wraps the request constituents into the authenticatedRequest constructor, and wraps the response into the wheneverAuthenticated combinator.

This authenticatedEndpoint operation is final, and it is the only user-facing operation for defining protected endpoints (the two other operations are private). It guarantees that the request will always have the authentication token in its headers, and that the response can always be Unauthorized.

Note

The authenticatedRequest operation takes several type parameters. In particular, they model the type of the request URL (U) and entity (E). These types must be tracked by the type system so that, eventually, an Endpoint[Req, Resp] is built, where the Req type is a tuple of all the information (URL and entity) carried by the request. In this example we enrich the request headers with the authentication token. However, instead of simply returning nested tuples (e.g. ((U, E), AuthenticationToken)), we rely on implicit Tupler instances to compute the type of the tuple. Tupler instances are defined in a way that always flattens nested tuples (e.g. they will return (U, E, AuthenticationToken)) and removes Unit types (e.g. if the URL is static—of type Url[Unit]—the tuplers return (E, AuthenticationToken)).

The authenticatedEndpoint operation can be used as follows:

/** Some resource requiring the request to provide a valid JWT token. Returns a message
  * “Hello ''user_name''” if the request is correctly authenticated, otherwise returns
  * an `Unauthorized` HTTP response.
  */
val someResource: Endpoint[AuthenticationToken, String] =
  authenticatedEndpoint(
    Get,
    path / "some-resource",
    emptyRequest,
    ok(textResponse)
  )

Since the request URL is static and the request has no entity, the information carried by the request is just the AuthenticationToken.

Server interpreter

Our Play-based server is implemented as follows:

def authenticatedRequest[U, E, UE, UET](
    method: Method,
    url: Url[U],
    entity: RequestEntity[E]
)(implicit
    tuplerUE: Tupler.Aux[U, E, UE],
    tuplerUET: Tupler.Aux[UE, AuthenticationToken, UET]
): Request[UET] = {
  // Extracts and validates user info from a request header
  val authenticationTokenRequestHeaders: RequestHeaders[Option[AuthenticationToken]] = {
    headers =>
      Valid(
        headers
          .get(HeaderNames.AUTHORIZATION)
          .flatMap(headerValue =>
            UserInfo.decodeToken(headerValue.stripPrefix("Bearer "))
          ) match {
          case Some(token) => Some(token)
          case None        => None
        }
      )
  }

  extractMethodUrlAndHeaders(method, url, authenticationTokenRequestHeaders)
    .toRequest[UET] {
      case (_, None) =>
        _ => Some(BodyParser(_ => Accumulator.done(Left(Results.Unauthorized))))
      case (u, Some(token)) =>
        hdrs => entity(hdrs).map(_.map(e => tuplerUET(tuplerUE(u, e), token)))
    } { uet =>
      val (ue, t) = tuplerUET.unapply(uet)
      val (u, _) = tuplerUE.unapply(ue)
      (u, Some(t))
    }
}

// Does nothing because `authenticatedReqest` already
// takes care of returning `Unauthorized` if the request
// is not properly authenticated
def wheneverAuthenticated[A](response: Response[A]): Response[A] = response

And the protected endpoint can be implemented as follows:

// Note that the `AuthenticationToken` is available to the implementations
// It can be used to check authorizations
someResource.implementedBy(token => s"Hello ${token.name}!")

Client interpreter

And our Play-based client is implemented as follows:

def authenticatedRequest[U, E, UE, UET](
    method: Method,
    url: Url[U],
    entity: RequestEntity[E]
)(implicit
    tuplerUE: Tupler.Aux[U, E, UE],
    tuplerUET: Tupler.Aux[UE, AuthenticationToken, UET]
): Request[UET] = {
  // Encodes the user info as a JWT object in the `Authorization` request header
  val authenticationTokenRequestHeaders: RequestHeaders[AuthenticationToken] = {
    (user, wsRequest) =>
      wsRequest.withHttpHeaders(
        HeaderNames.AUTHORIZATION -> s"Bearer ${user.token}"
      )
  }
  request(method, url, entity, headers = authenticationTokenRequestHeaders)
}

// Checks that the response is not `Unauthorized` before continuing
def wheneverAuthenticated[A](response: Response[A]): Response[A] = { (status, headers) =>
  if (status == Status.UNAUTHORIZED) {
    Some(_ => Left(new Exception("Unauthorized")))
  } else {
    response(status, headers)
  }
}

Putting things together

Our Client and Server instances are now able to have more sophisticated exchanges:

"login and access protected resource" in {
  for {
    maybeToken <- client.login("foobar")
    token = maybeToken.get
    _ = assert(token.decoded == UserInfo("Alice"))
    resource <- client.someResource(token)
  } yield assert(resource == "Hello Alice!")
}

This test first gets an authentication token by calling the login endpoint, and then accesses the protected endpoint by supplying its token.

Conclusion

This page shows how to include an application-specific aspect of the communication protocol at the algebra level, and how to implement interpreters for this extended algebra.

We only demonstrated how to implement client and server interpreters but the same approach can be used with documentation interpreters.