Alex Popov

My personal blog about my experience with Ruby, Rails and many other things

Allow Users to Authenticate With Username Only Using Devise, ActiveAdmin, Rails 4 and Ruby 2

| Comments

Probably there are not many cases where one wouldn't wont their users to have email addresses. Nevertheless, I had exactly this situation recently. It was quite a challenge (for me at least) to figure out all the things one needs to change in Devise, so as not to expect users to provide an email upon registration and sign-in and to work properly. Finally, I was able to set it to work properly and decided to save you the trouble, in case some of you have a similar setup.

Allow normal users to authenticate with username only, while keeping email authentication for admins in ActiveAdmin

Scenario

You are using ActiveAdmin (AA), Devise, Rails 4 and Ruby 2. You have two models/AA resources - AdminUsers (created by default after installing AA) and Users (generated using Devise). You want your users to be able to login only with username and not have the email attribute at all. At the same time you want your admins to be able to log-in in the AA backend via email.

Source

You can view the source on Github.

Conventions

I refer to the files unser app/models/ as models and the files under app/admin/ as resources.

Prerequisits:

  • Generate a new Rails application:
1
rails new devise_username_only
  • Add to Gemfile
1
gem 'activeadmin', github: 'gregbell/active_admin'
  • Install gems:
1
bundle install
  • Install ActiveAdmin:
1
rails g active_admin:install
  • Migrate the database:
1
rake db:migrate

So far you should have a working app with an admin backend, containing an AdminUsers resource. Test it by starting the server with rails s, going to http://localhost:3000/admin and logging in with credentials admin@example.com and password.

Actual work

Now comes the real work of generating your user model and doing a few tweaks.

Devise initializer

Modify the Devise initializer under config/initalizers/devire.rb. In particular, change the following lines to look like this:

1
2
config.case_insensitive_keys = [ :email, :username ]
config.strip_whitespace_keys = [ :email, :username ]

We we will leave the line:

1
# config.authentication_keys = [ :email ]

commented out, as we don't want to change the authentication keys globally. We want our admin users to still be able to log in with email. We will change the authentication keys only within the user model.

Also, uncomment config.scoped_views and set it to true:

1
config.scoped_views = true

We need this because we have more than one Devise model (AdminUsers and Users) and we want to modify the Users views. More info here.

User model

Generate the User model:

1
rails g devise User

In the user migration file under db/migrate/20131031100550_devise_create_admin_users.rb replace :email with :username so that you have the folloing two lines in the file:

1
2
t.string :username, null => false, :default => ""
add_index :users, :username, :unique => true

Tweak the User model:

  • set the desired Devise modules;
  • add the authentication keys option;
  • overwrite email_required? and email_changed?.

Your model should look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :omniauthable,
  # :registerable, :recoverable
  devise :database_authenticatable,
         :rememberable,
         :trackable,
         :validatable,
         :authentication_keys => [:username]

  def email_required?
    false
  end

  def email_changed?
    false
  end
end

I have not included :confirmable, :registerable, and :recoverable as I want all my user management to happen on the backend through ActiveAdmin. What's more, as the User model does not have the email attribute, these modules won't work anyway.

Finally, run the migration:

1
rake db:migrate

User resource

Register the User model as a resource in ActiveAdmin:

1
rails g active_admin:resource User

Modify the User resource to look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ActiveAdmin.register User do
  # This determines which attributes of the User model will be
  # displayed in the index page. I have left only username, but
  # feel free to uncomment the rest of the fields or add any
  # other of the User attributes.
  index do
    column :username
    # column :current_sign_in_at
    # column :last_sign_in_at
    # column :sign_in_count
    default_actions
  end

  # Default is :email, but we need to replace this with :username
  filter :username

  # This is the form for creating a new user using the Admin
  # backend. If you have added additional attributes to the
  # User model, you need to include them here.
  form do |f|
    f.inputs "User Details" do
      f.input :username
      f.input :password
      f.input :password_confirmation
    end
    f.actions
  end

  # This is related to Rails 4 and how it handles strong parameters.
  # Here we replace :email with :username.
  controller do
    def permitted_params
      params.permit user: [:username, :password, :password_confirmation]
    end
  end
end

Devise views

The Devise views need to be modified in order to reflect that the user is using their username and not their email. First, generate them:

1
rails g devise:views

Since I have not included the :confirmable, :registerable, and :recoverable modules, the only relevent view is app/views/devise/sessions/new.html.erb. You only need to change these lines:

1
2
<div><%= f.label :username %><br />
<%= f.text_field :username, :autofocus => true %></div>

Note that f.email_field needs to be changed to f.text_field, otherwise in newer browsers the built-in validation won't pass and you'll get error when you enter a username in that field.

To test your work:

Bear in mind that if you have not created a default home page containing a sign out link, you won't be able to log out by just entering http://localhost:3000/users/sign_out by default, as the sign-out route uses the :delete HTTP method. As a temporary workaround, in the Devise initializer set config.sign_out_via to :get and in routes.rb change devise_for :users to

1
devise_for :users do get '/users/sign_out' => 'devise/sessions#destroy' end

I hope I have saved you some time.

Comments