All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.choffmeister.auth.akkahttp.Authenticator.scala Maven / Gradle / Ivy

The newest version!
package de.choffmeister.auth.akkahttp

import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.server.Directive1
import akka.http.scaladsl.server.directives.RouteDirectives._
import akka.http.scaladsl.server.directives._
import akka.pattern.after
import de.choffmeister.auth.akkahttp.util.SimpleScheduler
import de.choffmeister.auth.common.JsonWebToken
import de.choffmeister.auth.common.JsonWebToken._

import scala.concurrent._
import scala.concurrent.duration._
import scala.util.Success

class Authenticator[Auth](
    realm: String,
    bearerTokenSecret: Array[Byte],
    fromUsernamePassword: (String, String) => Future[Option[Auth]],
    fromBearerToken: JsonWebToken => Future[Option[Auth]])(implicit executor: ExecutionContext) extends SecurityDirectives {
  def apply(): Directive1[Auth] = {
    bearerToken(acceptExpired = false).recover { rejs =>
      basic().recover(rejs2 => reject(rejs ++ rejs2: _*))
    }
  }

  def basic(minDelay: Option[FiniteDuration] = None): AuthenticationDirective[Auth] = {
    authenticateOrRejectWithChallenge[BasicHttpCredentials, Auth] {
      case Some(BasicHttpCredentials(username, password)) =>
        val auth = fromUsernamePassword(username, password)
        val delayedAuth = minDelay match {
          case None => auth
          case Some(delay) =>
            val delayed = after[Option[Auth]](delay, SimpleScheduler.instance)(Future(None))

            val promise = Promise[Option[Auth]]()
            auth.onComplete {
              case Success(Some(user)) => promise.success(Some(user))
              case _ => delayed.onComplete(_ => promise.success(None))
            }
            promise.future
        }

        delayedAuth map {
          case Some(user) => grant(user)
          case None => deny
        }

      case None => Future(deny)
    }
  }

  def bearerToken(acceptExpired: Boolean = false): AuthenticationDirective[Auth] = {
    def resolve(token: JsonWebToken): Future[AuthenticationResult[Auth]] = fromBearerToken(token).map {
      case Some(user) => grant(user)
      case None => deny(None)
    }

    authenticateOrRejectWithChallenge[OAuth2BearerToken, Auth] {
      case Some(OAuth2BearerToken(tokenStr)) =>
        JsonWebToken.read(tokenStr, bearerTokenSecret) match {
          case Right(token) => resolve(token)
          case Left(Expired(token)) if acceptExpired => resolve(token)
          case Left(error) => Future(deny(Some(error)))
        }
      case None => Future(deny(Some(Missing)))
    }
  }

  private def grant(user: Auth) = AuthenticationResult.success(user)
  private def deny = AuthenticationResult.failWithChallenge(createBasicChallenge)
  private def deny(error: Option[Error]) = AuthenticationResult.failWithChallenge(createBearerTokenChallenge(error))

  private def createBasicChallenge: HttpChallenge = {
    HttpChallenge("Basic", realm)
  }

  private def createBearerTokenChallenge(error: Option[Error]): HttpChallenge = {
    val desc = error match {
      case None => None
      case Some(Missing) => None
      case Some(Malformed) => Some("The access token is malformed")
      case Some(InvalidSignature) => Some("The access token has been manipulated")
      case Some(Incomplete) => Some("The token must at least contain the iat and exp claim")
      case Some(Expired(_)) => Some("The access token expired")
      case Some(UnsupportedAlgorithm(algo)) => Some(s"The signature algorithm $algo is not supported")
      case Some(Unknown) => Some("An unknown error occured")
    }
    val params = desc match {
      case Some(msg) => Map("error" -> "invalid_token", "error_description" -> msg)
      case None => Map.empty[String, String]
    }
    HttpChallenge("Bearer", realm, params)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy