Add User lockout mechanism #276

This commit is contained in:
hongwei1 2016-12-31 00:12:12 +01:00
parent 199214a265
commit 050a598d64
11 changed files with 138 additions and 2 deletions

View File

@ -440,5 +440,7 @@ object ToSchemify {
MappedEntitlement,
MappedPhysicalCard,
PinReset,
MappedCounterparty)
MappedCounterparty,
MappedBadLoginAttempts
)
}

View File

@ -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)

View File

@ -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."

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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"))

View File

@ -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() {