How to unit test OAuth2BearerToken in route header in akka http - scala

i have a route which needs an access token in its header to grant access to it for that i have this working code
def accessProtectedResource: server.Route =
path("access-protected-resource") {
get {
bearerToken { token =>
token match {
case Some(tokenValue) =>
complete(OK, routeResponseMessage.getResponse(OK.intValue,ServerMessages.AUTH_PASS,JsObject.empty))
case None => reject(AuthorizationFailedRejection)
}
}
}
}
private def bearerToken: Directive1[Option[String]] =
for {
authBearerHeader <- optionalHeaderValueByType(classOf[Authorization]).map(extractBearerToken)
xAuthCookie <- optionalCookie("X-Authorization-Token").map(_.map(_.value))
} yield authBearerHeader.orElse(xAuthCookie)
private def extractBearerToken(authHeader: Option[Authorization]): Option[String] =
authHeader.collect {
case Authorization(OAuth2BearerToken(token)) => token
}
when i hit the route through postman in the Authorization tab i selected the type to Bearer Token and add the token and send the request and everything works fine now i want to unit test this route
for this i am looking at this
but i am confused how can i add the header in a proper way in my unit test here is my code
"pass route /access-protected-resource" in {
routeResponseMessage.getResponse(OK.intValue, ServerMessages.AUTH_PASS, JsObject.empty)
val originHeader = Authorization(OAuth2BearerToken("accessTokenString"))
Get("http://0.0.0.0:8083/get-user-token") ~> originHeader ~> authenticationController.route ~> check {
}
}
but my route is getting rejected
- pass route /access-protected-resource *** FAILED ***
[info] Request was rejected with rejection MethodRejection(HttpMethod(POST)) (CheckValidUserTokenExistsTest.scala:76)
how to do this correctly ?

