Kris Rice
Kris Rice

Reputation: 1161

In Scala/Akka, how would I go about correctly implementing user permissions in combination with oAuth2?

Background:

I have a webserver that uses oAuth2 to verify user credentials. I have a Auth class that defines the methods to verify a user and provide a token, to which you pass the routes you want protected. If the credentials are correct, the protectedRoutes are returned.

This all works really well.

What I would now like to do is define user permissions. I only want some routes available to some users.

Current setup:

Auth trait:

trait Auth extends JSONMarshalling
  with ProtobufMarshalling[ApiCall, ApiResponse] {

  import spray.json._

  def BasicAuthAuthenticator(credentials: Credentials): Option[Route] = {
    credentials match {
      case [email protected](_) =>
        var message = "Incorrect Username"
        val database = DatabaseUtil.getInstance
        var resultingRoute = Option(complete(failedResponse(StatusCodes.Unauthorized, "Incorrect Username")))
        val userResult = database.queryByID[User, String](classOf[User], p.identifier)

        if (userResult.isDefined) {
          val user = classOf[User].cast(userResult.head)
          var loggedInUser = LoggedInUser(user)
          if (p.verify(user.password)) {
            if (user.username == "system") {
              loggedInUser = LoggedInUser(user, oneTime = true) // <- this is a one time use token, for security
            }
            loggedInUsers.append(loggedInUser)
            val token: AuthToken = loggedInUser.oAuthToken
            resultingRoute = Option(complete(token)) // <- Auth passed
          } else { // <- password is incorrect
            if (user.username == "system") {
              resultingRoute = Option(complete(failedResponse(StatusCodes.Unauthorized, "Invalid license key")))
            } else {
              resultingRoute = Option(complete(failedResponse(StatusCodes.Unauthorized, "Incorrect Password")))
            }
          }
        }
        resultingRoute

      case _ =>
        Option(complete(failedResponse(StatusCodes.Unauthorized, "Credentials Missing")))
    }
  }

  def oAuthAuthenticator(credentials: Credentials, protectedRoutes: Route): Option[Route] =
    credentials match {
      case [email protected](_) =>
        val user = loggedInUsers.find(user => p.verify(user.oAuthToken.access_token))
        if (user.isDefined) {
          if (user.head.oneTime) loggedInUsers -= user.head
          Option(protectedRoutes)
        } else {
          Option(complete(ApiResponse().withErrorResponse(ApiErrorResponse(StatusCodes.Unauthorized._1, "Token does not exist"))))
        }
      case _ =>
        Option(complete(ApiResponse().withErrorResponse(ApiErrorResponse(StatusCodes.Unauthorized._1, "No credentials provided"))))
    }

  private def failedResponse(statusCode: StatusCode, message: String): HttpResponse = {
    HttpResponse(statusCode, entity = HttpEntity(ContentTypes.`application/json`, ErrorResponse(message).toJson.toString))
  }

}

My HydraRoute class (used by multiple http/https sockets):

class HydraRoute(apiRoute: Route) extends Auth with CORSHandler {

  def masterRoute: Route = {
    concat(
      authRoute,
      protectedRoute,
      pingRoute
    )
  }
  
  private lazy val pingRoute: Route = {
    pathPrefix("ping") {
      pathEndOrSingleSlash {
        get {
          complete(StatusCodes.OK)
        }
      }
    }
  }

  private lazy val authRoute: Route = {
    pathPrefix("auth") {
      pathEndOrSingleSlash {
        authenticateBasic(realm = "auth", BasicAuthAuthenticator) { authResponse =>
          post {
            authResponse
          }
        }
      }
    }
  }

  private lazy val protectedRoute: Route = {
    authenticateOAuth2(realm = "api", oAuthAuthenticator(_, apiRoute)) { tokenRoute =>
      tokenRoute
    }
  }

}

And finally, to combine these when creating a socket:


Http().newServerAt("localhost", 8080).bind(new HydraRoute(HttpRoutes()).masterRoute)
      .onComplete {
        case Success(binding) =>
          val address = binding.localAddress
          system.log.info(s"HTTP Server is listening on ${address.getHostString}:${address.getPort}")
        case Failure(ex) =>
          system.log.error("HTTP Server could not be started", ex)
          stop()
      }

object HttpRoutes extends ProtobufMarshalling[CertificateRequest, CertificateResponse] {

  def apply()(implicit actorSystem: ActorSystem[_]): Route = {
    pathPrefix("api") {
      path("certificate") {
        pathEndOrSingleSlash {
          post {
            entity(as[CertificateRequest]) { certificateRequest => complete(SSLManager.registerEncryptedCertificate(certificateRequest)) }
          }
        }
      }
    }
  }
}

