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 http4s 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.

Authentication 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:

sourcecase 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:

sourceimport endpoints4s.algebra
import endpoints4s.Codec

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

  /** Authentication information. It is left abstract because clients and
    * servers may want to use different representations
    */
  type AuthenticationToken

  /** A response containing a JWT in a JSON document. */
  final def authenticationToken: Response[AuthenticationToken] = {
    val authenticationTokenSchema =
      field[String]("jwt_token")
        .xmapWithCodec(authenticationTokenCodec)
    ok(jsonResponse(authenticationTokenSchema))
  }

  /** Logic for decoding the JWT.
    * Servers validate the token signature, clients just decode without validating.
    */
  def authenticationTokenCodec: Codec[String, 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:

sourceimport 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[String, Option[AuthenticationToken]] = 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.

Authentication server interpreter

The server interpreter fixes the AuthenticationToken type member to UserInfo and implements the authenticationTokenCodec method:

sourceimport endpoints4s.http4s.server
import pdi.jwt.JwtCirce

trait ServerAuthentication[F[_]]
    extends server.Endpoints[F]
    with server.JsonEntitiesFromSchemas
    with Authentication {

  def privateKey: PrivateKey
  def publicKey: PublicKey

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

  def decodeToken(token: String): Validated[UserInfo] =
    Validated.fromTry(
      JwtCirce
        .decode(token, publicKey)
        .flatMap { claim =>
          io.circe.parser.parse(claim.content).toTry.flatMap(_.as[UserInfo].toTry)
        }
    )

  // Encodes the user info in the JWT session
  def authenticationTokenCodec: Codec[String, AuthenticationToken] =
    Codec.fromEncoderAndDecoder[String, AuthenticationToken] { authenticationToken =>
      JwtCirce.encode(UserInfo.codec(authenticationToken), privateKey, JwtAlgorithm.RS256)
    }(decodeToken(_))

}

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

The authenticationTokenCodec operation is implemented with the help of the library pauldijou/jwt-scala. It serializes the user info into JSON (via UserInfo.codec), and then creates a signed JWT from it with a private key.

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

sourceimport endpoints4s.http4s.server

class Server
    extends server.Endpoints[IO]
    with AuthenticationEndpoints
    with ServerAuthentication[IO] {

    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 http4s-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 it returns a UserInfo object wrapped in a Some. Otherwise, it returns None to signal that the API key is invalid.

Mid-way summary

What have we learnt so far?

We are only halfway through 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.

Authentication 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:

sourceimport endpoints4s.http4s.client

/** Interpreter for the [[Authentication]] algebra interface that produces
  * an http4s client (using `org.http4s.client.Client`).
  */
trait ClientAuthentication[F[_]]
    extends client.Endpoints[F]
    with client.JsonEntitiesFromSchemas
    with Authentication {

  def publicKey: PublicKey

  // 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 authenticationTokenCodec: Codec[String, AuthenticationToken] =
    Codec.fromEncoderAndDecoder[String, AuthenticationToken](_.token) { token =>
      Validated.fromTry(
        JwtCirce
          .decode(token, publicKey)
          .flatMap(claim =>
            io.circe.parser
              .parse(claim.content)
              .toTry
              .flatMap(
                _.as[UserInfo].toTry.map(userInfo => new AuthenticationToken(token, userInfo))
              )
          )
      )
    }


}

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 make it impossible to reach such a runtime error.

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

The authenticationTokenCodec operation is implemented as the dual of the server interpreter: it tries to decode the JWT, and then tries to parse its content and to decode it as a UserInfo object.

In case of failure, it returns an Invalid value, which will ultimately been reported to the user by throwing an exception. 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.

Putting things together (authentication)

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

source"wrong login using client" in {
  for {
    loginResult <- client.login.sendAndConsume("unknown")
  } yield assert(loginResult.isEmpty)
}
"valid login using client" in {
  for {
    loginResult <- client.login.sendAndConsume("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.

Protected endpoints 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.

source/** 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:

source/** 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.

Protected endpoints server interpreter

Our http4s-based server is implemented as follows:

sourcedef 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[Authorization]
            .flatMap {
              case Authorization(Credentials.Token(AuthScheme.Bearer, token)) =>
                decodeToken(token).toEither.toOption
              case _ => None
            }
        )
      }
  }

  new Request[UET] {

    // Data extracted from the incoming request
    type UrlAndHeaders = (U, AuthenticationToken)

    def matchAndParseHeaders(
        http4sRequest: org.http4s.Request[F]
    ): Option[Either[org.http4s.Response[F], Validated[UrlAndHeaders]]] =
      // First, check whether the incoming request matches this request description
      matchAndParseHeadersAsRight(method, url, emptyRequestHeaders, http4sRequest)
        // If this is the case, check whether there is a token in the request headers or not
        .map { errorResponseOrValidatedUrl =>
          authenticationTokenRequestHeaders(http4sRequest.headers) match {
            // There is a token, just add it to the data parsed from the URL
            case Valid(Some(token)) =>
              errorResponseOrValidatedUrl
                .map { validatedUrl =>
                  validatedUrl.map { case (urlData, _) => (urlData, token) }
                }
            // Otherwise, return an Unauthorized response
            case _ => Left(org.http4s.Response(Unauthorized))
          }
        }

    def parseEntity(
        urlAndHeaders: UrlAndHeaders,
        http4sRequest: org.http4s.Request[F]
    ): Effect[Either[org.http4s.Response[F], UET]] =
      entity(http4sRequest).map(_.map { entityData =>
        val (urlData, token) = urlAndHeaders
        tuplerUET(tuplerUE(urlData, entityData), token)
      })

  }
}

// 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:

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

Protected endpoints client interpreter

And our http4s-based client is implemented as follows:

sourcedef 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, http4sRequest) =>
      http4sRequest.putHeaders(
        Authorization(Credentials.Token(AuthScheme.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 == Unauthorized) {
    Some(_ => effect.raiseError(new Exception("Unauthorized")))
  } else {
    response(status, headers)
  }
}

Putting things together (protected endpoints)

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

source"login and access protected resource" in {
  for {
    maybeToken <- client.login.sendAndConsume("foobar")
    token = maybeToken.get
    _ = assert(token.decoded == UserInfo("Alice"))
    resource <- client.someResource.sendAndConsume(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.