!htp #20102 Fix authentication scheme of WWW-Authentication header for OAuth2 over HTTP (#20756)

This commit is contained in:
monkey-mas 2016-06-30 17:28:40 +09:00 committed by Konrad Malawski
parent d4d9c1943e
commit dce174b455
5 changed files with 33 additions and 28 deletions

View file

@ -20,4 +20,12 @@ public abstract class HttpChallenge {
public static HttpChallenge create(String scheme, String realm, Map<String, String> params) {
return new akka.http.scaladsl.model.headers.HttpChallenge(scheme, realm, Util.convertMapToScala(params));
}
}
public static HttpChallenge createBasic(String realm) {
return create("Basic", realm);
}
public static HttpChallenge createOAuth2(String realm) {
return create("Bearer", realm);
}
}

View file

@ -21,3 +21,10 @@ final case class HttpChallenge(scheme: String, realm: String,
/** Java API */
def getParams: util.Map[String, String] = params.asJava
}
object HttpChallenges {
def basic(realm: String): HttpChallenge = HttpChallenge("Basic", realm)
def oAuth2(realm: String): HttpChallenge = HttpChallenge("Bearer", realm)
}

View file

@ -18,18 +18,19 @@ class SecurityDirectivesSpec extends RoutingSpec {
val doOAuth2Auth = authenticateOAuth2PF("MyRealm", { case Credentials.Provided(identifier) identifier })
val authWithAnonymous = doBasicAuth.withAnonymousUser("We are Legion")
val challenge = HttpChallenge("Basic", "MyRealm")
val basicChallenge = HttpChallenges.basic("MyRealm")
val oAuth2Challenge = HttpChallenges.oAuth2("MyRealm")
"basic authentication" should {
"reject requests without Authorization header with an AuthenticationFailedRejection" in {
Get() ~> {
dontBasicAuth { echoComplete }
} ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsMissing, challenge) }
} ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsMissing, basicChallenge) }
}
"reject unauthenticated requests with Authorization header with an AuthenticationFailedRejection" in {
Get() ~> Authorization(BasicHttpCredentials("Bob", "")) ~> {
dontBasicAuth { echoComplete }
} ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsRejected, challenge) }
} ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsRejected, basicChallenge) }
}
"reject requests with an OAuth2 Bearer Token Authorization header with 401" in {
Get() ~> Authorization(OAuth2BearerToken("myToken")) ~> Route.seal {
@ -37,7 +38,7 @@ class SecurityDirectivesSpec extends RoutingSpec {
} ~> check {
status shouldEqual StatusCodes.Unauthorized
responseAs[String] shouldEqual "The supplied authentication is invalid"
header[`WWW-Authenticate`] shouldEqual Some(`WWW-Authenticate`(challenge))
header[`WWW-Authenticate`] shouldEqual Some(`WWW-Authenticate`(oAuth2Challenge))
}
}
"reject requests with illegal Authorization header with 401" in {
@ -46,7 +47,7 @@ class SecurityDirectivesSpec extends RoutingSpec {
} ~> 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))
header[`WWW-Authenticate`] shouldEqual Some(`WWW-Authenticate`(basicChallenge))
}
}
"extract the object representing the user identity created by successful authentication" in {
@ -74,12 +75,12 @@ class SecurityDirectivesSpec extends RoutingSpec {
"reject requests without Authorization header with an AuthenticationFailedRejection" in {
Get() ~> {
dontOAuth2Auth { echoComplete }
} ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsMissing, challenge) }
} ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsMissing, oAuth2Challenge) }
}
"reject unauthenticated requests with Authorization header with an AuthenticationFailedRejection" in {
Get() ~> Authorization(OAuth2BearerToken("myToken")) ~> {
dontOAuth2Auth { echoComplete }
} ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsRejected, challenge) }
} ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsRejected, oAuth2Challenge) }
}
"reject requests with a Basic Authorization header with 401" in {
Get() ~> Authorization(BasicHttpCredentials("Alice", "")) ~> Route.seal {
@ -87,7 +88,7 @@ class SecurityDirectivesSpec extends RoutingSpec {
} ~> check {
status shouldEqual StatusCodes.Unauthorized
responseAs[String] shouldEqual "The supplied authentication is invalid"
header[`WWW-Authenticate`] shouldEqual Some(`WWW-Authenticate`(challenge))
header[`WWW-Authenticate`] shouldEqual Some(`WWW-Authenticate`(basicChallenge))
}
}
"reject requests with illegal Authorization header with 401" in {
@ -96,7 +97,7 @@ class SecurityDirectivesSpec extends RoutingSpec {
} ~> 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))
header[`WWW-Authenticate`] shouldEqual Some(`WWW-Authenticate`(oAuth2Challenge))
}
}
"extract the object representing the user identity created by successful authentication" in {
@ -132,7 +133,7 @@ class SecurityDirectivesSpec extends RoutingSpec {
status shouldEqual StatusCodes.Unauthorized
headers.collect {
case `WWW-Authenticate`(challenge +: Nil) challenge
} shouldEqual Seq(challenge, otherChallenge)
} shouldEqual Seq(basicChallenge, otherChallenge)
}
}
}

View file

@ -301,10 +301,4 @@ abstract class SecurityDirectives extends SchemeDirectives {
def authorizeAsyncWithRequestContext(check: akka.japi.function.Function[RequestContext, CompletionStage[Boolean]], inner: Supplier[Route]): Route = RouteAdapter {
D.authorizeAsync(rc check(RequestContext.wrap(rc)).toScala)(inner.get().delegate)
}
/**
* Creates a `Basic` [[HttpChallenge]] for the given realm.
*/
def challengeFor(realm: String): HttpChallenge = HttpChallenge.create("Basic", realm)
}

View file

@ -13,13 +13,15 @@ import akka.http.scaladsl.util.FastFuture._
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.server.AuthenticationFailedRejection.{ CredentialsRejected, CredentialsMissing }
import scala.util.{ Try, Success }
import scala.util.Success
/**
* 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.
* and [[Authorization]]. Most prominently, HTTP Basic authentication and OAuth 2.0 Authorization Framework
* as defined in RFC 2617 and RFC 6750 respectively.
*
* See: <a href="https://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a>.
* See: <a href="https://www.ietf.org/rfc/rfc6750.txt">RFC 6750</a>.
*
* @groupname security Security directives
* @groupprio security 220
@ -95,7 +97,7 @@ trait SecurityDirectives {
authenticateOrRejectWithChallenge[BasicHttpCredentials, T] { cred
authenticator(Credentials(cred)).fast.map {
case Some(t) AuthenticationResult.success(t)
case None AuthenticationResult.failWithChallenge(challengeFor(realm))
case None AuthenticationResult.failWithChallenge(HttpChallenges.basic(realm))
}
}
}
@ -146,7 +148,7 @@ trait SecurityDirectives {
authenticateOrRejectWithChallenge[OAuth2BearerToken, T] { cred
authenticator(Credentials(cred)).fast.map {
case Some(t) AuthenticationResult.success(t)
case None AuthenticationResult.failWithChallenge(challengeFor(realm))
case None AuthenticationResult.failWithChallenge(HttpChallenges.oAuth2(realm))
}
}
}
@ -248,13 +250,6 @@ trait SecurityDirectives {
}
}
}
/**
* Creates a `Basic` [[HttpChallenge]] for the given realm.
*
* @group security
*/
def challengeFor(realm: String) = HttpChallenge(scheme = "Basic", realm = realm, params = Map.empty)
}
object SecurityDirectives extends SecurityDirectives