aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/xyz/driver/core/rest/auth
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/scala/xyz/driver/core/rest/auth')
-rw-r--r--src/main/scala/xyz/driver/core/rest/auth/AlwaysAllowAuthorization.scala14
-rw-r--r--src/main/scala/xyz/driver/core/rest/auth/AuthProvider.scala64
-rw-r--r--src/main/scala/xyz/driver/core/rest/auth/Authorization.scala11
-rw-r--r--src/main/scala/xyz/driver/core/rest/auth/AuthorizationResult.scala22
-rw-r--r--src/main/scala/xyz/driver/core/rest/auth/CachedTokenAuthorization.scala55
-rw-r--r--src/main/scala/xyz/driver/core/rest/auth/ChainedAuthorization.scala27
6 files changed, 193 insertions, 0 deletions
diff --git a/src/main/scala/xyz/driver/core/rest/auth/AlwaysAllowAuthorization.scala b/src/main/scala/xyz/driver/core/rest/auth/AlwaysAllowAuthorization.scala
new file mode 100644
index 0000000..ea29a6a
--- /dev/null
+++ b/src/main/scala/xyz/driver/core/rest/auth/AlwaysAllowAuthorization.scala
@@ -0,0 +1,14 @@
+package xyz.driver.core.rest.auth
+
+import xyz.driver.core.auth.{Permission, User}
+import xyz.driver.core.rest.ServiceRequestContext
+
+import scala.concurrent.Future
+
+class AlwaysAllowAuthorization[U <: User] extends Authorization[U] {
+ override def userHasPermissions(user: U, permissions: Seq[Permission])(
+ implicit ctx: ServiceRequestContext): Future[AuthorizationResult] = {
+ val permissionsMap = permissions.map(_ -> true).toMap
+ Future.successful(AuthorizationResult(authorized = permissionsMap, ctx.permissionsToken))
+ }
+}
diff --git a/src/main/scala/xyz/driver/core/rest/auth/AuthProvider.scala b/src/main/scala/xyz/driver/core/rest/auth/AuthProvider.scala
new file mode 100644
index 0000000..35b65f7
--- /dev/null
+++ b/src/main/scala/xyz/driver/core/rest/auth/AuthProvider.scala
@@ -0,0 +1,64 @@
+package xyz.driver.core.rest.auth
+
+import akka.http.scaladsl.model.headers.HttpChallenges
+import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsRejected
+import com.typesafe.scalalogging.Logger
+import xyz.driver.core._
+import xyz.driver.core.auth.{Permission, User}
+import xyz.driver.core.rest.{AuthorizedServiceRequestContext, ServiceRequestContext, serviceContext}
+
+import scala.concurrent.{ExecutionContext, Future}
+import scala.util.{Failure, Success}
+
+import scalaz.Scalaz.futureInstance
+import scalaz.OptionT
+
+abstract class AuthProvider[U <: User](val authorization: Authorization[U], log: Logger)(
+ implicit execution: ExecutionContext) {
+
+ import akka.http.scaladsl.server._
+ import Directives._
+
+ /**
+ * Specific implementation on how to extract user from request context,
+ * can either need to do a network call to auth server or extract everything from self-contained token
+ *
+ * @param ctx set of request values which can be relevant to authenticate user
+ * @return authenticated user
+ */
+ def authenticatedUser(implicit ctx: ServiceRequestContext): OptionT[Future, U]
+
+ /**
+ * Verifies if request is authenticated and authorized to have `permissions`
+ */
+ def authorize(permissions: Permission*): Directive1[AuthorizedServiceRequestContext[U]] = {
+ serviceContext flatMap { ctx =>
+ onComplete {
+ (for {
+ authToken <- OptionT.optionT(Future.successful(ctx.authToken))
+ user <- authenticatedUser(ctx)
+ authCtx = ctx.withAuthenticatedUser(authToken, user)
+ authorizationResult <- authorization.userHasPermissions(user, permissions)(authCtx).toOptionT
+
+ cachedPermissionsAuthCtx = authorizationResult.token.fold(authCtx)(authCtx.withPermissionsToken)
+ allAuthorized = permissions.forall(authorizationResult.authorized.getOrElse(_, false))
+ } yield (cachedPermissionsAuthCtx, allAuthorized)).run
+ } flatMap {
+ case Success(Some((authCtx, true))) => provide(authCtx)
+ case Success(Some((authCtx, false))) =>
+ val challenge =
+ HttpChallenges.basic(s"User does not have the required permissions: ${permissions.mkString(", ")}")
+ log.warn(
+ s"User ${authCtx.authenticatedUser} does not have the required permissions: ${permissions.mkString(", ")}")
+ reject(AuthenticationFailedRejection(CredentialsRejected, challenge))
+ case Success(None) =>
+ log.warn(
+ s"Wasn't able to find authenticated user for the token provided to verify ${permissions.mkString(", ")}")
+ reject(ValidationRejection(s"Wasn't able to find authenticated user for the token provided"))
+ case Failure(t) =>
+ log.warn(s"Wasn't able to verify token for authenticated user to verify ${permissions.mkString(", ")}", t)
+ reject(ValidationRejection(s"Wasn't able to verify token for authenticated user", Some(t)))
+ }
+ }
+ }
+}
diff --git a/src/main/scala/xyz/driver/core/rest/auth/Authorization.scala b/src/main/scala/xyz/driver/core/rest/auth/Authorization.scala
new file mode 100644
index 0000000..87d0614
--- /dev/null
+++ b/src/main/scala/xyz/driver/core/rest/auth/Authorization.scala
@@ -0,0 +1,11 @@
+package xyz.driver.core.rest.auth
+
+import xyz.driver.core.auth.{Permission, User}
+import xyz.driver.core.rest.ServiceRequestContext
+
+import scala.concurrent.Future
+
+trait Authorization[U <: User] {
+ def userHasPermissions(user: U, permissions: Seq[Permission])(
+ implicit ctx: ServiceRequestContext): Future[AuthorizationResult]
+}
diff --git a/src/main/scala/xyz/driver/core/rest/auth/AuthorizationResult.scala b/src/main/scala/xyz/driver/core/rest/auth/AuthorizationResult.scala
new file mode 100644
index 0000000..efe28c9
--- /dev/null
+++ b/src/main/scala/xyz/driver/core/rest/auth/AuthorizationResult.scala
@@ -0,0 +1,22 @@
+package xyz.driver.core.rest.auth
+
+import xyz.driver.core.auth.{Permission, PermissionsToken}
+
+import scalaz.Scalaz.mapMonoid
+import scalaz.Semigroup
+import scalaz.syntax.semigroup._
+
+final case class AuthorizationResult(authorized: Map[Permission, Boolean], token: Option[PermissionsToken])
+object AuthorizationResult {
+ val unauthorized: AuthorizationResult = AuthorizationResult(authorized = Map.empty, None)
+
+ implicit val authorizationSemigroup: Semigroup[AuthorizationResult] = new Semigroup[AuthorizationResult] {
+ private implicit val authorizedBooleanSemigroup = Semigroup.instance[Boolean](_ || _)
+ private implicit val permissionsTokenSemigroup =
+ Semigroup.instance[Option[PermissionsToken]]((a, b) => b.orElse(a))
+
+ override def append(a: AuthorizationResult, b: => AuthorizationResult): AuthorizationResult = {
+ AuthorizationResult(a.authorized |+| b.authorized, a.token |+| b.token)
+ }
+ }
+}
diff --git a/src/main/scala/xyz/driver/core/rest/auth/CachedTokenAuthorization.scala b/src/main/scala/xyz/driver/core/rest/auth/CachedTokenAuthorization.scala
new file mode 100644
index 0000000..4f4c811
--- /dev/null
+++ b/src/main/scala/xyz/driver/core/rest/auth/CachedTokenAuthorization.scala
@@ -0,0 +1,55 @@
+package xyz.driver.core.rest.auth
+
+import java.nio.file.{Files, Path}
+import java.security.{KeyFactory, PublicKey}
+import java.security.spec.X509EncodedKeySpec
+
+import pdi.jwt.{Jwt, JwtAlgorithm}
+import xyz.driver.core.auth.{Permission, User}
+import xyz.driver.core.rest.ServiceRequestContext
+
+import scala.concurrent.Future
+import scalaz.syntax.std.boolean._
+
+class CachedTokenAuthorization[U <: User](publicKey: => PublicKey, issuer: String) extends Authorization[U] {
+ override def userHasPermissions(user: U, permissions: Seq[Permission])(
+ implicit ctx: ServiceRequestContext): Future[AuthorizationResult] = {
+ import spray.json._
+
+ def extractPermissionsFromTokenJSON(tokenObject: JsObject): Option[Map[String, Boolean]] =
+ tokenObject.fields.get("permissions").collect {
+ case JsObject(fields) =>
+ fields.collect {
+ case (key, JsBoolean(value)) => key -> value
+ }
+ }
+
+ val result = for {
+ token <- ctx.permissionsToken
+ jwt <- Jwt.decode(token.value, publicKey, Seq(JwtAlgorithm.RS256)).toOption
+ jwtJson = jwt.parseJson.asJsObject
+
+ // Ensure jwt is for the currently authenticated user and the correct issuer, otherwise return None
+ _ <- jwtJson.fields.get("sub").contains(JsString(user.id.value)).option(())
+ _ <- jwtJson.fields.get("iss").contains(JsString(issuer)).option(())
+
+ permissionsMap <- extractPermissionsFromTokenJSON(jwtJson)
+
+ authorized = permissions.map(p => p -> permissionsMap.getOrElse(p.toString, false)).toMap
+ } yield AuthorizationResult(authorized, Some(token))
+
+ Future.successful(result.getOrElse(AuthorizationResult.unauthorized))
+ }
+}
+
+object CachedTokenAuthorization {
+ def apply[U <: User](publicKeyFile: Path, issuer: String): CachedTokenAuthorization[U] = {
+ lazy val publicKey: PublicKey = {
+ val publicKeyBase64Encoded = new String(Files.readAllBytes(publicKeyFile)).trim
+ val publicKeyBase64Decoded = java.util.Base64.getDecoder.decode(publicKeyBase64Encoded)
+ val spec = new X509EncodedKeySpec(publicKeyBase64Decoded)
+ KeyFactory.getInstance("RSA").generatePublic(spec)
+ }
+ new CachedTokenAuthorization[U](publicKey, issuer)
+ }
+}
diff --git a/src/main/scala/xyz/driver/core/rest/auth/ChainedAuthorization.scala b/src/main/scala/xyz/driver/core/rest/auth/ChainedAuthorization.scala
new file mode 100644
index 0000000..f5eb402
--- /dev/null
+++ b/src/main/scala/xyz/driver/core/rest/auth/ChainedAuthorization.scala
@@ -0,0 +1,27 @@
+package xyz.driver.core.rest.auth
+
+import xyz.driver.core.auth.{Permission, User}
+import xyz.driver.core.rest.ServiceRequestContext
+
+import scala.concurrent.{ExecutionContext, Future}
+import scalaz.Scalaz.{futureInstance, listInstance}
+import scalaz.syntax.semigroup._
+import scalaz.syntax.traverse._
+
+class ChainedAuthorization[U <: User](authorizations: Authorization[U]*)(implicit execution: ExecutionContext)
+ extends Authorization[U] {
+
+ override def userHasPermissions(user: U, permissions: Seq[Permission])(
+ implicit ctx: ServiceRequestContext): Future[AuthorizationResult] = {
+ def allAuthorized(permissionsMap: Map[Permission, Boolean]): Boolean =
+ permissions.forall(permissionsMap.getOrElse(_, false))
+
+ authorizations.toList.foldLeftM[Future, AuthorizationResult](AuthorizationResult.unauthorized) {
+ (authResult, authorization) =>
+ if (allAuthorized(authResult.authorized)) Future.successful(authResult)
+ else {
+ authorization.userHasPermissions(user, permissions).map(authResult |+| _)
+ }
+ }
+ }
+}