Skip to content

Example: Adding Remember Me option

Amadeus Folego edited this page Apr 27, 2015 · 4 revisions

Assuming you are already familiar with Adding authentication this guide will show how to implement a remember-me functionality, i.e.: persisting the user credentials even if the session is already over.

If you just wanna grab the code, check the warden-remember-me branch or see commit: 01f8fef.

Setting up the Model

Let's create a remember_me_token field on the users table:

# db/migrations/3_add_remember_me_token_to_users.rb

Sequel.migration do
  change do
    alter_table(:users) do
      add_column :remember_me_token, String

      add_index :remember_me_token
    end
  end
end

And create a method to regenerate the token for the user, when required:

# models/user.rb
require 'securerandom'

#... class User < ...
  def remember_me!
    token = SecureRandom.urlsafe_base64

    until User.where(remember_me_token: token).empty? do
      token = SecureRandom.urlsafe_base64
    end

    update remember_me_token: token
  end
#...

Markup

Make sure that the remember_me param is being passed:

diff --git a/views/user_sessions/new.html.erb b/views/user_sessions/new.html.erb
index 86574ca..65682e4 100644
--- a/views/user_sessions/new.html.erb
+++ b/views/user_sessions/new.html.erb
@@ -38,7 +38,7 @@
         <input type="password" name="password" class="form-control" placeholder="Password" required>
         <div class="checkbox">
           <label>
-            <input type="checkbox" value="remember-me"> Remember me
+            <input type="checkbox" name="remember_me"> Remember me
           </label>
         </div>
         <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>

Configure Warden

Let's create a strategy for authentication via remember_me_token:

# config/warden.rb

#...
Warden::Strategies.add(:remember_me_token) do
  def valid?
    request.cookies['remember_me_token']
  end

  def authenticate!
    if token = request.cookies['remember_me_token']
      if user = User.first(remember_me_token: token)
        success!(user)
      end
    end
  end
end

And also configure the middleware to use this strategy:

# ./yogurt.rb

   use Warden::Manager do |manager|
     manager.scope_defaults :default,
-      strategies: [:password],
+      strategies: [:password, :remember_me_token],
       action: 'user_sessions/unauthenticated'
     manager.failure_app = self
   end

Setting the cookie token

If the user authenticated successfully we'll check if he sent the remember_me param. If that's the case we'll generate a token and store on the cookies.

We don't have access to the response cookies on the context that we'll perform this check, so we'll have to pull in Rack::Cookies.

Add gem 'rack-contrib' to your Gemfile and bundle. Plug Rack::Cookies to the application:

# ./yogurt.rb
 require 'roda'
+require 'rack/contrib'
 require './config/warden'

 class Yogurt < Roda
+  use Rack::Cookies
   use Rack::Session::Cookie, secret: ENV['SECRET']
   use Rack::MethodOverride

Finally, we can hook onto the authentication succeeded and before logout events to setup and teardown the token:

# config/warden.rb
#...
Warden::Manager.after_authentication do |user, auth, opts|
  if auth.params['remember_me']
    user.remember_me!

    auth.env['rack.cookies']['remember_me_token'] = user.remember_me_token
  end
end

Warden::Manager.before_logout do |user, auth, opts|
  user.update remember_me_token: nil
end

A small caveat

For some reason you'll notice you won't be able to log out, the issue is that before_logout is not being triggered even though we called env['warden'].logout. This is a documented issue.

This can be easily overcomed, by calling #authenticated? before logging out:

diff --git a/routes/user_sessions.rb b/routes/user_sessions.rb
index aef5b56..046e229 100644
--- a/routes/user_sessions.rb
+++ b/routes/user_sessions.rb
@@ -10,7 +10,7 @@ class Yogurt
       end

       r.delete do
-        env['warden'].logout
+        env['warden'].logout if env['warden'].authenticated?

         flash[:success] = "You are logged out"

That's it.