How about using addCredential and testing only bearerToken method wrapped with Route.seal like this(https://github.com/ItoYo16u/scala-usage-playground/blob/master/src/test/scala/web/akka/JWTDirectivesSpec.scala)?

Related

scala akka does not redirect

I have the following backend. If going to the "localhost:8080", the login page is loaded by redirecting from "/" to "login". Login page is loaded. At submitting the login form, the "perform-login" is called. However, for some reason, there is no redirect to "storage" page. Why?
P.S. If requesting the page "storage" manually, it is loaded. The problem is with the redirect from "login" page to "storage" page. Probably, it has something to do with setting the cookies, as this command also has the return type Route.
Scala version: 2.13.6,
Akka HTTP version: 10.2.6
object Backend {
def main(args: Array[String]) = {
implicit val system = ActorSystem(Behaviors.empty, "lowlevel")
// needed for the future map/flatmap in the end
implicit val executionContext: ExecutionContext = system.executionContext
val topLevelRoute: Route =
concat(
pathSingleSlash {
redirect("login", StatusCodes.PermanentRedirect)
},
path("login") {
complete("my login page")
},
path("storage") {
cookie("token") { tokenCookie =>
println("you managed to login, token:" + tokenCookie.value + "ENDLINE")
complete("my storage page")
}
},
path("perform-login") {
formFields("Username", "Password") { (username, password) =>
var isAbleToLogin = database.isUserLoggedIn(username, password)
if (isAbleToLogin == true) {
setCookie(HttpCookie("token", value="ThisIsMyStrongAccessToken")) {
//TODO: debug why does not redirect to main page
redirect("storage", StatusCodes.PermanentRedirect)
}
}
else {
reject(ValidationRejection("bad credentials"))
}
}
},
path(Remaining) { pathRest =>
reject(ValidationRejection("topLevelRoute, unknown path:" + pathRest + "ENDLINE"))
}
)
val binding = Http().newServerAt("localhost", 8080).bind(topLevelRoute)
println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
StdIn.readLine() // let it run until user presses return
binding
.flatMap(_.unbind()) // trigger unbinding from the port
.onComplete(_ => system.terminate()) // and shutdown when done
}
Solution:
When I was navigating to the "/" page, the redirect to the "/login" page happens due to the "document / redirect" request type (if analysing the network).
But, in case of redirecting from "/login" page to "/storage" page, the request is of type "xhr /redirect" that cannot be done on the server side, i.e. I had to add $(location).attr('href', 'storage') to my jQuery script to make it work.

akka http handleNotFound rejection is only working for POST method

i have the following akka http rejection handling code taken from https://doc.akka.io/docs/akka-http/current/routing-dsl/rejections.html
val message = "The requested resource could not be found."
implicit def myRejectionHandler = RejectionHandler.newBuilder()
.handleNotFound {
complete(HttpResponse(NotFound
,entity = HttpEntity(ContentTypes.`application/json`, s"""{"rejection": "$message"}"""
)))
}.result()
val route: Route = handleRejections(myRejectionHandler) {
handleExceptions(myExceptionHandler) {
concat(
path("event-by-id") {
get {
parameters('id.as[String]) {
id =>
complete("id")
}
}
}
,
post {
path("create-event") {
entity(as[Event]) {
event =>
complete(OK, "inserted")
}
}
}
)
}
}
}
val bindingFuture = Http().bindAndHandle(route, hostName, port)
when i hit localhost:8080/random
i got the message
HTTP method not allowed, supported methods: POST
and when i select POST and hit localhost:8080/random
i got the message
{
"rejection": "The requested resource could not be found."
}
why i did not get the same message when my route request was GET ?
in the docs the handleNotFound was working with GET request https://doc.akka.io/docs/akka-http/current/routing-dsl/rejections.html
This is happens, probably because of order of directives, you are using: in your configuration if incoming request does not match with event-by-id URL path, then it goes to the next handler, which expects that request should have POST method first of all, because post directive goes first, before path("create-event").
What you can try to do is change directives order to the next one, for second route:
path("create-event") {
post {
entity(as[Event]) { event =>
complete(OK, "inserted")
}
}
}
Hope this helps!

Basic Authentication in play framework

How to implement Basic Authentication for web sockets using play framework.
I am creating a web socket using play framework.
I would like to do basic authentication and send 401 if authentication fails.
Below is my code and i am not able to send "{code=401, message=unauthorized access}" as response
def ChatServer(): WebSocket = WebSocket.accept[String, String] { request =>
if (Util.doBasicAuthentication(request.headers)) {
ActorFlow.actorRef { out =>
ChatActor.props(out)
}
} else throw new RuntimeException("Unauthorized Access")
}
Whenever authentication fails, i am not able to send the response back as "unauthorized access" instead i am ending up with exceptions
As described in the Play documentation, use WebSocket.acceptOrResult:
def socket = WebSocket.acceptOrResult[String, String] { request =>
Future.successful {
if (Util.doBasicAuthentication(request.headers)) {
Right(ActorFlow.actorRef { out =>
ChatActor.props(out)
})
} else {
Left(Unauthorized)
}
}
}

Silhouette and mobile application

I've used as example play-silhouette-angular-seed.
Authorization via Satellizer works fine.
When I try to authorize via iOs app I got next error:
com.mohiva.play.silhouette.impl.exceptions.UnexpectedResponseException:
[Silhouette][facebook] Cannot build OAuth2Info because of invalid response format:
List((/access_token,List(ValidationError(List(error.path.missing),WrappedArray()))))
I got an error 400 in this function from OAuth2Provider.scala :
protected def getAccessToken(code: String)(implicit request: RequestHeader): Future[OAuth2Info] = {
httpLayer.url(settings.accessTokenURL).withHeaders(headers: _*).post(Map(
ClientID -> Seq(settings.clientID),
ClientSecret -> Seq(settings.clientSecret),
GrantType -> Seq(AuthorizationCode),
Code -> Seq(code),
RedirectURI -> Seq(resolveCallbackURL(settings.redirectURL))) ++ settings.accessTokenParams.mapValues(Seq(_))).flatMap { response =>
logger.debug("[Silhouette][%s] Access token response: [%s]".format(id, response.body))
Future.from(buildInfo(response))
}
}
This error has been risen because Satellizer for authentication via Facebook send to server an 'authentication code' and Silhouette server use this code to get Facebook 'access token' and create user.
Facebook iOs SDK, instead, obtained 'Access token' and I've tried to send it to server in Json in field 'code' like 'Satellizer.
To resolve this issue I send an 'access token' in Json field named 'access_token' and use next code to authenticate mobile application:
class MobileSocialAuthController #Inject() (
val messagesApi: MessagesApi,
userService: UserService,
authInfoRepository: AuthInfoRepository,
socialProviderRegistry: SocialProviderRegistry,
val env: Environment[User, JWTAuthenticator])
extends Silhouette[User, JWTAuthenticator]
{
def authenticate(provider: String) = UserAwareAction.async(parse.json) {
implicit request =>
provider match {
case "facebook" =>
request.body.asOpt[OAuth2Info] match {
case Some(authInfo) =>
(socialProviderRegistry.get[FacebookProvider](provider) match {
case Some(p: FacebookProvider) =>
for {
profile <-p.retrieveProfile(authInfo)
user <- userService.save(profile)
authInfo <- authInfoRepository.save(profile.loginInfo, authInfo)
authenticator <- env.authenticatorService.create(profile.loginInfo)
token <- env.authenticatorService.init(authenticator)
} yield {
env.eventBus.publish(LoginEvent(user, request, request2Messages))
Ok(Json.obj("token" -> token))
}
case _ => Future.failed(new ProviderException(s"Cannot authenticate with unexpected social provider $provider"))
}).recover {
case e: ProviderException =>
logger.error("Unexpected provider error", e)
Unauthorized(Json.obj("message" -> Messages("could.not.authenticate")))
}
case _ =>
Future(BadRequest(Json.obj(
"message" -> "Bad OAuth2 json.")))
}
case _ =>
Future(BadRequest(Json.obj(
"message" -> "You can use only Facebook account for authentication.")))
}
}
}
As a result, I have a token which I use in ios application to obtain resources.
This happens when the OAuth2Provider gets a response it can't parse, which is, any non-success response. So there can be many reasons for this error, for instance the authorization code is invalid or expired, or you haven't configured the redirect_uri properly (check your Facebook app configuration on the Facebook dev site to set the redirect_uri).
Silhouette does log the response it gets from Facebook which should help you debug what the actual issue is, the log line to look for is in the snippet you provided:
logger.debug("[Silhouette][%s] Access token response:...
So check your logs, there you should see the response from Facebook, likely with an error indicating why they couldn't give you an access_token.

spray authenticate directive returns different HTTP status codes

I am trying a basic authentication on post request in spray.io 1.3.2 using authenticate directive. My code looks following:
val route: Route = {
pathPrefix("ato") {
pathPrefix("v1") {
path("orders" / "updateStatus") {
post {
authenticate(BasicAuth(userPasswordAuthenticator _, realm = "bd ato import api")) {
user =>
entity(as[String]) {e =>
complete {
s"Hello $e "
}
}
}
}
}
}
}
}
def userPasswordAuthenticator(userPass: Option[UserPass]): Future[Option[String]] =
Future {
if (userPass.exists(up => up.user == ato_import_v1_usr && up.pass == ato_import_v1_pwd)) Some("ato_v1")
else None
}
This works perfectly fine, authorized Status Ok 200, unauthorized 401. However when the order of directives is changed as follows:
val route: Route = {
pathPrefix("ato") {
pathPrefix("v1") {
authenticate(BasicAuth(userPasswordAuthenticator _, realm = "bd ato import api")) {
user =>
path("orders" / "updateStatus") {
post {
entity(as[String]) {e =>
complete {
s"Hello $e "
}
}
}
}
}
}
}
}
I am getting Status 405, HTTP method not allowed for unauthorized access. I am not sure why that happens. From certain point it make sense, path is not matched because of missing credentials etc.
Could someone please clarify that?
The reason why I wanted to put authorization at v1 level is that I wanted to make every version protected by different password. Is there a way how to achieve that? What is the best practice in chaining directives?
I would like to follow DRY principle.
Thanks