From 050a598d6473bc6e567fe205c6089b598e716a6d Mon Sep 17 00:00:00 2001 From: hongwei1 Date: Sat, 31 Dec 2016 00:12:12 +0100 Subject: [PATCH] Add User lockout mechanism #276 --- src/main/scala/bootstrap/liftweb/Boot.scala | 4 +- src/main/scala/code/api/directlogin.scala | 3 ++ src/main/scala/code/api/util/APIUtil.scala | 2 + .../scala/code/bankconnectors/Connector.scala | 5 +++ .../bankconnectors/KafkaMappedConnector.scala | 5 +++ .../code/bankconnectors/LocalConnector.scala | 6 +++ .../bankconnectors/LocalMappedConnector.scala | 32 ++++++++++++++ .../ObpJvmMappedConnector.scala | 6 +++ .../dataAccess/MappedBadLoginAttempts.scala | 29 +++++++++++++ .../scala/code/model/dataAccess/OBPUser.scala | 42 ++++++++++++++++++- .../code/api/v1_3_0/PhysicalCardsTest.scala | 6 +++ 11 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/main/scala/code/model/dataAccess/MappedBadLoginAttempts.scala diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 8a501109c..76de85637 100644 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -440,5 +440,7 @@ object ToSchemify { MappedEntitlement, MappedPhysicalCard, PinReset, - MappedCounterparty) + MappedCounterparty, + MappedBadLoginAttempts + ) } diff --git a/src/main/scala/code/api/directlogin.scala b/src/main/scala/code/api/directlogin.scala index 0a8744c9d..67bb02882 100644 --- a/src/main/scala/code/api/directlogin.scala +++ b/src/main/scala/code/api/directlogin.scala @@ -99,6 +99,9 @@ object DirectLogin extends RestHelper with Loggable { if (userId == 0) { message = ErrorMessages.InvalidLoginCredentials httpCode = 401 + } else if (userId == OBPUser.usernameLockedStateCode) { + message = ErrorMessages.LockedLoginUsername + httpCode = 401 } else { val claims = Map("" -> "") val (token:String, secret:String) = generateTokenAndSecret(claims) diff --git a/src/main/scala/code/api/util/APIUtil.scala b/src/main/scala/code/api/util/APIUtil.scala index bb27a6802..19e1baa17 100644 --- a/src/main/scala/code/api/util/APIUtil.scala +++ b/src/main/scala/code/api/util/APIUtil.scala @@ -88,6 +88,8 @@ object ErrorMessages { val InvalidDirectLoginParameters = "OBP-20012: Invalid direct login parameters" + val LockedLoginUsername = "OBP-20013: The account has been locked, please contact administrator !" + // Resource related messages val BankNotFound = "OBP-30001: Bank not found. Please specify a valid value for BANK_ID." val CustomerNotFound = "OBP-30002: Customer not found. Please specify a valid value for CUSTOMER_NUMBER." diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index ab1f34901..d929628a4 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -760,5 +760,10 @@ trait Connector { List(ownerView, publicView, accountantsView, auditorsView).flatten } + def incrementBadLoginAttempts(username:String):Unit + + def userIsLocked(username:String):Boolean + + def resetBadLoginAttempts(username:String):Unit } diff --git a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala index 3e8d0f377..63774d862 100644 --- a/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/KafkaMappedConnector.scala @@ -1190,6 +1190,11 @@ object KafkaMappedConnector extends Connector with Loggable { return json.parse("""{"error":"could not send message to kafka"}""") } + override def incrementBadLoginAttempts(username: String): Unit = Empty + + override def userIsLocked(username: String): Boolean = false + + override def resetBadLoginAttempts(username: String): Unit = Empty } diff --git a/src/main/scala/code/bankconnectors/LocalConnector.scala b/src/main/scala/code/bankconnectors/LocalConnector.scala index 4890c9f8c..7138eb5f4 100644 --- a/src/main/scala/code/bankconnectors/LocalConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalConnector.scala @@ -618,4 +618,10 @@ private object LocalConnector extends Connector with Loggable { override def createOrUpdateBranch(branch: BranchJsonPost ): Box[Branch] = Empty override def getBranch(bankId : BankId, branchId: BranchId) : Box[MappedBranch]= Empty + + override def incrementBadLoginAttempts(username: String): Unit = Empty + + override def userIsLocked(username: String): Boolean = false + + override def resetBadLoginAttempts(username: String): Unit = Empty } diff --git a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index ddb90843c..e780a6e14 100644 --- a/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -37,6 +37,7 @@ import scala.concurrent.ExecutionContext.Implicits.global object LocalMappedConnector extends Connector with Loggable { type AccountType = MappedBankAccount + val maxBadLoginAttempts = Props.get("max.bad.login.attempts") openOr "10" // Gets current challenge level for transaction request override def getChallengeThreshold(userId: String, accountId: String, transactionRequestType: String, currency: String): (BigDecimal, String) = { @@ -845,4 +846,35 @@ Store one or more transactions ) } + override def incrementBadLoginAttempts(username: String): Unit ={ + + MappedBadLoginAttempts.find(By(MappedBadLoginAttempts.mUsername, username)) match { + case Full(loginAttempt) => + loginAttempt.mLastFailureDate(now).mBadAttemptsSinceLastSuccess(loginAttempt.mBadAttemptsSinceLastSuccess+1).save + case _ => + MappedBadLoginAttempts.create.mUsername(username).mBadAttemptsSinceLastSuccess(0).save() + } + } + + /** + * check the bad login attempts,if it exceed the "max.bad.login.attempts"(in default.props), it return false. + */ + override def userIsLocked(username: String): Boolean = { + + MappedBadLoginAttempts.find(By(MappedBadLoginAttempts.mUsername, username)) match { + case Empty => true //When the username first login in. No records, so it is empty + case loginAttempt if(loginAttempt.get.mBadAttemptsSinceLastSuccess < (maxBadLoginAttempts.toInt-1)) => true + case _ => false + } + } + + override def resetBadLoginAttempts(username: String): Unit = { + + MappedBadLoginAttempts.find(By(MappedBadLoginAttempts.mUsername, username)) match { + case Full(loginAttempt) => + loginAttempt.mLastFailureDate(now).mBadAttemptsSinceLastSuccess(0).save + case _ => + MappedBadLoginAttempts.create.mUsername(username).mBadAttemptsSinceLastSuccess(0).save() + } + } } diff --git a/src/main/scala/code/bankconnectors/ObpJvmMappedConnector.scala b/src/main/scala/code/bankconnectors/ObpJvmMappedConnector.scala index 8330ff3a1..fa10fd400 100644 --- a/src/main/scala/code/bankconnectors/ObpJvmMappedConnector.scala +++ b/src/main/scala/code/bankconnectors/ObpJvmMappedConnector.scala @@ -1209,5 +1209,11 @@ private def saveTransaction(fromAccount: AccountType, toAccount: AccountType, am override def createOrUpdateBranch(branch: BranchJsonPost ): Box[Branch] = Empty override def getBranch(bankId : BankId, branchId: BranchId) : Box[MappedBranch]= Empty + + override def incrementBadLoginAttempts(username: String): Unit = Empty + + override def userIsLocked(username: String): Boolean = false + + override def resetBadLoginAttempts(username: String): Unit = Empty } diff --git a/src/main/scala/code/model/dataAccess/MappedBadLoginAttempts.scala b/src/main/scala/code/model/dataAccess/MappedBadLoginAttempts.scala new file mode 100644 index 000000000..20bd2608b --- /dev/null +++ b/src/main/scala/code/model/dataAccess/MappedBadLoginAttempts.scala @@ -0,0 +1,29 @@ +package code.model.dataAccess + +import java.util.Date + +import net.liftweb.mapper._ + +class MappedBadLoginAttempts extends LoginAttempt with LongKeyedMapper[MappedBadLoginAttempts] with IdPK { + def getSingleton = MappedBadLoginAttempts + + object mUsername extends MappedString(this, 255) + object mBadAttemptsSinceLastSuccess extends MappedInt(this) + object mLastFailureDate extends MappedDateTime(this) + + override def username: String = mUsername.get + + override def badAttemptsSinceLastSuccess: Int = mBadAttemptsSinceLastSuccess.get + + override def lastFailureDate: Date = mLastFailureDate.get +} + +object MappedBadLoginAttempts extends MappedBadLoginAttempts with LongKeyedMetaMapper[MappedBadLoginAttempts] { + override def dbIndexes = UniqueIndex(mUsername) :: super.dbIndexes +} + +trait LoginAttempt { + def username: String + def badAttemptsSinceLastSuccess : Int + def lastFailureDate : Date +} diff --git a/src/main/scala/code/model/dataAccess/OBPUser.scala b/src/main/scala/code/model/dataAccess/OBPUser.scala index 3067419c7..fcdedc788 100644 --- a/src/main/scala/code/model/dataAccess/OBPUser.scala +++ b/src/main/scala/code/model/dataAccess/OBPUser.scala @@ -33,7 +33,7 @@ package code.model.dataAccess import java.util.UUID -import code.api.util.APIUtil +import code.api.util.{APIUtil, ErrorMessages} import code.api.{DirectLogin, OAuthHandshake} import code.bankconnectors.Connector import net.liftweb.common._ @@ -159,6 +159,9 @@ class OBPUser extends MegaProtoUser[OBPUser] with Logger { object OBPUser extends OBPUser with MetaMegaProtoUser[OBPUser]{ import net.liftweb.util.Helpers._ + /**Marking the locked state to show different error message */ + val usernameLockedStateCode = 999999999 + val connector = Props.get("connector").openOrThrowException("no connector set") override def emailFrom = Props.get("mail.users.userinfo.sender.address", "sender-not-set") @@ -357,11 +360,30 @@ import net.liftweb.util.Helpers._ findUserByUsername(name) match { case Full(user) => if (user.validated_? && + // Check whether user is locked or not + Connector.connector.vend.userIsLocked(name) && user.getProvider() == Props.get("hostname","") && user.testPassword(Full(password))) { + // if login in correctly, reset or set the bad login attemps to 0. + Connector.connector.vend.resetBadLoginAttempts(name) Full(user.user) } + //recording the login faild times when password is wrong + else if (user.validated_? && + // Check whether user is locked or not + Connector.connector.vend.userIsLocked(name) && + !user.testPassword(Full(password)) + ) { + Connector.connector.vend.incrementBadLoginAttempts(name) + Empty + } + else if (!Connector.connector.vend.userIsLocked(name) + ) { + info(ErrorMessages.LockedLoginUsername) + S.error(S.?(ErrorMessages.LockedLoginUsername)) + Full(usernameLockedStateCode) + } else { connector match { case "kafka" => @@ -440,12 +462,17 @@ import net.liftweb.util.Helpers._ override def login = { def loginAction = { if (S.post_?) { + val usernameFromGui = S.param("username").getOrElse("") S.param("username"). flatMap(name => findUserByUsername(name)) match { case Full(user) if user.validated_? && // Check if user came from localhost user.getProvider() == Props.get("hostname","") && + // Check whether user is locked or not + Connector.connector.vend.userIsLocked(usernameFromGui) && user.testPassword(S.param("password")) => { + // if login in correctly, reset or set the bad login attemps to 0. + Connector.connector.vend.resetBadLoginAttempts(usernameFromGui) val preLoginState = capturePreLoginState() info("login redir: " + loginRedirect.get) val redir = loginRedirect.get match { @@ -463,6 +490,19 @@ import net.liftweb.util.Helpers._ }) } + // This case is to record the login faild times when password is wrong + case Full(user) if user.validated_? && + // Check whether user is locked or not + Connector.connector.vend.userIsLocked(usernameFromGui) && + !user.testPassword(S.param("password")) =>{ + Connector.connector.vend.incrementBadLoginAttempts(usernameFromGui) + S.error(S.?("passwords.do.not.match")) + } + + // This case is to send the error to GUI, when the username is locked + case Full(user) if !Connector.connector.vend.userIsLocked(usernameFromGui) => + S.error(S.?(ErrorMessages.LockedLoginUsername)) + case Full(user) if !user.validated_? => S.error(S.?("account.validation.error")) diff --git a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala index 17f1866ef..3fba1cc7c 100644 --- a/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala +++ b/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala @@ -179,6 +179,12 @@ class PhysicalCardsTest extends ServerSetup with DefaultUsers with DefaultConne override def getBranch(bankId: BankId, branchId: BranchId): Box[MappedBranch]= Empty override def getCounterpartyByCounterpartyId(counterpartyId: CounterpartyId): Box[CounterpartyTrait] = ??? + + override def incrementBadLoginAttempts(username: String): Unit = Empty + + override def userIsLocked(username: String): Boolean = false + + override def resetBadLoginAttempts(username: String): Unit = Empty } override def beforeAll() {