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?
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"))))
}