Smallthoughts

Thoughts on Smalltalk

Managing failed login attempts

Similar to the login filter, if you supply a two-argument block to TLLoginComponent>>#onLoginFail:, it will be evaluated when a user login attempt fails. The arguments passed to the block are the username and remote address of the failed login attempt. The block should evaluate to nil or text to be displayed to the user inplace of the standard authorization error message (TLLoginComponent>>#authenticationErrorText).

This can be used, together with application properties and a login filter to implement failed login policies such as disabling accounts after too many failed login attempts or ignoring logins from remote addresses that have been repeatedly attempting to login using many usernames.

Here is the code from TLTestApp that implements a maximum failed login attempts policy:

In TLTestApp initialize:

    loginComponent onLogin: [ :user | self loginFilter: user ].
loginComponent onLoginFail: [ :username :address |
self loginFailedUser: username from: address ].

TLTestApp methods:

loginFailedUser: username from: address
"After the configured number of consecutive
failed login attempts, disable the account
for 2 minutes. Answer nil or text to be displayed
in place of the normal authorization failure message."


^ (loginComponent authenticationManager
usernameExists: username)
ifTrue: [
| failedUser failureCount |
failedUser := loginComponent authenticationManager
userForUsername: username.
failureCount := failedUser applicationProperties
at: 'failedLoginAttempts' ifAbsent: [ 0 ].
failureCount < self maximumFailedLoginAttempts
ifTrue: [
failedUser applicationProperties
at: 'failedLoginAttempts'
put: (failureCount + 1).
loginComponent saveUser: failedUser.
nil ]
ifFalse: [
failedUser applicationProperties
at: 'disabledForLoginFailures'
put: (DateAndTime current) + 2 minutes.
loginComponent saveUser: failedUser.
self disabledForLoginFailuresText ]]
ifFalse: [ nil ]

loginFilter: user
"Return nil to allow login, or text to be displayed to the user if login is disallowed.
First we check for disable from login failures, then for user requested disable."


^ (self loginFilterDisableForLoginFailures: user) ifNil: [self loginFilterDisabledByUser: user ]

loginFilterDisableForLoginFailures: user
"If logins have been disabled for too many login failures, return text to present to user,
otherwise nil to allow login.

Note that we use the same message here as is used in #loginFailedUser:from: so that
there is no indication to the user that this time the correct password was actually entered."


| until |
^ (user applicationProperties includesKey: 'disabledForLoginFailures')
ifTrue: [
until := user applicationProperties at: 'disabledForLoginFailures'.
until < DateAndTime current
ifTrue: [
user applicationProperties removeKey: 'disabledForLoginFailures'.
nil ]
ifFalse: [
self disabledForLoginFailuresText ]]
ifFalse: [ nil ]

loggedIn: user
"sent from onAnswer in self initialize."

user applicationProperties removeKey: 'failedLoginAttempts' ifAbsent: [].