aboutsummaryrefslogtreecommitdiff
path: root/core-rest/src/main/scala/xyz/driver/core/rest/auth
diff options
context:
space:
mode:
Diffstat (limited to 'core-rest/src/main/scala/xyz/driver/core/rest/auth')
-rw-r--r--core-rest/src/main/scala/xyz/driver/core/rest/auth/AlwaysAllowAuthorization.scala14
-rw-r--r--core-rest/src/main/scala/xyz/driver/core/rest/auth/AuthProvider.scala75
-rw-r--r--core-rest/src/main/scala/xyz/driver/core/rest/auth/Authorization.scala11
-rw-r--r--core-rest/src/main/scala/xyz/driver/core/rest/auth/AuthorizationResult.scala22
-rw-r--r--core-rest/src/main/scala/xyz/driver/core/rest/auth/CachedTokenAuthorization.scala55
-rw-r--r--core-rest/src/main/scala/xyz/driver/core/rest/auth/ChainedAuthorization.scala27
6 files changed, 204 insertions, 0 deletions
diff --git a/core-rest/src/main/scala/xyz/driver/core/rest/auth/AlwaysAllowAuthorization.scala b/core-rest/src/main/scala/xyz/driver/core/rest/auth/AlwaysAllowAuthorization.scala
new file mode 100644
index 0000000..5007774
--- /dev/null
+++ b/core-rest/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/core-rest/src/main/scala/xyz/driver/core/rest/auth/AuthProvider.scala b/core-rest/src/main/scala/xyz/driver/core/rest/auth/AuthProvider.scala
new file mode 100644
index 0000000..e1a94e1
--- /dev/null
+++ b/core-rest/src/main/scala/xyz/driver/core/rest/auth/AuthProvider.scala
@@ -0,0 +1,75 @@
+package xyz.driver.core.rest.auth
+
+import akka.http.scaladsl.server.directives.Credentials
+import com.typesafe.scalalogging.Logger
+import scalaz.OptionT
+import xyz.driver.core.auth.{AuthToken, Permission, User}
+import xyz.driver.core.rest.errors.{ExternalServiceException, UnauthorizedException}
+import xyz.driver.core.rest.{AuthorizedServiceRequestContext, ContextHeaders, ServiceRequestContext, serviceContext}
+
+import scala.concurrent.{ExecutionContext, Future}
+
+abstract class AuthProvider[U <: User](
+ val authorization: Authorization[U],
+ log: Logger,
+ val realm: String
+)(implicit execution: ExecutionContext) {
+
+ import akka.http.scaladsl.server._
+ import Directives.{authorize => akkaAuthorize, _}
+
+ def this(authorization: Authorization[U], log: Logger)(implicit executionContext: ExecutionContext) =
+ this(authorization, log, "driver.xyz")
+
+ /**
+ * 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]
+
+ protected def authenticator(context: ServiceRequestContext): AsyncAuthenticator[U] = {
+ case Credentials.Missing =>
+ log.info(s"Request (${context.trackingId}) missing authentication credentials")
+ Future.successful(None)
+ case Credentials.Provided(authToken) =>
+ authenticatedUser(context.withAuthToken(AuthToken(authToken))).run.recover({
+ case ExternalServiceException(_, _, Some(UnauthorizedException(_))) => None
+ })
+ }
+
+ /**
+ * Verifies that a user agent is properly authenticated, and (optionally) authorized with the specified permissions
+ */
+ def authorize(
+ context: ServiceRequestContext,
+ permissions: Permission*): Directive1[AuthorizedServiceRequestContext[U]] = {
+ authenticateOAuth2Async[U](realm, authenticator(context)) flatMap { authenticatedUser =>
+ val authCtx = context.withAuthenticatedUser(context.authToken.get, authenticatedUser)
+ onSuccess(authorization.userHasPermissions(authenticatedUser, permissions)(authCtx)) flatMap {
+ case AuthorizationResult(authorized, token) =>
+ val allAuthorized = permissions.forall(authorized.getOrElse(_, false))
+ akkaAuthorize(allAuthorized) tflatMap { _ =>
+ val cachedPermissionsCtx = token.fold(authCtx)(authCtx.withPermissionsToken)
+ provide(cachedPermissionsCtx)
+ }
+ }
+ }
+ }
+
+ /**
+ * Verifies if request is authenticated and authorized to have `permissions`
+ */
+ def authorize(permissions: Permission*): Directive1[AuthorizedServiceRequestContext[U]] = {
+ serviceContext flatMap (authorize(_, permissions: _*))
+ }
+}
+
+object AuthProvider {
+ val AuthenticationTokenHeader: String = ContextHeaders.AuthenticationTokenHeader
+ val PermissionsTokenHeader: String = ContextHeaders.PermissionsTokenHeader
+ val SetAuthenticationTokenHeader: String = "set-authorization"
+ val SetPermissionsTokenHeader: String = "set-permissions"
+}
diff --git a/core-rest/src/main/scala/xyz/driver/core/rest/auth/Authorization.scala b/core-rest/src/main/scala/xyz/driver/core/rest/auth/Authorization.scala
new file mode 100644
index 0000000..1a5e9be
--- /dev/null
+++ b/core-rest/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/core-rest/src/main/scala/xyz/driver/core/rest/auth/AuthorizationResult.scala b/core-rest/src/main/scala/xyz/driver/core/rest/auth/AuthorizationResult.scala
new file mode 100644
index 0000000..efe28c9
--- /dev/null
+++ b/core-rest/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/core-rest/src/main/scala/xyz/driver/core/rest/auth/CachedTokenAuthorization.scala b/core-rest/src/main/scala/xyz/driver/core/rest/auth/CachedTokenAuthorization.scala
new file mode 100644
index 0000000..66de4ef
--- /dev/null
+++ b/core-rest/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/core-rest/src/main/scala/xyz/driver/core/rest/auth/ChainedAuthorization.scala b/core-rest/src/main/scala/xyz/driver/core/rest/auth/ChainedAuthorization.scala
new file mode 100644
index 0000000..131e7fc
--- /dev/null
+++ b/core-rest/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 |+| _)
+ }
+ }
+ }
+}