Tuesday, April 7, 2009

Quick Notes on restful_authentication with email activation

A good step-by-step guide how to install user authentication system with email activation can be found here

There are still a few issues when using those wonderful plugins.

1. Emailing activation code using a gmail account doesn't work explicitly with the current Rails version 2.2.2. In order to make it work, place the following class named smtp_tsl.rb into the lib folder (This is not my code, I found it in some news group. I don't remember where, sorry.)

require "openssl"
require "net/smtp"

class Net::SMTP
class << self
send :remove_method, :start
attr_accessor :use_tls
end

@use_tls = true

def self.start( address, port = nil,
helo = 'localhost.localdomain',
user = nil, secret = nil, authtype = nil, use_tls = ::Net::SMTP.use_tls,
&block) # :yield: smtp
new(address, port).start(helo, user, secret, authtype, use_tls, &block)
end

alias tls_old_start start

def start( helo = 'localhost.localdomain',
user = nil, secret = nil, authtype = nil, use_tls = ::Net::SMTP.use_tls ) # :yield: smtp
start_method = use_tls ? :do_tls_start : :do_start
if block_given?
begin
send start_method, helo, user, secret, authtype
return yield(self)
ensure
do_finish
end
else
send start_method, helo, user, secret, authtype
return self
end
end

private

def do_tls_start(helodomain, user, secret, authtype)
raise IOError, 'SMTP session already started' if @started
check_auth_args user, secret, authtype if user or secret

sock = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
@socket = Net::InternetMessageIO.new(sock)
@socket.read_timeout = 60 #@read_timeout
@socket.debug_output = @debug_output

check_response(critical { recv_response() })
do_helo(helodomain)

raise 'openssl library not installed' unless defined?(OpenSSL)
starttls
ssl = OpenSSL::SSL::SSLSocket.new(sock)
ssl.sync_close = true
ssl.connect
@socket = Net::InternetMessageIO.new(ssl)
@socket.read_timeout = 60 #@read_timeout
@socket.debug_output = @debug_output
do_helo(helodomain)

authenticate user, secret, authtype if user
@started = true
ensure
unless @started
# authentication failed, cancel connection.
@socket.close if not @started and @socket and not @socket.closed?
@socket = nil
end
end

def do_helo(helodomain)
begin
if @esmtp
ehlo helodomain
else
helo helodomain
end
rescue Net::ProtocolError
if @esmtp
@esmtp = false
@error_occured = false
retry
end
raise
end
end

def starttls
getok('STARTTLS')
end

alias tls_old_quit quit

def quit
begin
getok('QUIT')
rescue EOFError
end
end

end unless Net::SMTP.private_method_defined? :do_tls_start or
Net::SMTP.method_defined? :tls?


Now, in order to configure your ActionMailer, create a file email.rb in your config/initializers and place the following code into it:

require 'smtp_tls'

ActionMailer::Base.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
:address => "smtp.gmail.com",
:port => 587,
:domain => "gmail.com",
:authentication => :plain,
:user_name => "<mygmailaccount>@gmail.com",
:password => "<mypassword>"
}
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.raise_delivery_errors = true
ActionMailer::Base.default_charset = "utf-8"
2. acts_as_state_machine assigns the activation code twice. As a result, the activation code saved in the database is not the same as the activation code sent to the user... Logically, the initial state of the state machine should be "passive", not "pending" and then the activation code is assigned once... except the machine doesn't change the state to "pending" for some reason. The hack I found on the internet is to reload the object from the database before sending an email... except, it nukes the object associations. My own hack is provided below, replace make_activation_code function in the model with the following one:

    def make_activation_code
unless self.state == 'pending'
self.deleted_at = nil
self.activation_code = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
end
end


3. I have implemented a couple of functions of my own:
a. resend activation code

This was trivial

b. resend password if forgotten

The user account in this case should be active, i.e. the state machine makes a self-transition from the active state to the same. In order to be able to execute code on transitions, I used the following enhancement of the acts_as_state_machine plugin (big thanks to this guy).

Also, I created 2 extra fields in the model - passreset_code and passreset_at. Once the user requests to resend the password, passreset_code is created and sent to them. Once the user clicks on the link with this code, they're presented with a form to reset the password. Once they update the password, the code is cleared.

In order to implement this, add the following transitions to the model:
  event :resetpassword do
transitions :from => :active, :to => :active, :on_transition => :make_passreset_code
end

event :updatepassword do
transitions :from => :active, :to => :active, :on_transition => :do_update_password
end


and the following protected methods:
    def make_passreset_code
self.passreset_code = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
end

def do_update_password
self.passreset_at = Time.now.utc
self.passreset_code = nil
end