Generalised Authentication Directives to accept Bearer Tokens

Make type of credential authentication explicit

Cleanup
This commit is contained in:
pjan vandaele 2015-06-03 17:33:18 +09:00
parent 8d72e30235
commit 4dde089d82
9 changed files with 276 additions and 55 deletions

View file

@ -17,45 +17,72 @@ public class HttpBasicAuthenticationTest extends JUnitRouteTest {
HttpBasicAuthenticator<String> authenticatedUser =
new HttpBasicAuthenticator<String>("test-realm") {
@Override
public Future<Option<String>> authenticate(BasicUserCredentials credentials) {
public Future<Option<String>> 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<String> authenticatedToken =
new OAuth2Authenticator<String>("test-realm") {
@Override
public Future<Option<String>> authenticate(OAuth2Credentials credentials) {
if (credentials.available() && // no anonymous access
credentials.identifier().equals("myToken") &&
credentials.verify("myToken"))
return authenticateAs("myToken");
else return refuseAccess();
}
};
Handler1<String> helloWorldHandler =
new Handler1<String>() {
@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)

View file

@ -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({

View file

@ -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