How to add an Authlogic hook to record login attempts

Published 19 October 2010

We want to record all login attempts to our application, both successful and unsuccessful, using Authlogic.\r\n\r\nIt turns out this is quite easy. I was originally trying to hook into the Authlogic callback, such as before_persisting, before_create, etc.\r\n\r\nUnfortunately many of these callback are called multiple times on each login, and the _create callbacks are only called on successful logins, so failed logins are not recorded.\r\n\r\nThe solution is to subclass the *Session save method.\r\n\r\nh2. Create a new table\r\n\r\nAssuming you already have a User table with the standard login, crypted_password, password_salt and persistence_token fields, you first create a table for recording login attempts:\r\n\r\n@@@\r\nscript/generate migration AddLoginsTable\r\n@@@\r\n\r\nand add the following content:\r\n\r\n@@@ ruby\r\nclass AddLoginsTable < ActiveRecord::Migration\r\n def self.up\r\n create_table :logins do |t|\r\n t.string :login\r\n t.integer :user_id\r\n t.timestamp :created_at\r\n end\r\n end\r\n\r\n def self.down\r\n drop_table :logins\r\n end\r\nend\r\n@@@\r\n\r\nThis simply acts as a log of login attempts.\r\n\r\nh2. Override save in the UserSession\r\n\r\nUsing standard username/password logins, you should have a UserSession model class, which subclasses Authlogic::Session::Base.\r\n\r\nSimply add the follow save method:\r\n\r\n@@@ ruby\r\nclass UserSession < Authlogic::Session::Base\r\n \r\n def save(&block)\r\n success = super\r\n Login.create({:login => login, :user_id => user ? user.id : nil})\r\n success\r\n end\r\nend\r\n@@@\r\n\r\nHere the username is recorded in the login field, and corresponding user is set in the user_id column.\r\n\r\nIf the login failed (due to invalid password, deactivated user, etc) then this will be null.\r\n\r\nI deliberately don’t capture the password because I don’t want to be storing user password in plain text, even if they are incorrect.\r\n\r\nYou may be wondering, why didn’t I just do this?\r\n\r\n@@@ ruby\r\nLogin.create({:login => login, :user => user}) # this breaks my tests!\r\n@@@\r\n\r\nI tried this and got infinite loops in my tests, where the user object had not yet been persisted to the database. \r\n\r\nThis is because creating a login object with a user object which has not yet been persisted causes the user object to be saved, which in turn calls save on my user session.\r\nAnd this of course results in the loop.\r\n\r\nAny questions? Please leave a comment