diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/headers/Authorization.java b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/Authorization.java index 57eb43fe32..276deba442 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/headers/Authorization.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/Authorization.java @@ -17,4 +17,7 @@ public abstract class Authorization extends akka.http.scaladsl.model.HttpHeader public static Authorization basic(String username, String password) { return create(HttpCredentials.createBasicHttpCredentials(username, password)); } + public static Authorization oauth2(String token) { + return create(HttpCredentials.createOAuth2BearerToken(token)); + } } diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/values/HttpBasicAuthenticationTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/HttpBasicAuthenticationTest.java index de4368ca28..0369d94d97 100644 --- a/akka-http-tests/src/test/java/akka/http/javadsl/server/values/HttpBasicAuthenticationTest.java +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/HttpBasicAuthenticationTest.java @@ -17,45 +17,72 @@ public class HttpBasicAuthenticationTest extends JUnitRouteTest { HttpBasicAuthenticator authenticatedUser = new HttpBasicAuthenticator("test-realm") { @Override - public Future> authenticate(BasicUserCredentials credentials) { + public Future> authenticate(BasicCredentials credentials) { if (credentials.available() && // no anonymous access - credentials.userName().equals("sina") && - credentials.verifySecret("1234")) + credentials.identifier().equals("sina") && + credentials.verify("1234")) return authenticateAs("Sina"); else return refuseAccess(); } }; + OAuth2Authenticator authenticatedToken = + new OAuth2Authenticator("test-realm") { + @Override + public Future> authenticate(OAuth2Credentials credentials) { + if (credentials.available() && // no anonymous access + credentials.identifier().equals("myToken") && + credentials.verify("myToken")) + return authenticateAs("myToken"); + else return refuseAccess(); + } + }; + Handler1 helloWorldHandler = new Handler1() { @Override - public RouteResult apply(RequestContext ctx, String user) { - return ctx.complete("Hello "+user+"!"); + public RouteResult apply(RequestContext ctx, String identifier) { + return ctx.complete("Identified as "+identifier+"!"); } }; TestRoute route = testRoute( - path("secure").route( + path("basicSecure").route( authenticatedUser.route( handleWith1(authenticatedUser, helloWorldHandler) ) + ), + path("oauthSecure").route( + authenticatedToken.route( + handleWith1(authenticatedToken, helloWorldHandler) + ) ) ); @Test public void testCorrectUser() { HttpRequest authenticatedRequest = - HttpRequest.GET("/secure") + HttpRequest.GET("/basicSecure") .addHeader(Authorization.basic("sina", "1234")); route.run(authenticatedRequest) .assertStatusCode(200) - .assertEntity("Hello Sina!"); + .assertEntity("Identified as Sina!"); + } + @Test + public void testCorrectToken() { + HttpRequest authenticatedRequest = + HttpRequest.GET("/oauthSecure") + .addHeader(Authorization.oauth2("myToken")); + + route.run(authenticatedRequest) + .assertStatusCode(200) + .assertEntity("Identified as myToken!"); } @Test public void testRejectAnonymousAccess() { - route.run(HttpRequest.GET("/secure")) + route.run(HttpRequest.GET("/basicSecure")) .assertStatusCode(401) .assertEntity("The resource requires authentication, which was not supplied with the request") .assertHeaderExists("WWW-Authenticate", "Basic realm=\"test-realm\""); @@ -63,7 +90,7 @@ public class HttpBasicAuthenticationTest extends JUnitRouteTest { @Test public void testRejectUnknownUser() { HttpRequest authenticatedRequest = - HttpRequest.GET("/secure") + HttpRequest.GET("/basicSecure") .addHeader(Authorization.basic("joe", "0000")); route.run(authenticatedRequest) @@ -73,7 +100,7 @@ public class HttpBasicAuthenticationTest extends JUnitRouteTest { @Test public void testRejectWrongPassword() { HttpRequest authenticatedRequest = - HttpRequest.GET("/secure") + HttpRequest.GET("/basicSecure") .addHeader(Authorization.basic("sina", "1235")); route.run(authenticatedRequest) diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/TestServer.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/TestServer.scala index 5f5c1d2bf5..237c8f70c0 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/TestServer.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/TestServer.scala @@ -5,7 +5,7 @@ package akka.http.scaladsl.server import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport -import akka.http.scaladsl.server.directives.UserCredentials +import akka.http.scaladsl.server.directives.Credentials import com.typesafe.config.{ ConfigFactory, Config } import akka.actor.ActorSystem import akka.stream.ActorMaterializer @@ -23,7 +23,7 @@ object TestServer extends App { import Directives._ def auth: AuthenticatorPF[String] = { - case p @ UserCredentials.Provided(name) if p.verifySecret(name + "-password") ⇒ name + case p @ Credentials.Provided(name) if p.verify(name + "-password") ⇒ name } val bindingFuture = Http().bindAndHandle({ diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/SecurityDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/SecurityDirectivesSpec.scala index f19ecd2251..8d3a5f8f4f 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/SecurityDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/SecurityDirectivesSpec.scala @@ -11,26 +11,37 @@ import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.server.AuthenticationFailedRejection.{ CredentialsRejected, CredentialsMissing } class SecurityDirectivesSpec extends RoutingSpec { - val dontAuth = authenticateBasicAsync[String]("MyRealm", _ ⇒ Future.successful(None)) - val doAuth = authenticateBasicPF("MyRealm", { case UserCredentials.Provided(name) ⇒ name }) - val authWithAnonymous = doAuth.withAnonymousUser("We are Legion") + val dontBasicAuth = authenticateBasicAsync[String]("MyRealm", _ ⇒ Future.successful(None)) + val dontOAuth2Auth = authenticateOAuth2Async[String]("MyRealm", _ ⇒ Future.successful(None)) + val doBasicAuth = authenticateBasicPF("MyRealm", { case Credentials.Provided(identifier) ⇒ identifier }) + val doOAuth2Auth = authenticateOAuth2PF("MyRealm", { case Credentials.Provided(identifier) ⇒ identifier }) + val authWithAnonymous = doBasicAuth.withAnonymousUser("We are Legion") val challenge = HttpChallenge("Basic", "MyRealm") "basic authentication" should { "reject requests without Authorization header with an AuthenticationFailedRejection" in { Get() ~> { - dontAuth { echoComplete } + dontBasicAuth { echoComplete } } ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsMissing, challenge) } } "reject unauthenticated requests with Authorization header with an AuthenticationFailedRejection" in { Get() ~> Authorization(BasicHttpCredentials("Bob", "")) ~> { - dontAuth { echoComplete } + dontBasicAuth { echoComplete } } ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsRejected, challenge) } } + "reject requests with an OAuth2 Bearer Token Authorization header with 401" in { + Get() ~> Authorization(OAuth2BearerToken("myToken")) ~> Route.seal { + dontOAuth2Auth { echoComplete } + } ~> check { + status shouldEqual StatusCodes.Unauthorized + responseAs[String] shouldEqual "The supplied authentication is invalid" + header[`WWW-Authenticate`] shouldEqual Some(`WWW-Authenticate`(challenge)) + } + } "reject requests with illegal Authorization header with 401" in { Get() ~> RawHeader("Authorization", "bob alice") ~> Route.seal { - dontAuth { echoComplete } + dontBasicAuth { echoComplete } } ~> check { status shouldEqual StatusCodes.Unauthorized responseAs[String] shouldEqual "The resource requires authentication, which was not supplied with the request" @@ -39,7 +50,7 @@ class SecurityDirectivesSpec extends RoutingSpec { } "extract the object representing the user identity created by successful authentication" in { Get() ~> Authorization(BasicHttpCredentials("Alice", "")) ~> { - doAuth { echoComplete } + doBasicAuth { echoComplete } } ~> check { responseAs[String] shouldEqual "Alice" } } "extract the object representing the user identity created for the anonymous user" in { @@ -51,7 +62,55 @@ class SecurityDirectivesSpec extends RoutingSpec { object TestException extends RuntimeException Get() ~> Authorization(BasicHttpCredentials("Alice", "")) ~> { Route.seal { - doAuth { _ ⇒ throw TestException } + doBasicAuth { _ ⇒ throw TestException } + } + } ~> check { status shouldEqual StatusCodes.InternalServerError } + } + } + "bearer token authentication" should { + "reject requests without Authorization header with an AuthenticationFailedRejection" in { + Get() ~> { + dontOAuth2Auth { echoComplete } + } ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsMissing, challenge) } + } + "reject unauthenticated requests with Authorization header with an AuthenticationFailedRejection" in { + Get() ~> Authorization(OAuth2BearerToken("myToken")) ~> { + dontOAuth2Auth { echoComplete } + } ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsRejected, challenge) } + } + "reject requests with a Basic Authorization header with 401" in { + Get() ~> Authorization(BasicHttpCredentials("Alice", "")) ~> Route.seal { + dontBasicAuth { echoComplete } + } ~> check { + status shouldEqual StatusCodes.Unauthorized + responseAs[String] shouldEqual "The supplied authentication is invalid" + header[`WWW-Authenticate`] shouldEqual Some(`WWW-Authenticate`(challenge)) + } + } + "reject requests with illegal Authorization header with 401" in { + Get() ~> RawHeader("Authorization", "bob alice") ~> Route.seal { + dontOAuth2Auth { 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(OAuth2BearerToken("myToken")) ~> { + doOAuth2Auth { echoComplete } + } ~> check { responseAs[String] shouldEqual "myToken" } + } + "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(OAuth2BearerToken("myToken")) ~> { + Route.seal { + doOAuth2Auth { _ ⇒ throw TestException } } } ~> check { status shouldEqual StatusCodes.InternalServerError } } @@ -62,7 +121,7 @@ class SecurityDirectivesSpec extends RoutingSpec { val otherAuth: Directive1[String] = authenticateOrRejectWithChallenge { (cred: Option[HttpCredentials]) ⇒ Future.successful(Left(otherChallenge)) } - val bothAuth = dontAuth | otherAuth + val bothAuth = dontBasicAuth | otherAuth Get() ~> Route.seal(bothAuth { echoComplete }) ~> check { status shouldEqual StatusCodes.Unauthorized diff --git a/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala b/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala index 2a0794bcac..68fe36a603 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala @@ -5,7 +5,7 @@ package akka.http.impl.server import akka.http.impl.util.JavaMapping -import akka.http.javadsl.server.values.{ PathMatcher, BasicUserCredentials } +import akka.http.javadsl.server.values.{ PathMatcher, BasicCredentials, OAuth2Credentials } import akka.http.scaladsl.model.StatusCodes.Redirection import akka.http.scaladsl.server.util.TupleOps.Join @@ -13,7 +13,7 @@ import scala.language.implicitConversions import scala.annotation.tailrec import scala.collection.immutable import akka.http.javadsl.model.ContentType -import akka.http.scaladsl.server.directives.{ UserCredentials, ContentTypeResolver } +import akka.http.scaladsl.server.directives.{ Credentials, ContentTypeResolver } import akka.http.scaladsl.server.directives.FileAndResourceDirectives.DirectoryRenderer import akka.http.scaladsl.model.HttpHeader import akka.http.scaladsl.model.headers.{ HttpCookie, CustomHeader } @@ -80,17 +80,40 @@ private[http] object RouteImplementation extends Directives with server.RouteCon authenticateBasicAsync(authenticator.realm, { creds ⇒ val javaCreds = creds match { - case UserCredentials.Missing ⇒ - new BasicUserCredentials { + case Credentials.Missing ⇒ + new BasicCredentials { def available: Boolean = false - def userName: String = throw new IllegalStateException("Credentials missing") - def verifySecret(secret: String): Boolean = throw new IllegalStateException("Credentials missing") + def identifier: String = throw new IllegalStateException("Credentials missing") + def verify(secret: String): Boolean = throw new IllegalStateException("Credentials missing") } - case p @ UserCredentials.Provided(name) ⇒ - new BasicUserCredentials { + case p @ Credentials.Provided(name) ⇒ + new BasicCredentials { def available: Boolean = true - def userName: String = name - def verifySecret(secret: String): Boolean = p.verifySecret(secret) + def identifier: String = name + def verify(secret: String): Boolean = p.verify(secret) + } + } + + authenticator.authenticate(javaCreds) + }).flatMap { user ⇒ + addExtraction(authenticator.asInstanceOf[RequestVal[Any]], user) + } + + case OAuth2Authentication(authenticator) ⇒ + authenticateOAuth2Async(authenticator.realm, { creds ⇒ + val javaCreds = + creds match { + case Credentials.Missing ⇒ + new OAuth2Credentials { + def available: Boolean = false + def identifier: String = throw new IllegalStateException("Credentials missing") + def verify(secret: String): Boolean = throw new IllegalStateException("Credentials missing") + } + case p @ Credentials.Provided(name) ⇒ + new OAuth2Credentials { + def available: Boolean = true + def identifier: String = name + def verify(secret: String): Boolean = p.verify(secret) } } diff --git a/akka-http/src/main/scala/akka/http/impl/server/RouteStructure.scala b/akka-http/src/main/scala/akka/http/impl/server/RouteStructure.scala index 9981538080..6d7bd3a120 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/RouteStructure.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/RouteStructure.scala @@ -6,7 +6,7 @@ package akka.http.impl.server import java.io.File import akka.http.javadsl.model.ws.Message -import akka.http.javadsl.server.values.{ PathMatcher, HttpBasicAuthenticator } +import akka.http.javadsl.server.values.{ PathMatcher, HttpBasicAuthenticator, OAuth2Authenticator } import akka.stream.javadsl.Flow import scala.language.existentials @@ -54,6 +54,7 @@ private[http] object RouteStructure { } case class Extract(extractions: Seq[StandaloneExtractionImpl[_]])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute case class BasicAuthentication(authenticator: HttpBasicAuthenticator[_])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute + case class OAuth2Authentication(authenticator: OAuth2Authenticator[_])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute case class EncodeResponse(coders: immutable.Seq[Coder])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute case class DecodeRequest(coders: immutable.Seq[Coder])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/values/HttpBasicAuthenticator.scala b/akka-http/src/main/scala/akka/http/javadsl/server/values/HttpBasicAuthenticator.scala index 4eb6a562b4..9ce8b01d2f 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/values/HttpBasicAuthenticator.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/values/HttpBasicAuthenticator.scala @@ -12,22 +12,23 @@ import scala.concurrent.Future import scala.reflect.ClassTag /** - * Represents existing or missing HTTP Basic authentication credentials. + * Represents existing or missing Http Basic authentication credentials. */ -trait BasicUserCredentials { +trait BasicCredentials { /** * Were credentials provided in the request? */ def available: Boolean /** - * The username as sent in the request. + * The identifier as sent in the request. */ - def userName: String + def identifier: String + /** * Verifies the given secret against the one sent in the request. */ - def verifySecret(secret: String): Boolean + def verify(secret: String): Boolean } /** @@ -37,7 +38,7 @@ trait BasicUserCredentials { */ abstract class HttpBasicAuthenticator[T](val realm: String) extends AbstractDirective with ExtractionImplBase[T] with RequestVal[T] { protected[http] implicit def classTag: ClassTag[T] = reflect.classTag[AnyRef].asInstanceOf[ClassTag[T]] - def authenticate(credentials: BasicUserCredentials): Future[Option[T]] + def authenticate(credentials: BasicCredentials): Future[Option[T]] /** * Creates a return value for use in [[authenticate]] that successfully authenticates the requests and provides diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/values/OAuth2Authenticator.scala b/akka-http/src/main/scala/akka/http/javadsl/server/values/OAuth2Authenticator.scala new file mode 100644 index 0000000000..6845115048 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/javadsl/server/values/OAuth2Authenticator.scala @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.javadsl.server.values + +import akka.http.impl.server.{ ExtractionImplBase, RouteStructure } +import akka.http.javadsl.server.{ AbstractDirective, RequestVal, Route } +import akka.http.scaladsl.util.FastFuture + +import scala.concurrent.Future +import scala.reflect.ClassTag + +/** + * Represents existing or missing OAuth 2 authentication credentials. + */ +trait OAuth2Credentials { + /** + * Were credentials provided in the request? + */ + def available: Boolean + + /** + * The identifier as sent in the request. + */ + def identifier: String + + /** + * Verifies the given secret against the one sent in the request. + */ + def verify(secret: String): Boolean +} + +/** + * Implement this class to provide an OAuth 2 Bearer Token authentication check. The [[authenticate]] method needs to be implemented + * to check if the supplied or missing credentials are authenticated and provide a domain level object representing + * the user as a [[RequestVal]]. + */ +abstract class OAuth2Authenticator[T](val realm: String) extends AbstractDirective with ExtractionImplBase[T] with RequestVal[T] { + protected[http] implicit def classTag: ClassTag[T] = reflect.classTag[AnyRef].asInstanceOf[ClassTag[T]] + def authenticate(credentials: OAuth2Credentials): Future[Option[T]] + + /** + * Creates a return value for use in [[authenticate]] that successfully authenticates the requests and provides + * the given user. + */ + def authenticateAs(user: T): Future[Option[T]] = FastFuture.successful(Some(user)) + + /** + * Refuses access for this user. + */ + def refuseAccess(): Future[Option[T]] = FastFuture.successful(None) + + /** + * INTERNAL API + */ + protected[http] final def createRoute(first: Route, others: Array[Route]): Route = + RouteStructure.OAuth2Authentication(this)(first, others.toList) +} diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/SecurityDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/SecurityDirectives.scala index 95e4058167..561522ebd5 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/SecurityDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/SecurityDirectives.scala @@ -29,10 +29,10 @@ trait SecurityDirectives { */ type AuthenticationResult[+T] = Either[HttpChallenge, T] - type Authenticator[T] = UserCredentials ⇒ Option[T] - type AsyncAuthenticator[T] = UserCredentials ⇒ Future[Option[T]] - type AuthenticatorPF[T] = PartialFunction[UserCredentials, T] - type AsyncAuthenticatorPF[T] = PartialFunction[UserCredentials, Future[T]] + type Authenticator[T] = Credentials ⇒ Option[T] + type AsyncAuthenticator[T] = Credentials ⇒ Future[Option[T]] + type AuthenticatorPF[T] = PartialFunction[Credentials, T] + type AsyncAuthenticatorPF[T] = PartialFunction[Credentials, Future[T]] /** * Extracts the potentially present [[HttpCredentials]] provided with the request's [[Authorization]] header. @@ -55,8 +55,8 @@ trait SecurityDirectives { */ def authenticateBasicAsync[T](realm: String, authenticator: AsyncAuthenticator[T]): AuthenticationDirective[T] = extractExecutionContext.flatMap { implicit ec ⇒ - authenticateOrRejectWithChallenge[BasicHttpCredentials, T] { basic ⇒ - authenticator(UserCredentials(basic)).fast.map { + authenticateOrRejectWithChallenge[BasicHttpCredentials, T] { cred ⇒ + authenticator(Credentials(cred)).fast.map { case Some(t) ⇒ AuthenticationResult.success(t) case None ⇒ AuthenticationResult.failWithChallenge(challengeFor(realm)) } @@ -83,6 +83,49 @@ trait SecurityDirectives { else FastFuture.successful(None)) } + /** + * A directive that wraps the inner route with OAuth2 Bearer Token authentication support. + * The given authenticator determines whether the credentials in the request are valid + * and, if so, which user object to supply to the inner route. + */ + def authenticateOAuth2[T](realm: String, authenticator: Authenticator[T]): AuthenticationDirective[T] = + authenticateOAuth2Async(realm, cred ⇒ FastFuture.successful(authenticator(cred))) + + /** + * A directive that wraps the inner route with OAuth2 Bearer Token authentication support. + * The given authenticator determines whether the credentials in the request are valid + * and, if so, which user object to supply to the inner route. + */ + def authenticateOAuth2Async[T](realm: String, authenticator: AsyncAuthenticator[T]): AuthenticationDirective[T] = + extractExecutionContext.flatMap { implicit ec ⇒ + authenticateOrRejectWithChallenge[OAuth2BearerToken, T] { cred ⇒ + authenticator(Credentials(cred)).fast.map { + case Some(t) ⇒ AuthenticationResult.success(t) + case None ⇒ AuthenticationResult.failWithChallenge(challengeFor(realm)) + } + } + } + + /** + * A directive that wraps the inner route with OAuth2 Bearer Token authentication support. + * The given authenticator determines whether the credentials in the request are valid + * and, if so, which user object to supply to the inner route. + */ + def authenticateOAuth2PF[T](realm: String, authenticator: AuthenticatorPF[T]): AuthenticationDirective[T] = + authenticateOAuth2(realm, authenticator.lift) + + /** + * A directive that wraps the inner route with OAuth2 Bearer Token authentication support. + * The given authenticator determines whether the credentials in the request are valid + * and, if so, which user object to supply to the inner route. + */ + def authenticateOAuth2PFAsync[T](realm: String, authenticator: AsyncAuthenticatorPF[T]): AuthenticationDirective[T] = + extractExecutionContext.flatMap { implicit ec ⇒ + authenticateOAuth2Async(realm, credentials ⇒ + if (authenticator isDefinedAt credentials) authenticator(credentials).fast.map(Some(_)) + else FastFuture.successful(None)) + } + /** * 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 @@ -134,24 +177,29 @@ object SecurityDirectives extends SecurityDirectives /** * Represents authentication credentials supplied with a request. Credentials can either be - * [[UserCredentials.Missing]] or can be [[UserCredentials.Provided]] in which case a username is + * [[Credentials.Missing]] or can be [[Credentials.Provided]] in which case an identifier 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 +sealed trait Credentials +object Credentials { + case object Missing extends Credentials + abstract case class Provided(identifier: String) extends Credentials { + def verify(secret: String): Boolean } - def apply(cred: Option[BasicHttpCredentials]): UserCredentials = + def apply(cred: Option[HttpCredentials]): Credentials = { cred match { case Some(BasicHttpCredentials(username, receivedSecret)) ⇒ - new UserCredentials.Provided(username) { - def verifySecret(secret: String): Boolean = secret secure_== receivedSecret + new Credentials.Provided(username) { + def verify(secret: String): Boolean = secret secure_== receivedSecret } - case None ⇒ UserCredentials.Missing + case Some(OAuth2BearerToken(token)) ⇒ + new Credentials.Provided(token) { + def verify(secret: String): Boolean = secret secure_== token + } + case None ⇒ Credentials.Missing } + } } import SecurityDirectives._