Implementing HTTP Basic authentication with Spray
I’ve been working with Spray for almost a year now and have been thinking of blogging about some of the things I’ve found out during that time. After answering this question on StackOverflow I realized I had most of a blog post there, so here’s my attempt at explaining how to make HTTP Basic authentication work with Spray.
When implementing authentication for a REST API, I first came upon the Authentication and Authorization page on the Spray wiki. That page says the documentation is for for Spray 0.9.0, however, I’ve found most of it still applies as of Spray 1.3.1 running on Akka 2.3.4.
The first thing to understand is that there are two parts to the process:
Authentication, which validates the user’s credentials (i.e, login and password, or perhaps an SSL client certificate) and creates some kind of object which carries the user’s information.
Authorization, which uses the data contained within that object to validate whether the user’s action is allowed.
First of all, I have an ApiUser
class that holds the user’s login and password. It could also hold additional info such as the user’s name, email, etc. I use scala-bcrypt to hash the user’s password, and added a withPassword
method to set the user’s password (which is never stored unencrypted) and a passwordMatches
method which verifies if the user’s password matches the given password. hashedPassword
is an Option[String]
so I can set it to None
to disable a user’s login.
import com.github.t3hnar.bcrypt._
import org.mindrot.jbcrypt.BCrypt
case class ApiUser(login: String,
hashedPassword: Option[String] = None) {
def withPassword(password: String) = copy (hashedPassword = Some(password.bcrypt(generateSalt)))
def passwordMatches(password: String): Boolean = hashedPassword.exists(hp => BCrypt.checkpw(password, hp))
}
Once I had a class to hold the user’s information, I created another class to carry the authenticated user’s information and verify whether the user has permission to perform an action. I decided to create a separate case class to separate user information (name, email, etc.) from the details of authorization.
class AuthInfo(val user: ApiUser) {
def hasPermission(permission: String) = {
// Code to verify whether user has the given permission
}
}
I am currently using Strings to identify permissions, although it is not recommended (search for “stringly typed” on your favorite search engine to find out why). This scheme can be as simple as always returning true
(i.e., all authenticated users have access to all URIs) or can be extended to provide a full-fledged RBAC scheme
With this set up, we can go ahead with the authentication step. According to RFC 2617, when using HTTP Basic authentication the client must send an Authorization
header such as the following:
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
The string after Basic
contains the userId and password, separated by a colon and encoded using Base64. If the Authorization
header is not provided, or if the userid/password combination is incorrect, the server should reply with a 401 Unauthorized
status code, with the WWW-Authenticate
header containing a realm. The realm is just a String to identify the server. Spray provides a BasicAuth
class to take care of all that automatically in conjunction with the authenticate
directive.
According to the Spray documentation, the authenticate
directive can receive either a Future[Authentication[T]]
or a ContextAuthenticator[T]
. A ContextAuthenticator
is simply a method with that receives a RequestContext
(which contains all the request information) and returns a Future[Authentication[T]]
. Since we need to authenticate based on request data (in this case, the Authorization
header), we need to use the second option.
Spray provides a BasicAuth
class that extends Authentication[T]
and takes care of decoding the login and password from the Authorization
header, but we need to provide a function to do the authentication itself (i.e., validating the user and password) and return a Future[Option[T]]
. In our case, T
is our AuthInfo
class, so it should return a Future
containing Some(authInfo)
if the login and password are valid, or None
if they aren’t. I found it most flexible to create a trait that can be mixed into any routes or into the Actor that runs the routes.
trait Authenticator {
def basicUserAuthenticator(implicit ec: ExecutionContext): AuthMagnet[AuthInfo] = {
def validateUser(userPass: Option[UserPass]): Option[AuthInfo] = {
for {
p <- userPass
user <- Repository.apiUsers(p.user)
if user.passwordMatches(p.pass)
} yield new AuthInfo(user)
}
def authenticator(userPass: Option[UserPass]): Future[Option[AuthInfo]] = future { validateUser(userPass) }
BasicAuth(authenticator _, realm = "Private API")
}
}
In my application, Repository.apiUsers
is an object with an apply
method that receives the user’s login and returns an Option[ApiUser]
. You can replace it with, for example, a Slick query or a call to an LDAP server, depending on your application’s needs. One of the nice things about Spray is that, since everything is a Future
, those requests will happen asynchronously.
With this, we can mix the Authenticator
trait into the Actor that runs the routes, and use the authenticate
directive in those routes that need to be authenticated. authenticate
will pass the AuthInfo
object generated by the validateUser
method as a parameter into its closure, and you can then use it wherever you need that info in your routes.
class ApiActor extends Actor
with ActorLogging
with Authenticator {
implicit val executionContext = context.dispatcher
override def receive = {
runRoute(
pathEndOrSingleSlash {
// Any user, authenticated or not, can enter here
complete("System OK")
} ~
pathPrefix("api") {
authenticate(basicUserAuthenticator) { authInfo =>
path("private") {
get {
// All authenticated users can enter here
complete(s"Hi, ${authInfo.user.login}")
}
}
}
}
)
}
What if you need more fine-grained authorization? For example, you might want to allow all users to GET a particular URI, but only some users to PUT or POST. That is what the authorize
directive is for. authorize
receives a by-name that returns true
if the user is allowed to perform the action, and false
if she isn’t. You can use the authInfo
object to check that.
authenticate(basicUserAuthenticator) { authInfo =>
path("private") {
get {
// All authenticated users can enter here
complete(s"Hi, ${authInfo.user.login}")
}
post {
authorize(authInfo.hasPermission("post") {
// Only those users that have the "post" permission will be allowed in here
}
}
}
}
If the authorization is unsuccessful, Spray will return the HTTP 403 Forbidden
status code.
I hope this has been useful. At some point I would like to extend this to use multiple authentication schemes (starting with SSL Client Certificate and possibly continuing on to use OAuth), as well as extricate the code from the application I am working on and perhaps create a reusable library, but that will be for the future. For now, enjoy!