mitrakov

Authentication example for Http4s

Sep 3rd, 2019
351
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Scala 3.99 KB | None | 0 0
  1. // Example of Nonce-based authentication with http4s.
  2. // Authentication middleware see at https://pastebin.com/G4y4qURn
  3. // for Cryptobits library add to build.sbt: "org.reactormonk" %% "cryptobits" % 1.2
  4.  
  5. import java.security.MessageDigest
  6. import java.util.UUID
  7. import cats.Applicative
  8. import com.typesafe.config.ConfigFactory
  9. import scala.collection.mutable
  10. import scala.compat.Platform
  11. import scala.concurrent.duration._
  12. import scala.util.Try
  13.  
  14. object AuthService {
  15.   sealed trait TokenError
  16.   case object InvalidToken extends TokenError
  17.   case object InvalidUserId extends TokenError
  18.   case object TokenExpired extends TokenError
  19. }
  20.  
  21. class AuthService[F[_]: Applicative](userDao: UserDao[F]) {
  22.   private val config = ConfigFactory.load()
  23.   private val secretKey = config.getString("auth.secret.key")
  24.   private val tokenLifetime = config.getInt("auth.token.lifetime.hours")
  25.  
  26.   private val sha256 = MessageDigest.getInstance("SHA-256")
  27.   private val crypto = CryptoBits(PrivateKey(secretKey.getBytes))
  28.   private val login2nonceMap = mutable.Map.empty[String, String]
  29.  
  30.   def signUp(login: String, passwordHex: String, email: String): F[Either[String, String]] = {
  31.     import cats.implicits._
  32.  
  33.     val validationResult = for {
  34.       _ <- validateUserName(login)
  35.       _ <- validatePassword(passwordHex)
  36.       _ <- validateEmail(email)
  37.     } yield ()
  38.  
  39.     validationResult flatTraverse (_ => storeUser(login, passwordHex, email))
  40.   }
  41.  
  42.   def signIn(login: String, signature: String, cnonce: String): F[Option[String]] = {
  43.     import cats.implicits._
  44.  
  45.     userDao.findUser(login) map { _ flatMap { user =>
  46.       val nonce = login2nonceMap.getOrElse(login, "error")
  47.       val password = user.passwordHash
  48.       val expectedSignature = sha256.digest(s"$nonce$cnonce$password".getBytes).map("%02x" format _).mkString
  49.       user.userId match {
  50.         case Some(id) if signature == expectedSignature =>
  51.           login2nonceMap -= login
  52.           Some(crypto.signToken(id.toString, Platform.currentTime.toString))
  53.         case _ => None
  54.       }
  55.     }}
  56.   }
  57.  
  58.   def userExists(login: String): F[Boolean] = {
  59.     import cats.implicits._
  60.     userDao.findUser(login) map (_.isDefined)
  61.   }
  62.  
  63.   def generateNonce(login: String): String = {
  64.     val nonce = UUID.randomUUID().toString.replace("-", "")
  65.     login2nonceMap += (login -> nonce)
  66.     nonce
  67.   }
  68.  
  69.   def isTokenValid(token: String): Either[TokenError, Long] = {
  70.     import cats.implicits._
  71.     lazy val regex = """([\da-f]*)-(\d*)-(\d*)""".r
  72.     lazy val isActive = token match {
  73.       case regex(_, nonce, _) if (Platform.currentTime milliseconds) - (nonce.toLong milliseconds) < (tokenLifetime hours) => Right(Unit)
  74.       case _ => Left(TokenExpired)
  75.     }
  76.  
  77.     for {
  78.       userIdStr <- crypto.validateSignedToken(token) toRight InvalidToken
  79.       userId <- Try(userIdStr.toLong).toEither.leftMap(_ => InvalidUserId)
  80.       _ <- isActive
  81.     } yield userId
  82.   }
  83.  
  84.   private def validateUserName(name: String): Either[String, Unit] = {
  85.     Either.cond(name.matches("[\\w.\\-]{4,64}"), Unit, "Incorrect user name. It must be at least 4 characters long and contain latin characters or digits")
  86.   }
  87.  
  88.   private def validatePassword(passwordHex: String): Either[String, Unit] = {
  89.     // recommended conditions for a client:
  90.     // password.length >= 8 && password.matches(".*[A-Z].*") && password.matches(".*[a-z].*") && password.matches(".*[\\d].*")
  91.     Either.cond(passwordHex.matches("[\\da-f]{32,}"), Unit, "Incorrect password. Password must be at least 32 chars long HEX encoded string")
  92.   }
  93.  
  94.   private def validateEmail(email: String): Either[String, Unit] = {
  95.     Either.cond(email.matches(emailRegex), Unit, "Incorrect email address")
  96.   }
  97.  
  98.   private def storeUser(login: String, passwordHex: String, email: String): F[Either[String, String]] = {
  99.     import cats.implicits._
  100.  
  101.     val row = UserRow(None, login, passwordHex, email)
  102.     userDao.saveUser(row) map (_.map(newId => crypto.signToken(newId.toString, Platform.currentTime.toString)))
  103.   }
  104. }
Add Comment
Please, Sign In to add comment