· 6 years ago · Sep 03, 2019, 06:56 PM
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
5import java.security.MessageDigest
6import java.util.UUID
7import cats.Applicative
8import com.typesafe.config.ConfigFactory
9import scala.collection.mutable
10import scala.compat.Platform
11import scala.concurrent.duration._
12import scala.util.Try
13
14object 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
21class 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}