Initial Approach:

I added an enum with a case for each user, into the Auth trait, like such:

enum UserPermissions(allowedRoutes: ArrayBuffer[Route]) {
  case ADMIN extends UserPermissions(ArrayBuffer.empty[Route])
  case REGISTRATION extends UserPermission(ArrayBuffer.empty[Route])
  case SYSTEM extends UserPermissions(ArrayBuffer.empty[Route])
  
  def getAllowedRoutes: ArrayBuffer = allowedRoutes
  
  def addRoute(route: Route): Unit = allowedRoutes.append(route)
}

def registerRoute(username: String, route: Route): Unit = {
  val userPermission: UserPermissions = UserPermissions.valueOf(username.toUpperCase)
  if (!userPermission.getAllowedRoutes.contains(route)) {
    userPermission.addRoute(route)
  }
}

What I would like to do, inside the oAuthAuthenticator is something like this:

      case [email protected](_) =>
        val user = loggedInUsers.find(user => p.verify(user.oAuthToken.access_token))
        if (user.isDefined) {
          val userPermissions = UserPermissions.valueOf(user.head.user.username.toUpperCase) // <- get permissions for this user
          
          if (userPermissions.getAllowedRoutes.contains(/* SOMEHOW GET THE CALLING ROUTE */)) {
            if (user.head.oneTime) loggedInUsers -= user.head // remove token if its a one-time use token
            Option(protectedRoutes)
          } else {
            Option(complete(ApiResponse().withErrorResponse(ApiErrorResponse(StatusCodes.Unauthorized._1, "User does not have permission to access this route"))))
          }
        } else {
          Option(complete(ApiResponse().withErrorResponse(ApiErrorResponse(StatusCodes.Unauthorized._1, "Token does not exist"))))
        }

How would I go about correctly implementing this?

Upvotes: 1

Views: 30

Answers (1)

Kris Rice
Kris Rice

Reputation: 1161

I think I have a solution.

Using this answer, I was able to extract the calling URI and convert it to a string. I simply register URI strings into the appropriate UserPermissions enum case, then check it in oAuth2 function.

Revised oAuth2 route:

  private lazy val protectedRoute: Route = {
    extractUri { uri =>
      val callingURI = uri.toRelative.path.dropChars(1).toString

      actorSystem.log.info(s"Calling URI:$callingURI")

      authenticateOAuth2(realm = "api", oAuthAuthenticator(_, apiRoute, callingURI)) { tokenRoute =>
        tokenRoute
      }
    }
  }

Revised Auth trait:

enum UserPermissions(allowedRoutes: ArrayBuffer[String]) {
  case ADMIN extends UserPermissions(ArrayBuffer.empty[String])
  case REGISTRATION extends UserPermissions(ArrayBuffer.empty[String])
  case SYSTEM extends UserPermissions(ArrayBuffer.empty[String])
  
  def getAllowedRoutes: ArrayBuffer[String] = allowedRoutes
  
  def addRoute(route: String): Unit = allowedRoutes.append(route)
}

def registerRoute(username: String, route: String): Unit = {
  val userPermission: UserPermissions = UserPermissions.valueOf(username.toUpperCase)
  if (!userPermission.getAllowedRoutes.contains(route)) {
    userPermission.addRoute(route)
  }
}


  def oAuthAuthenticator(credentials: Credentials, protectedRoutes: Route, callingURI: String): Option[Route] =
    credentials match {
      case [email protected](_) =>
        val user = loggedInUsers.find(user => p.verify(user.oAuthToken.access_token))
        if (user.isDefined) {
          val userPermissions = UserPermissions.valueOf(user.head.user.username.toUpperCase) // <- get permissions for this user
          
          if (userPermissions.getAllowedRoutes.contains(callingURI)) {
            if (user.head.oneTime) loggedInUsers -= user.head // remove token if its a one-time use token
            Option(protectedRoutes)
          } else {
            Option(complete(ApiResponse().withErrorResponse(ApiErrorResponse(StatusCodes.Unauthorized._1, "User does not have permission to access this route"))))
          }
        } else {
          Option(complete(ApiResponse().withErrorResponse(ApiErrorResponse(StatusCodes.Unauthorized._1, "Token does not exist"))))
        }
      case _ =>
        Option(complete(ApiResponse().withErrorResponse(ApiErrorResponse(StatusCodes.Unauthorized._1, "No credentials provided"))))
    }

Upvotes: 1

Related Questions