Welcome to another installment in my Let’s build with Ruby on Rails: Extending Devise series. This tutorial teaches you how to sign in with Twitter using Devise, Omniauth, and Ruby on Rails.
Getting started
If you would like to use my Kickoff Tailwind template you can download it here
Create a new app
$ rails new devise_sign_in_with_twitter -m kickoff_tailwind/template.rb
The command above assumes you have downloaded the kickoff_tailwind repo and have the folder name kickoff_tailwind
in the same directory as the app you are creating.
We’ll reference these gems in this guide. You may already have Devise installed if you used my kickoff_tailwind template.
Gemfile:
gem 'devise'
gem 'omniauth'
gem 'omniauth-twitter'
Migrations
Generate a new migration for the user model. It will have two fields called provider
and uid
. Both are strings.
rails g migration AddOmniauthToUsers provider:string uid:string
rails db:migrate
== 20191005160517 AddOmniauthToUsers: migrating ===============================
-- add_column(:users, :provider, :string)
-> 0.0028s
-- add_column(:users, :uid, :string)
-> 0.0007s
== 20191005160517 AddOmniauthToUsers: migrated (0.0037s) ======================
Devise configuration
We need to create a new application on https://developer.twitter.com. This process isn’t 100% straight-forward but is possible. There’s an application step you will need to go through.
To get the API keys shown below there is a process for Twitter. You need to go through an application phase but once done there should be a Keys and Tokens
tab within your Twitter developer app.
Once you have your keys you can add them to your Application Credentials. Run the following:
$ rails credentials:edit
If you get some feedback about what editor you would like to use, you’ll need to re-run the script with a editor of your choice. I prefer Visual Studio code.
No $EDITOR to open file in. Assign one like this:
EDITOR="mate --wait" bin/rails credentials:edit
For editors that fork and exit immediately, it's important to pass a wait flag,
otherwise the credentials will be saved immediately with no chance to edit.
EDITOR="code --wait" bin/rails credentials:edit
We need to add a new configuration to our devise configuration next. Using the credentials we just added we pass those through a special ominauth twitter configuration.
# app/config/initializers/devise.rb
Devise.setup do |config|
...
config.omniauth :twitter, Rails.application.credentials.fetch(:twitter_api_public), Rails.application.credentials.fetch(:twitter_api_secret)
...
end
Make User model omniauthable
We need to modify our existing Devise install to account for the new omniauth logic. Doing so happens in the User model. You can provide multiple providers depending on your app. For the sake of brevity, I’m sticking to Twitter for now.
# app/models/user.rb
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:omniauthable, omniauth_providers: %i[twitter] # add these two!
end
With this added, Devise will create methods for us for each provider we pass. It does not create the _url
methods like other link helpers typically do.
user_{provider}_omniauth_authorize_path
user_{provider}_omniauth_callback_path
OmniAuth routing
With the model in place, our routing needs some attention. We need to tap into the Devise omniauth callbacks controller to do some logic there.
Change the routes file to the following:
# config/routes.rb
require 'sidekiq/web'
Rails.application.routes.draw do
authenticate :user, lambda { |u| u.admin? } do
mount Sidekiq::Web => '/sidekiq'
end
devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
root to: 'home#index'
end
_Note: this guide assumes you are using my kickoff_tailwind
template. Your routes file may differ from the above if not.
OmniAuth Controllers
We are passing the path in which the OmniAuth Callbacks controller exists in our routes.rb
file. Let’s create that folder and file next inside app/controllers/users
# app/controllers/users/omniauth_callbacks_controller
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def twitter
# You need to implement the method below in your model (e.g. app/models/user.rb)
@user = User.from_omniauth(request.env["omniauth.auth"])
if @user.persisted?
sign_in_and_redirect @user, event: :authentication #this will throw if @user is not activated
set_flash_message(:notice, :success, kind: "Twitter") if is_navigational_format?
else
session["devise.twitter_data"] = request.env["omniauth.auth"].except("extra")
redirect_to new_user_registration_url
end
end
def failure
redirect_to root_path
end
end
More logic in the user model
There’s a method called from_omniauth
we haven’t defined yet but referenced in the controller. We’ll add this method to the user model:
# app/models/user.rb
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:omniauthable, omniauth_providers: %i[twitter]
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
user.email = auth.info.email
user.password = Devise.friendly_token[0, 20]
user.name = auth.info.name # assuming the user model has a name
user.username = auth.info.nickname # assuming the user model has a username
#user.image = auth.info.image # assuming the user model has an image
# If you are using confirmable and the provider(s) you use validate emails,
# uncomment the line below to skip the confirmation emails.
# user.skip_confirmation!
end
end
end
The method above will try to find any existing user or create on if the user doesn’t exist yet. While doing so we pass in information such as email
, name
, and an automatic password made possible by Devise. Here you can also pass and image
if you have an avatar of some type on your user model already. For this guide, I will skip that step.
Adding a “Sign in with Twitter” link
If you’re using my kickoff_tailwind application template you should already have a view for the login screen. We’ll make a small change to add a new “Sign in with Twitter” button. Devise ads this dynamically for us but I wanted a more refined style.
<!-- app/views/devise/sessions/new.html.erb -->
<% content_for :devise_form do %>
<h2 class="heading text-4xl font-bold pt-4 mb-8">Log in</h2>
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<div class="mb-6">
<%= f.label :email, class:"label" %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "input" %>
</div>
<div class="mb-6">
<%= f.label :password, class:"label" %>
<%= f.password_field :password, autocomplete: "current-password", class: "input" %>
</div>
<div class="mb-6 flex">
<% if devise_mapping.rememberable? -%>
<%= f.check_box :remember_me, class:"mr-2 leading-tight" %>
<%= f.label :remember_me, class:"block text-grey-darker" %>
<% end -%>
</div>
<div class="mb-6">
<%= f.submit "Log in", class: "btn btn-default" %>
</div>
<div class="mb-6">
<%= link_to "Sign in with Twitter", user_twitter_omniauth_authorize_path, class: "btn bg-blue-500 text-white w-full block text-center" %>
</div>
<% end %>
<hr class="border mt-6" />
<%= render "devise/shared/links" %>
<% end %>
<%= render "devise/shared/form_wrap" %>
All that has changed here is the addition of this line:
<div class="mb-6">
<%= link_to "Sign in with Twitter", user_twitter_omniauth_authorize_path, class: "btn bg-blue-500 text-white w-full block text-center" %>
</div>
And we are left with some quick design like so:
Clicking this should hopefully get us what we are after. But instead I get a OAuth::Unauthorized
message. After some googling I found out that we need to have a series of callback urls defined in your Twitter developer account.
The URL schemes below worked for me.
Now we can try clicking “Sign in with Twitter” again.
Success! Now we can authorize the app and hopefully be redirected back to our demo Rails application.
But now I get a new error that has to do with our session size. This is a warning when your Cookie size is over the 4K limit. To get around this we can harness a gem called ‘activerecord-session_store’
Add that to your gemfile
gem 'activerecord-session_store'
and run bundle install
Then we need a new migration:
rails generate active_record:session_migration
rake db:migrate
Running via Spring preloader in process 6189
create db/migrate/20191005165228_add_sessions_table.rb
Running via Spring preloader in process 6203
== 20191005165228 AddSessionsTable: migrating =================================
-- create_table(:sessions)
-> 0.0022s
-- add_index(:sessions, :session_id, {:unique=>true})
-> 0.0015s
-- add_index(:sessions, :updated_at)
-> 0.0018s
== 20191005165228 AddSessionsTable: migrated (0.0056s) ========================
Finally we need to add a new session_store.rb
file to our config/initializers
folder:
# config/initializers/session_store.rb
Rails.application.config.session_store :active_record_store, :key => '_my_app_session'
Inside you can copy and paste this one-liner. Feel free to name your key something more descriptive.
Be sure to restart your server if you haven’t.
Try logging in again. This time we don’t get errors! But what we do get it redirected to the Sign Up view.
This is issue most likely do to the current null: false
constraint on the users table and email column:
# db/schema.rb
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
...
We need to swap that to true. Let’s do so with a new migration:
rails g migration changeEmailOnUsers
Inside that file add the following:
class ChangeEmailOnUsers < ActiveRecord::Migration[6.0]
def change
change_column :users, :email, :string, null: true
end
end
Then run:
rails db:migrate
Restart your server and try to “Sign in with Twitter” again. Your schema.rb
file should remove the null:
method entirely if all goes correctly.
A gotcha (with Twitter)
If you try logging in currently you will be redirected to localhost:3000/users/sign_up
. This is because there’s some behind the scenes validation going on with the Devise gem. Sadly, no errors or logs tell us what has happened.
I searched for some answers here and found out that. Twitter doesn’t exactly require an email to create an account via API. Our Devise configuration requires an e-mail to be present. To override this (for now) you can add this method in the user model.
# app/models/user.rb
class User < ApplicationRecord
...
def email_required?
false
end
end
This tells Devise to make emails not required for authentication.
Logging in now should work
Sweeet! It works.
Wrapping up
My implementation here is very rigid but gets the job done. If you want to allow users to sign in with multiple providers it might make sense to automate some of the methods in the controller and model layer. You could even go pretty deep with configuration layer automation so a user enters their API keys and preferred providers and the rest is automatic. We did something similar on Jumpstart
Coming up I’ll introduce new providers and extend the Devise gem even more. Stay tuned!
Shameless plug time
I have a new course called Hello Rails. Hello Rails is a modern course designed to help you start using and understanding Ruby on Rails fast. If you’re a novice when it comes to Ruby or Ruby on Rails I invite you to check out the site. The course will be much like these builds but a super more in-depth version with more realistic goals and deliverables. Download your copy today!!
Follow @hello_rails and myself @justalever on Twitter.
The post How to Sign in with Twitter using Devise, Omniauth, and Ruby on Rails appeared first on Web-Crunch.
Top comments (0)