//
you're reading...
User Authentication

How to implement user authentication with restful_authentication and 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
Advertisements

About RailsBlogger

I'm a Software Developer with over 10 years of experience, Java and Ruby on Rails.

Discussion

2 thoughts on “How to implement user authentication with restful_authentication and email activation

  1. Thanks for posting your solution to acts_as_state_machine assigning the activation code twice. I wish I'd found it sooner, but you've definitely saved me a headache figuring out the right way to address it.

    Posted by Jack | August 28, 2010, 11:34 pm
  2. There are some ActionMailer + gmail issues when using Rails >= 2.3.5.

    Don't use angular brackets (“”) in the “from” field! The mailer will add the sender email automatically. If your mailer fails silently, then look for possible syntax errors. At the same time, reply_to is ok with angular brackets.

    Also, there is no need anymore to use the smtp_tsl.rb file to work with gmail. It's enough to install the “tlsmail” gem and require it in the mail configuration file.

    Posted by Anonymous | November 9, 2010, 8:03 pm

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: