scalacats-effecthttp4s

Scala Http4s: combine 2 AuthMiddleware


I create 2 middlewares for 2 authentification roles:

First:

private val basicStudentAuthMethod = Kleisli.apply[IO, Request[IO], Either[String, Student]] { req =>
val authHeader = req.headers.get[Authorization]
authHeader match {
  case Some(Authorization(BasicCredentials(creds))) if creds._1.trim == studentLogin && creds._2.trim == studentPassword =>
    IO(Right(Student(creds._1)))
  case Some(_) => IO(Left("Wrong creds"))
  case None => IO(Left("Auth error"))
 }
}

val studentBasicAuthMiddleware: AuthMiddleware[IO, Student] = AuthMiddleware(basicStudentAuthMethod, onFailure)

val authStudentRoutes = AuthedRoutes.of[Student, IO] {
case GET -> Root / "auth" as _ =>
    Ok("Welcome, student")
} 
   

Second one:

private val basicAdminAuthMethod = Kleisli.apply[IO, Request[IO], Either[String, Admin]] { req =>
val authHeader = req.headers.get[Authorization]
authHeader match {
  case Some(Authorization(BasicCredentials(creds))) if creds._1.trim == adminLogin && creds._2.trim == adminPassword =>
    IO(Right(Admin(creds._1)))
  case Some(_) => IO(Left("Wrong creds"))
  case None => IO(Left("Auth error"))
}
}

val adminBasicAuthMiddleware: AuthMiddleware[IO, Admin] = AuthMiddleware(basicAdminAuthMethod, onFailure)

val authAdminRoutes = AuthedRoutes.of[Admin, IO] {
  case GET -> Root / "auth" as _ =>
     Ok("Welcome, admin")
  }

Common realization for "on failure":

 val onFailure: AuthedRoutes[String, IO] = Kleisli { (req: AuthedRequest[IO, String]) =>
     OptionT.pure[IO](Response(status = Status.Unauthorized))
}

And I'm trying to combine them as follows:

 import cats.MonoidK.ops.toAllMonoidKOps

 val authRoute =  studentBasicAuthMiddleware(authStudentRoutes) <+> adminBasicAuthMiddleware(authAdminRoutes)

The behavior of this code like this, when I pass correct student credentials, it works well.

But when I pass correct Admin credentials, I receive "401 Unauthorized".

Expected, that <+> composition first applies to the request on the basicStudentAuthMethod method, and, if creds are wrong, it then applies to the request on the basicAdminAuthMethod. And, if Admin creds are correct, authentification for Admin with be successful.

But, in fact, first it check, that creds for Student are wrong, than, that creds for Admin are correct, and finally, again check credentials for Student. And since creds for Student are incorrect, authentification failed.

Is there any way to compose 2 widdlewares?

EDIT:

  1. Code at scalastie: https://scastie.scala-lang.org/cgdoGlUqT5KLhvcZlGQ8wQ

  2. Alternative approach: https://scastie.scala-lang.org/vxpvR1iUSUGuKnG0IPT3Hw


Solution

  • AFAIK, the common approach is to have a single AuthMiddleware that can produce a Role.
    Then you can validate that Role on each route and decide what to do with it.

    Something like this:

    sealed trait Role
    final case class Student(login: String) extends Role
    final case class Admin(login: String) extends Role
    
    val studentLogin = "student"
    val studentPassword = "student"
    val adminLogin = "admin"
    val adminPassword = "admin"
    
    val basicAuthMethod =
      Kleisli.apply[IO, Request[IO], Either[String, Role]] { req =>
        val authHeader = req.headers.get[Authorization]
        authHeader match {
          case Some(Authorization(BasicCredentials((login, password)))) =>
            if (login == studentLogin && password == studentPassword)
              IO(Right(Student(login)))
            else if (login == adminLogin && password == adminPassword)
              IO(Right(Admin(login)))
            else
              IO(Left("Wrong creds"))
              
          case _ =>
            IO(Left("Auth error"))
        }
      }
    
    val onFailure: AuthedRoutes[String, IO] =
      Kleisli { (req: AuthedRequest[IO, String]) =>
        OptionT.pure[IO](Response(status = Status.Unauthorized))
      }
    
    val basicAuthMiddleware: AuthMiddleware[IO, Role] =
      AuthMiddleware(basicAuthMethod, onFailure)
    
    val authRoutes = AuthedRoutes.of[Role, IO] {
      case GET -> Root / "auth" as role =>
        role match {
          case Student(studentId) =>
            Ok(s"Welcome, student: ${studentId}")
            
          case Admin(adminId) =>
            Ok(s"Welcome, admin: ${adminId}")
        }
    }
    

    You can see the code running here.