authenticate

Authenticates a request by checking credentials supplied in the request and extracts a value representing the authenticated principal.

Signature

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

The signature shown is simplified, the real signature uses magnets. [1]

[1]See The Magnet Pattern for an explanation of magnet-based overloading.

Description

On the lowest level, authenticate, takes either a Future[Authentication[T]] which authenticates based on values from the lexical scope or a value of type ContextAuthenticator[T] = RequestContext Future[Authentication[T]] which extracts authentication data from the RequestContext. The returned value of type Authentication[T] must either be the authenticated principal which will be supplied to the inner route or a rejection to reject the request with if authentication failed .

Both variants return futures so that the actual authentication procedure runs detached from route processing and processing of the inner route will be continued once the authentication finished. This allows longer-running authentication tasks (like looking up credentials in a database) to run without blocking the HttpService actor, freeing it for other requests. The authenticate directive itself isn’t tied to any HTTP-specific details so that various authentication schemes can be implemented on top of authenticate.

Standard HTTP-based authentication which uses the WWW-Authenticate header containing challenge data and Authorization header for receiving credentials is implemented in subclasses of HttpAuthenticator.

HTTP Basic Authentication

spray supports HTTP basic authentication through the BasicHttpAuthenticator and provides a series of convenience constructors for different scenarios with BasicAuth(). Make sure to use basic authentication only over SSL because credentials are transferred in plaintext.

Implementing a UserPassAuthenticator

The most generic way of deploying HTTP basic authentication uses a UserPassAuthenticator to validate a user/password combination. It is defined like this:

type UserPassAuthenticator[T] = Option[UserPass]  Future[Option[T]]

Its job is to map a user/password combination (if existent in the request) to an authenticated custom principal of type T (if authenticated).

def myUserPassAuthenticator(userPass: Option[UserPass]): Future[Option[String]] =
  Future {
    if (userPass.exists(up => up.user == "John" && up.pass == "p4ssw0rd")) Some("John")
    else None
  }

val route =
  sealRoute {
    path("secured") {
      authenticate(BasicAuth(myUserPassAuthenticator _, realm = "secure site")) { userName =>
        complete(s"The user is '$userName'")
      }
    }
  }

Get("/secured") ~> route ~> check {
  status === StatusCodes.Unauthorized
  responseAs[String] === "The resource requires authentication, which was not supplied with the request"
  header[HttpHeaders.`WWW-Authenticate`].get.challenges.head === HttpChallenge("Basic", "secure site")
}

val validCredentials = BasicHttpCredentials("John", "p4ssw0rd")
Get("/secured") ~>
  addCredentials(validCredentials) ~> // adds Authorization header
  route ~> check {
  responseAs[String] === "The user is 'John'"
}

val invalidCredentials = BasicHttpCredentials("Peter", "pan")
Get("/secured") ~>
  addCredentials(invalidCredentials) ~>  // adds Authorization header
  route ~> check {
    status === StatusCodes.Unauthorized
    responseAs[String] === "The supplied authentication is invalid"
    header[HttpHeaders.`WWW-Authenticate`].get.challenges.head === HttpChallenge("Basic", "secure site")
  }

From configuration

There are several overloads to configure users from the configuration file. Obviously, this is neither a secure (plaintext passwords) nor a scalable approach. If you don’t pass in a custom config users are configured from the Configuration path spray.routing.users.

def extractUser(userPass: UserPass): String = userPass.user
val config = ConfigFactory.parseString("John = p4ssw0rd")

val route =
  sealRoute {
    path("secured") {
      authenticate(BasicAuth(realm = "secure site", config = config, createUser = extractUser _)) { userName =>
        complete(s"The user is '$userName'")
      }
    }
  }

Get("/secured") ~> route ~> check {
  status === StatusCodes.Unauthorized
  responseAs[String] === "The resource requires authentication, which was not supplied with the request"
  header[HttpHeaders.`WWW-Authenticate`].get.challenges.head === HttpChallenge("Basic", "secure site")
}

val validCredentials = BasicHttpCredentials("John", "p4ssw0rd")
Get("/secured") ~>
  addCredentials(validCredentials) ~>  // adds Authorization header
  route ~> check {
  responseAs[String] === "The user is 'John'"
}

val invalidCredentials = BasicHttpCredentials("Peter", "pan")
Get("/secured") ~>
  addCredentials(invalidCredentials) ~>  // adds Authorization header
  route ~> check {
    status === StatusCodes.Unauthorized
    responseAs[String] === "The supplied authentication is invalid"
    header[HttpHeaders.`WWW-Authenticate`].get.challenges.head === HttpChallenge("Basic", "secure site")
  }

From LDAP

(todo)