diff --git a/akka-http-tests/src/test/scala/akka/http/server/TestServer.scala b/akka-http-tests/src/test/scala/akka/http/server/TestServer.scala
index 3538dd3200..e966ef07f6 100644
--- a/akka-http-tests/src/test/scala/akka/http/server/TestServer.scala
+++ b/akka-http-tests/src/test/scala/akka/http/server/TestServer.scala
@@ -4,6 +4,8 @@
package akka.http.server
+import akka.http.marshalling.Marshaller
+import akka.http.server.directives.AuthenticationDirectives._
import com.typesafe.config.{ ConfigFactory, Config }
import scala.concurrent.duration._
import akka.actor.ActorSystem
@@ -28,11 +30,24 @@ object TestServer extends App {
import ScalaRoutingDSL._
+ def auth =
+ HttpBasicAuthenticator.provideUserName {
+ case p @ UserCredentials.Provided(name) ⇒ p.verifySecret(name + "-password")
+ case _ ⇒ false
+ }
+
+ implicit val html = Marshaller.nodeSeqMarshaller(MediaTypes.`text/html`)
+
handleConnections(bindingFuture) withRoute {
get {
path("") {
complete(index)
} ~
+ path("secure") {
+ HttpBasicAuthentication("My very secure site")(auth) { user ⇒
+ complete(
Hello { user }. Access has been granted!)
+ }
+ } ~
path("ping") {
complete("PONG!")
} ~
@@ -47,16 +62,16 @@ object TestServer extends App {
Console.readLine()
system.shutdown()
- lazy val index = HttpResponse(
- entity = HttpEntity(MediaTypes.`text/html`,
- """|
- |
- | Say hello to akka-http-core!
- | Defined resources:
- |
- |
- |""".stripMargin))
+ lazy val index =
+
+
+ Say hello to akka-http-core!
+ Defined resources:
+
+
+
}
diff --git a/akka-http-tests/src/test/scala/akka/http/server/directives/AuthenticationDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/server/directives/AuthenticationDirectivesSpec.scala
new file mode 100644
index 0000000000..b29cb3c6b2
--- /dev/null
+++ b/akka-http-tests/src/test/scala/akka/http/server/directives/AuthenticationDirectivesSpec.scala
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2009-2014 Typesafe Inc.
+ */
+
+package akka.http.server
+package directives
+
+import akka.http.model._
+import akka.http.model.headers._
+import akka.http.server.AuthenticationFailedRejection.{ CredentialsRejected, CredentialsMissing }
+import akka.http.server.directives.AuthenticationDirectives._
+
+import scala.concurrent.Future
+
+class AuthenticationDirectivesSpec extends RoutingSpec {
+ val dontAuth = HttpBasicAuthentication("MyRealm")(HttpBasicAuthenticator[String](_ ⇒ Future.successful(None)))
+ val doAuth = HttpBasicAuthentication("MyRealm")(HttpBasicAuthenticator.provideUserName(_ ⇒ true))
+ val authWithAnonymous = doAuth.withAnonymousUser("We are Legion")
+
+ val challenge = HttpChallenge("Basic", "MyRealm")
+
+ "the 'HttpBasicAuthentication' directive" should {
+ "reject requests without Authorization header with an AuthenticationFailedRejection" in {
+ Get() ~> {
+ dontAuth { echoComplete }
+ } ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsMissing, challenge) }
+ }
+ "reject unauthenticated requests with Authorization header with an AuthenticationFailedRejection" in {
+ Get() ~> Authorization(BasicHttpCredentials("Bob", "")) ~> {
+ dontAuth { echoComplete }
+ } ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsRejected, challenge) }
+ }
+ "reject requests with illegal Authorization header with 401" in {
+ Get() ~> RawHeader("Authorization", "bob alice") ~> sealRoute {
+ dontAuth { echoComplete }
+ } ~> check {
+ status shouldEqual StatusCodes.Unauthorized
+ responseAs[String] shouldEqual "The resource requires authentication, which was not supplied with the request"
+ header[`WWW-Authenticate`] shouldEqual Some(`WWW-Authenticate`(challenge))
+ }
+ }
+ "extract the object representing the user identity created by successful authentication" in {
+ Get() ~> Authorization(BasicHttpCredentials("Alice", "")) ~> {
+ doAuth { echoComplete }
+ } ~> check { responseAs[String] shouldEqual "Alice" }
+ }
+ "extract the object representing the user identity created for the anonymous user" in {
+ Get() ~> {
+ authWithAnonymous { echoComplete }
+ } ~> check { responseAs[String] shouldEqual "We are Legion" }
+ }
+ "properly handle exceptions thrown in its inner route" in {
+ object TestException extends RuntimeException
+ Get() ~> Authorization(BasicHttpCredentials("Alice", "")) ~> {
+ sealRoute {
+ doAuth { _ ⇒ throw TestException }
+ }
+ } ~> check { status shouldEqual StatusCodes.InternalServerError }
+ }
+ }
+ "AuthenticationDirectives facilities" should {
+ "properly stack several authentication directives" in {
+ val otherChallenge = HttpChallenge("MyAuth", "MyRealm2")
+ val otherAuth: Directive1[String] = AuthenticationDirectives.authenticateOrRejectWithChallenge { (cred: Option[HttpCredentials]) ⇒
+ Future.successful(Left(otherChallenge))
+ }
+ val bothAuth = dontAuth | otherAuth
+
+ Get() ~> sealRoute {
+ bothAuth { echoComplete }
+ } ~> check {
+ status shouldEqual StatusCodes.Unauthorized
+ headers.collect {
+ case `WWW-Authenticate`(challenge +: Nil) ⇒ challenge
+ } shouldEqual Seq(challenge, otherChallenge)
+ }
+ }
+ }
+}
diff --git a/akka-http/src/main/scala/akka/http/server/Directive.scala b/akka-http/src/main/scala/akka/http/server/Directive.scala
index c867b82be4..cdc0ec738b 100644
--- a/akka-http/src/main/scala/akka/http/server/Directive.scala
+++ b/akka-http/src/main/scala/akka/http/server/Directive.scala
@@ -13,7 +13,7 @@ import FastFuture._
/**
* A directive that provides a tuple of values of type `L` to create an inner route.
*/
-sealed abstract class Directive[L] private (implicit val ev: Tuple[L]) {
+abstract class Directive[L](implicit val ev: Tuple[L]) {
/**
* Calls the inner route with a tuple of extracted values of type `L`.
diff --git a/akka-http/src/main/scala/akka/http/server/Directives.scala b/akka-http/src/main/scala/akka/http/server/Directives.scala
index 2edd93f9e7..58eadaa825 100644
--- a/akka-http/src/main/scala/akka/http/server/Directives.scala
+++ b/akka-http/src/main/scala/akka/http/server/Directives.scala
@@ -9,6 +9,7 @@ import directives._
// FIXME: the comments are kept as a reminder which directives are not yet imported
trait Directives extends RouteConcatenation
+ with AuthenticationDirectives
with BasicDirectives
with CacheConditionDirectives
//with ChunkingDirectives
@@ -30,6 +31,6 @@ trait Directives extends RouteConcatenation
with RespondWithDirectives
with RouteDirectives
with SchemeDirectives
-//with SecurityDirectives
+ with SecurityDirectives
object Directives extends Directives
diff --git a/akka-http/src/main/scala/akka/http/server/Rejection.scala b/akka-http/src/main/scala/akka/http/server/Rejection.scala
index 6b03411349..6ce710c05e 100644
--- a/akka-http/src/main/scala/akka/http/server/Rejection.scala
+++ b/akka-http/src/main/scala/akka/http/server/Rejection.scala
@@ -5,7 +5,7 @@
package akka.http.server
import akka.http.model._
-import akka.http.model.headers.{ ByteRange, HttpEncoding }
+import akka.http.model.headers.{ HttpChallenge, ByteRange, HttpEncoding }
import scala.collection.immutable
@@ -125,7 +125,7 @@ case class UnacceptedResponseEncodingRejection(supported: HttpEncoding) extends
* specified in the cause.
*/
case class AuthenticationFailedRejection(cause: AuthenticationFailedRejection.Cause,
- challengeHeaders: List[HttpHeader]) extends Rejection
+ challenge: HttpChallenge) extends Rejection
object AuthenticationFailedRejection {
/**
diff --git a/akka-http/src/main/scala/akka/http/server/RejectionHandler.scala b/akka-http/src/main/scala/akka/http/server/RejectionHandler.scala
index 130438b5df..2ad4615194 100644
--- a/akka-http/src/main/scala/akka/http/server/RejectionHandler.scala
+++ b/akka-http/src/main/scala/akka/http/server/RejectionHandler.scala
@@ -31,14 +31,23 @@ object RejectionHandler {
def default(implicit ec: ExecutionContext) = apply(default = true) {
case Nil ⇒ complete(NotFound, "The requested resource could not be found.")
- case AuthenticationFailedRejection(cause, challengeHeaders) +: _ ⇒
+ case rejections @ (AuthenticationFailedRejection(cause, _) +: _) ⇒
val rejectionMessage = cause match {
case CredentialsMissing ⇒ "The resource requires authentication, which was not supplied with the request"
case CredentialsRejected ⇒ "The supplied authentication is invalid"
}
- ctx ⇒ ctx.complete(Unauthorized, challengeHeaders, rejectionMessage)
+ val challenges = rejections.collect { case AuthenticationFailedRejection(_, challenge) ⇒ challenge }
+ // Multiple challenges per WWW-Authenticate header are allowed per spec,
+ // however, it seems many browsers will ignore all challenges but the first.
+ // Therefore, multiple WWW-Authenticate headers are rendered, instead.
+ //
+ // See https://code.google.com/p/chromium/issues/detail?id=103220
+ // and https://bugzilla.mozilla.org/show_bug.cgi?id=669675
+ val authenticateHeaders = challenges.map(`WWW-Authenticate`(_))
- case AuthorizationFailedRejection +: _ ⇒
+ complete(Unauthorized, authenticateHeaders, rejectionMessage)
+
+ case AuthorizationFailedRejection +: _ ⇒
complete(Forbidden, "The supplied authentication is not authorized to access this resource")
case MalformedFormFieldRejection(name, msg, _) +: _ ⇒
diff --git a/akka-http/src/main/scala/akka/http/server/directives/AuthenticationDirectives.scala b/akka-http/src/main/scala/akka/http/server/directives/AuthenticationDirectives.scala
new file mode 100644
index 0000000000..ab6e7c4e4b
--- /dev/null
+++ b/akka-http/src/main/scala/akka/http/server/directives/AuthenticationDirectives.scala
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2009-2014 Typesafe Inc.
+ */
+
+package akka.http.server
+package directives
+
+import scala.reflect.ClassTag
+
+import scala.concurrent.{ ExecutionContext, Future }
+
+import akka.http.util._
+import akka.http.util.FastFuture._
+
+import akka.http.model.headers._
+import akka.http.server.AuthenticationFailedRejection.{ CredentialsRejected, CredentialsMissing }
+
+/**
+ * Provides directives for securing an inner route using the standard Http authentication headers [[`WWW-Authenticate`]]
+ * and [[Authorization]]. Most prominently, HTTP Basic authentication as defined in RFC 2617.
+ */
+trait AuthenticationDirectives {
+ import BasicDirectives._
+ import AuthenticationDirectives._
+
+ /**
+ * The result of an HTTP authentication attempt is either the user object or
+ * an HttpChallenge to present to the browser.
+ */
+ type AuthenticationResult[+T] = Either[HttpChallenge, T]
+
+ /**
+ * Given [[UserCredentials]] the HttpBasicAuthenticator
+ * returns a Future of either the authenticated user object or None of the user
+ * couldn't be authenticated.
+ */
+ type HttpBasicAuthenticator[T] = UserCredentials ⇒ Future[Option[T]]
+
+ object HttpBasicAuthentication {
+ def challengeFor(realm: String) = HttpChallenge(scheme = "Basic", realm = realm, params = Map.empty)
+
+ /**
+ * A directive that wraps the inner route with Http Basic authentication support. The given authenticator
+ * is used to determine if the credentials in the request are valid and which user object to supply
+ * to the inner route.
+ */
+ def apply[T](realm: String)(authenticator: HttpBasicAuthenticator[T]): AuthenticationDirective[T] =
+ extractExecutionContext.flatMap { implicit ctx ⇒
+ authenticateOrRejectWithChallenge[BasicHttpCredentials, T] { basic ⇒
+ authenticator(authDataFor(basic)).fast.map {
+ case Some(t) ⇒ AuthenticationResult.success(t)
+ case None ⇒ AuthenticationResult.failWithChallenge(challengeFor(realm))
+ }
+ }
+ }
+
+ private def authDataFor(cred: Option[BasicHttpCredentials]): UserCredentials =
+ cred match {
+ case Some(BasicHttpCredentials(username, receivedSecret)) ⇒
+ new UserCredentials.Provided(username) {
+ def verifySecret(secret: String): Boolean = secret secure_== receivedSecret
+ }
+ case None ⇒ UserCredentials.Missing
+ }
+ }
+}
+
+object AuthenticationDirectives extends AuthenticationDirectives {
+ import BasicDirectives._
+ import RouteDirectives._
+ import FutureDirectives._
+ import HeaderDirectives._
+
+ /**
+ * Represents authentication credentials supplied with a request. Credentials can either be
+ * [[UserCredentials.Missing]] or can be [[UserCredentials.Provided]] in which case a username is
+ * supplied and a function to check the known secret against the provided one in a secure fashion.
+ */
+ sealed trait UserCredentials
+ object UserCredentials {
+ case object Missing extends UserCredentials
+ abstract case class Provided(username: String) extends UserCredentials {
+ def verifySecret(secret: String): Boolean
+ }
+ }
+
+ object AuthenticationResult {
+ def success[T](user: T): AuthenticationResult[T] = Right(user)
+ def failWithChallenge(challenge: HttpChallenge): AuthenticationResult[Nothing] = Left(challenge)
+ }
+
+ object HttpBasicAuthenticator {
+ implicit def apply[T](f: UserCredentials ⇒ Future[Option[T]]): HttpBasicAuthenticator[T] =
+ new HttpBasicAuthenticator[T] {
+ def apply(credentials: UserCredentials): Future[Option[T]] = f(credentials)
+ }
+ def fromPF[T](pf: PartialFunction[UserCredentials, Future[T]])(implicit ec: ExecutionContext): HttpBasicAuthenticator[T] =
+ new HttpBasicAuthenticator[T] {
+ def apply(credentials: UserCredentials): Future[Option[T]] =
+ if (pf.isDefinedAt(credentials)) pf(credentials).fast.map(Some(_))
+ else FastFuture.successful(None)
+ }
+
+ def checkAndProvide[T](check: UserCredentials.Provided ⇒ Boolean)(provide: String ⇒ T)(implicit ec: ExecutionContext): HttpBasicAuthenticator[T] =
+ HttpBasicAuthenticator.fromPF {
+ case p @ UserCredentials.Provided(name) if check(p) ⇒ FastFuture.successful(provide(name))
+ }
+ def provideUserName(check: UserCredentials.Provided ⇒ Boolean)(implicit ec: ExecutionContext): HttpBasicAuthenticator[String] =
+ checkAndProvide(check)(identity)
+ }
+
+ /**
+ * Lifts an authenticator function into a directive. The authenticator function gets passed in credentials from the
+ * [[Authorization]] header of the request. If the function returns ``Right(user)`` the user object is provided
+ * to the inner route. If the function returns ``Left(challenge)`` the request is rejected with an
+ * [[AuthenticationFailedRejection]] that contains this challenge to be added to the response.
+ *
+ */
+ def authenticateOrRejectWithChallenge[T](authenticator: Option[HttpCredentials] ⇒ Future[AuthenticationResult[T]]): AuthenticationDirective[T] =
+ extractExecutionContext.flatMap { implicit ctx ⇒
+ extractCredentials.flatMap { cred ⇒
+ onSuccess(authenticator(cred)).flatMap {
+ case Right(user) ⇒ provide(user)
+ case Left(challenge) ⇒
+ val cause = if (cred.isEmpty) CredentialsMissing else CredentialsRejected
+ reject(AuthenticationFailedRejection(cause, challenge)): Directive1[T]
+ }
+ }
+ }
+
+ /**
+ * Lifts an authenticator function into a directive. Same as ``authenticateOrRejectWithChallenge`` above but only applies
+ * the authenticator function with a certain type of credentials.
+ */
+ def authenticateOrRejectWithChallenge[C <: HttpCredentials: ClassTag, T](authenticator: Option[C] ⇒ Future[AuthenticationResult[T]]): AuthenticationDirective[T] =
+ authenticateOrRejectWithChallenge[T] { cred ⇒
+ authenticator {
+ cred.collect {
+ case c: C ⇒ c
+ }
+ }
+ }
+
+ trait AuthenticationDirective[T] extends Directive1[T] {
+ /**
+ * Returns a copy of this authenticationDirective that will provide ``Some(user)`` if credentials
+ * were supplied and otherwise ``None``.
+ */
+ def optional: Directive1[Option[T]] =
+ this.map(Some(_): Option[T]).recover {
+ case AuthenticationFailedRejection(CredentialsMissing, _) +: _ ⇒ provide(None)
+ case rejs ⇒ reject(rejs: _*)
+ }
+
+ /**
+ * Returns a copy of this authenticationDirective that uses the given object as the
+ * anonymous user which will be used if no credentials were supplied in the request.
+ */
+ def withAnonymousUser(anonymous: T): Directive1[T] =
+ optional.map(_.getOrElse(anonymous))
+ }
+ object AuthenticationDirective {
+ implicit def apply[T](other: Directive1[T]): AuthenticationDirective[T] =
+ new AuthenticationDirective[T] { def tapply(inner: Tuple1[T] ⇒ Route) = other.tapply(inner) }
+ }
+
+ def extractCredentials: Directive1[Option[HttpCredentials]] =
+ optionalHeaderValueByType[`Authorization`]().map(_.map(_.credentials))
+}
\ No newline at end of file
diff --git a/akka-http/src/main/scala/akka/http/server/directives/CookieDirectives.scala b/akka-http/src/main/scala/akka/http/server/directives/CookieDirectives.scala
index e5752b98d9..ddfa99ee00 100644
--- a/akka-http/src/main/scala/akka/http/server/directives/CookieDirectives.scala
+++ b/akka-http/src/main/scala/akka/http/server/directives/CookieDirectives.scala
@@ -16,7 +16,7 @@ trait CookieDirectives {
/**
* Extracts an HttpCookie with the given name. If the cookie is not present the
- * request is rejected with a respective [[spray.routing.MissingCookieRejection]].
+ * request is rejected with a respective [[MissingCookieRejection]].
*/
def cookie(name: String): Directive1[HttpCookie] =
headerValue(findCookie(name)) | reject(MissingCookieRejection(name))
diff --git a/akka-http/src/main/scala/akka/http/server/directives/SecurityDirectives.scala b/akka-http/src/main/scala/akka/http/server/directives/SecurityDirectives.scala
new file mode 100644
index 0000000000..c164b1b272
--- /dev/null
+++ b/akka-http/src/main/scala/akka/http/server/directives/SecurityDirectives.scala
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2009-2014 Typesafe Inc.
+ */
+
+package akka.http.server
+package directives
+
+import BasicDirectives._
+import RouteDirectives._
+
+trait SecurityDirectives {
+ /**
+ * Applies the given authorization check to the request.
+ * If the check fails the route is rejected with an [[AuthorizationFailedRejection]].
+ */
+ def authorize(check: ⇒ Boolean): Directive0 = authorize(_ ⇒ check)
+
+ /**
+ * Applies the given authorization check to the request.
+ * If the check fails the route is rejected with an [[AuthorizationFailedRejection]].
+ */
+ def authorize(check: RequestContext ⇒ Boolean): Directive0 =
+ extract(check).flatMap[Unit](if (_) pass else reject(AuthorizationFailedRejection)) &
+ cancelRejection(AuthorizationFailedRejection)
+}