backend/app/utils/auth/providers/DatabaseUserProvider.scala (101 lines of code) (raw):

package utils.auth.providers import commands.TFACommands import model.frontend.user.{NewGenesisUser, PartialUser, UserRegistration} import model.frontend.TotpActivation import model.user.{DBUser, NewUser, UserPermissions} import play.api.libs.json.{JsBoolean, JsNumber, JsValue} import play.api.mvc.{AnyContent, Request} import services.users.UserManagement import services.DatabaseAuthConfig import utils.attempt._ import utils.auth.{PasswordHashing, PasswordValidator, RequireNotRegistered, RequireRegistered} import utils.auth.totp.{SecureSecretGenerator, TfaToken, Totp} import utils.{Epoch, Logging} import scala.concurrent.ExecutionContext /** * A UserAuthenticator implementation that authenticates a valid user based on credentials stored in the local database */ class DatabaseUserProvider(val config: DatabaseAuthConfig, passwordHashing: PasswordHashing, users: UserManagement, totp: Totp, ssg: SecureSecretGenerator, passwordValidator: PasswordValidator) (implicit ec: ExecutionContext) extends UserProvider with Logging { override def clientConfig: Map[String, JsValue] = Map( "require2fa" -> JsBoolean(config.require2FA), "minPasswordLength" -> JsNumber(config.minPasswordLength) ) override def authenticate(request: Request[AnyContent], time: Epoch): Attempt[PartialUser] = { for { formData <- request.body.asFormUrlEncoded.toAttempt(Attempt.Left(ClientFailure("No form data"))) username <- formData.get("username").flatMap(_.headOption).toAttempt(Attempt.Left(ClientFailure("No username form parameter"))) password <-formData.get("password").flatMap(_.headOption).toAttempt(Attempt.Left(ClientFailure("No password form parameter"))) tfaCode = formData.get("tfa").flatMap(_.headOption) dbUser <- passwordHashing.verifyUser(users.getUser(username), password, RequireRegistered) _ <- totp.checkUser2fa(config.require2FA, dbUser.totpSecret, tfaCode, time) } yield dbUser.toPartial } override def generate2faToken(username: String, instance: String): Attempt[TfaToken] = { val secret = ssg.createRandomSecret(totp.algorithm).toBase32 val url = s"otpauth://totp/$username?secret=$secret&issuer=${config.totpIssuer}%20($instance)" Attempt.Right(TfaToken(secret, url)) } override def genesisUser(request: JsValue, time: Epoch): Attempt[PartialUser] = { for { userData <- request.validate[NewGenesisUser].toAttempt encryptedPassword <- passwordHashing.hash(userData.password) _ <- passwordValidator.validate(userData.password) secret <- TFACommands.check2FA(config.require2FA, userData.totpActivation, totp, time) // We will immediately register after creating user = DBUser(userData.username, None, None, invalidationTime = None, registered = false, totpSecret = None) created <- users.createUser(user, UserPermissions.bigBoss) registered <- users.registerUser(userData.username, userData.displayName, Some(encryptedPassword), secret) } yield registered.toPartial } override def createUser(username: String, request: JsValue): Attempt[PartialUser] = { for { wholeUser <- request.validate[NewUser].toAttempt _ <- if (username == wholeUser.username) Attempt.Right(()) else Attempt.Left(ClientFailure("Username in URL didn't match that in payload.")) _ <- passwordValidator.validate(wholeUser.password) hash <- passwordHashing.hash(wholeUser.password) user <- users.createUser( DBUser(wholeUser.username, Some("New User"), Some(hash), invalidationTime = None, registered = false, totpSecret = None), UserPermissions.default ) } yield user.toPartial } override def registerUser(request: JsValue, time: Epoch): Attempt[Unit] = { request.validate[UserRegistration].toAttempt.flatMap { userData => logger.info(s"Attempt to register ${userData.username}") for { _ <- passwordHashing.verifyUser(users.getUser(userData.username), userData.previousPassword, RequireNotRegistered) _ <- passwordValidator.validate(userData.newPassword) newHash <- passwordHashing.hash(userData.newPassword) secret <- TFACommands.check2FA(config.require2FA, userData.totpActivation, totp, time) _ <- users.registerUser(userData.username, userData.displayName, Some(newHash), secret) } yield { logger.info(s"Registered ${userData.username}") () } } } override def removeUser(username: String): Attempt[Unit] = { users.removeUser(username) } override def updatePassword(username: String, newPassword: String): Attempt[Unit] = { for { passwordHash <- passwordHashing.hash(newPassword) _ <- passwordValidator.validate(newPassword) _ <- users.updateUserPassword(username, passwordHash) } yield () } override def enrollUser2FA(username: String, totpActivation: TotpActivation, time: Epoch): Attempt[Unit] = { for { // this is deliberately hardcoded to true, we also pass through a Some, rather than None in this call. secret <- TFACommands.check2FA(require2FA = true, Some(totpActivation), totp, time) _ <- users.updateTotpSecret(username, secret) } yield () } override def removeUser2FA(username: String): Attempt[Unit] = { for { _ <- if (config.require2FA) Attempt.Left(SecondFactorRequired("This system requires 2FA so you cannot disable it.")) else Attempt.Right(()) _ <- users.updateTotpSecret(username, None) } yield () } }