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

pl.touk.nussknacker.ui.security.oauth2.OAuth2Authenticator.scala Maven / Gradle / Ivy

package pl.touk.nussknacker.ui.security.oauth2

import akka.http.scaladsl.server.directives.Credentials.Provided
import akka.http.scaladsl.server.directives.{Credentials, SecurityDirectives}
import cats.data.NonEmptyList
import com.typesafe.scalalogging.LazyLogging
import io.circe.Json
import pl.touk.nussknacker.engine.util.SensitiveDataMasker
import pl.touk.nussknacker.engine.util.SensitiveDataMasker.JsonMasker
import pl.touk.nussknacker.ui.security.api.AuthenticatedUser
import pl.touk.nussknacker.ui.security.oauth2.jwt.{ParsedJwtToken, RawJwtToken}
import sttp.client3.SttpBackend

import java.security.Key
import scala.concurrent.{ExecutionContext, Future}

class OAuth2Authenticator(service: OAuth2Service[AuthenticatedUser, _])(
    implicit ec: ExecutionContext,
    sttpBackend: SttpBackend[Future, Any]
) extends SecurityDirectives.AsyncAuthenticator[AuthenticatedUser]
    with LazyLogging {

  def apply(credentials: Credentials): Future[Option[AuthenticatedUser]] =
    authenticate(credentials)

  private[security] def authenticate(credentials: Credentials): Future[Option[AuthenticatedUser]] = {
    credentials match {
      case Provided(token) => authenticate(token)
      case _               => Future.successful(Option.empty)
    }
  }

  private[oauth2] def authenticate(token: String): Future[Option[AuthenticatedUser]] =
    service.checkAuthorizationAndAuthenticateUser(token).map(prf => Option(prf._1)).recover {
      case OAuth2ErrorHandler(ex) =>
        logger.debug("Access token rejected:", ex)
        Option.empty // Expired or non-exists token - user not authenticated
    }

}

object OAuth2Authenticator extends LazyLogging {

  def apply(
      service: OAuth2Service[AuthenticatedUser, _]
  )(implicit ec: ExecutionContext, sttpBackend: SttpBackend[Future, Any]): OAuth2Authenticator =
    new OAuth2Authenticator(service)

}

object OAuth2ErrorHandler {

  def unapply(t: Throwable): Option[Throwable] = Some(t).filter(apply)

  def apply(t: Throwable): Boolean = t match {
    case OAuth2CompoundException(errors) => errors.toList.collectFirst { case e @ OAuth2ServerError(_) => e }.isEmpty
    case _                               => false
  }

  trait OAuth2Error {
    def msg: String
  }

  final case class OAuth2CompoundException(errors: NonEmptyList[OAuth2Error]) extends Exception {
    override def getMessage: String =
      errors.toList.mkString("OAuth2 exception with the following errors:\n - ", "\n - ", "")
  }

  trait OAuth2JwtError extends OAuth2Error

  final case class OAuth2JwtDecodeRawError(rawToken: RawJwtToken, cause: Throwable) extends OAuth2JwtError {
    override def msg: String = s"Failure in jwt decode: ${cause.getLocalizedMessage}. Token: ${rawToken.masked}"
  }

  final case class OAuth2JwtKeyDetermineError(token: ParsedJwtToken, cause: Throwable) extends OAuth2JwtError {
    override def msg: String = s"Failure in key determining: ${cause.getLocalizedMessage}. Token: ${token.masked}"
  }

  final case class OAuth2JwtDecodeClaimsError(token: ParsedJwtToken, key: Key, cause: Throwable)
      extends OAuth2JwtError {
    override def msg: String =
      s"Failure in decoding json using key: ${cause.getLocalizedMessage}. Token: ${token.masked}"
  }

  final case class OAuth2JwtDecodeClaimsJsonError(
      token: ParsedJwtToken,
      key: Key,
      tokenClaimsJson: Json,
      cause: Throwable
  ) extends OAuth2JwtError {
    override def msg: String =
      s"Failure in decoding token claims: ${mask(cause)}. Token: ${token.masked}, token claims: ${tokenClaimsJson.masked.noSpaces}"
  }

  private def mask(cause: Throwable) = {
    cause.getLocalizedMessage.replaceAll("Got value '.*'", s"Got value '${SensitiveDataMasker.placeholder}'")
  }

  final case class OAuth2AuthenticationRejection(msg: String) extends OAuth2Error

  final case class OAuth2AccessTokenRejection(msg: String) extends OAuth2Error

  final case class OAuth2ServerError(msg: String) extends OAuth2Error
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy