Omniauth without Devise
Previously I’ve covered Github omniauth with Devise, and only github omniauth with Devise (without email-based registration).
An even simpler solution would be to sign in via a social login provider without Devise at all! Here’s the easiest way to do it.
Before at superails.com was using devise and omniauth, but for simplicity (I do not want to manage user passwords, confirm accounts via email) I decided to remove devise and keep only oAuth login!
This is how I moved to oAuth-only login.
First, add the gem omniauth
gems:
# Gemfile
gem 'omniauth-github', github: 'omniauth/omniauth-github', branch: 'master'
gem 'omniauth-google-oauth2'
gem "omniauth-rails_csrf_protection", "~> 1.0"
If you are using Github omniauth, you can generate API credentials here
For development environment you can use
Homepage URL:
http://localhost:3000
Authorization callback URLs
http://localhost:3000/auth/github/callback
http://localhost:3000/auth/google_oauth2/callback
Add your social provider API credentials to the Rails app:
# echo > config/initializers/omniauth.rb
# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :github, "GITHUB_ID", "GITHUB_SECRET"
provider :google_oauth2, "GOOGLE_ID", "GOOGLE_SECRET"
end
Create a user model. We will also add a few static pages:
-
/landing_page
that can be accessed without authentication -
/dashboard
that requires authentication
rails g controller static_pages landing_page dashboard
rails g model User provider uid name email image
Routes:
# config/routes.rb
root 'static_pages#landing_page'
get 'dashboard', to: 'static_pages#dashboard'
get 'auth/github/callback', to: 'sessions#create'
get 'auth/google_oauth2/callback', to: 'sessions#create'
get 'auth/failure', to: 'sessions#failure'
delete 'sign_out', to: 'sessions#destroy'
# get 'login', to: redirect('/auth/github'), as: 'login'
Gems like devise provide some default methods, that we will have to add on our own now:
-
def current_user
- get the current user from session params. -
def user_signed_in?
- check if there is a current user. -
def authenticate_user!
- to restrict controller actions for non-authenticated users. -
helper_method :current_user
- to makecurrent_user
available in views. -
helper_method :user_signed_in
- to makeuser_signed_in
available in views.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :current_user
helper_method :user_signed_in?
def authenticate_user!
redirect_to root_path, alert: "Requires authentication" unless user_signed_in?
end
def current_user
@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
end
def user_signed_in?
# converts current_user to a boolean by negating the negation
!!current_user
end
end
The button to /auth/github
will redirect to the github login page.
# app/views/layouts/application.html.erb
<%= link_to 'Home', root_path %>
<% if current_user %>
<%= current_user.email %>
<%= link_to 'Dashboard', dashboard_path %>
<%= button_to 'Logout', sign_out_path, method: :delete, data: { turbo: false } %>
<% else %>
<%= button_to "Sign in with Github", "/auth/github", data: { turbo: false } %>
<%= button_to "Sign in with Google", "/auth/google_oauth2", data: { turbo: false } %>
<% end %>
After successful authentication, the user should be redirected to sessions#create
with request.env['omniauth.auth']
.
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def create
@user = User.from_omniauth(request.env['omniauth.auth'])
if @user.persisted?
session[:user_id] = @user.id
redirect_path = request.env['omniauth.origin'] || dashboard_path
redirect_to redirect_path, notice: "Logged in as #{@user.name}"
else
redirect_to root_url, alert: "Failure"
end
end
def destroy
session[:user_id] = nil
redirect_to root_path, notice: "Logged out"
end
def failure
redirect_to root_path, alert: "Failure"
end
end
from_omniauth
will find the users email
and uid
in the data provided by github/google, and find or create the user.
# app/models/user.rb
class User < ApplicationRecord
validates :provider, presence: true
validates :uid, presence: true, uniqueness: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, presence: true, uniqueness: true
def self.from_omniauth(omniauth_params)
# TODO: what if 2 providers have same email?
# TODO: what if email is not present in the omniauth payload?
provider = omniauth_params.provider
uid = omniauth_params.uid
user = User.find_or_initialize_by(provider:, uid:)
user.email = omniauth_params.info.email
user.name = omniauth_params.info.name
user.image = omniauth_params.info.image
user.save
user
end
end
Finally, require authentication to visit /dashboard
:
# app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
before_action :authenticate_user!, only: %i[dashboard]
def landing_page
end
def dashboard
end
end
Constraints #
Instead of using a before_action :authenticate_user!
on a controller level, you can authorize user on the route level even before the controller gets touched!
Gems like devise offer helpers like authenticate :user
that is available inside routes out of the box.
In this case we will have to build our own route constraint that has access to request.session
:
# app/constraints/user_constraint.rb
class UserConstraint
def initialize(&block)
@block = block
end
def matches?(request)
user = current_user(request)
user.present? && @block.call(user)
end
def current_user(request)
User.find_by(id: request.session[:user_id])
end
end
Now we can wrap routes that require authenticated user, or admin user in this constraint:
# config/routes.rb
# authenticate :user, ->(user) { user.admin? } do # <- devise syntax
constraints UserConstraint.new { |user| user.admin? } do
mount GoodJob::Engine, at: "good_job"
mount Avo::Engine, at: Avo.configuration.root_path
end
# authenticated :user do # <- devise syntax
constraints UserConstraint.new { |user| user.present? } do
get 'dashboard', to: 'static#dashboard'
end
Nice!
Tests #
bundle add faker
- add Gem Faker to mock Omniauth hashes like Faker::Omniauth.github
, Faker::Omniauth.google
.
Add some helper methods to log in as a random user, or as a defined user:
# test/test_helper.rb
# log in as a user from the database
def sign_in(user)
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new(
provider: user.provider,
uid: user.uid,
info: {
email: user.email
}
)
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:github]
post '/auth/github'
follow_redirect!
end
# random github user
def login_with_github
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new(Faker::Omniauth.github)
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:github]
post '/auth/github'
follow_redirect!
end
# random google user
def login_with_google_oauth2
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new(Faker::Omniauth.google)
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:google]
post '/auth/google_oauth2'
follow_redirect!
end
Write a lot of tests for different scenarios:
# test/controllers/sessions_controller_test.rb
require 'test_helper'
class SessionsControllerTest < ActionDispatch::IntegrationTest
setup do
# without this omniauth.origin can be set to the one in previous test (tags_url) resulting in flaky tests
OmniAuth.config.before_callback_phase do |env|
env['omniauth.origin'] = nil
end
end
test 'authenticated github user should get dashboard' do
assert_raises(ActionController::RoutingError) do
get dashboard_url
end
# get dashboard_url
# assert_response :redirect
# assert_redirected_to root_url
# assert_equal 'Requires authentication', flash[:alert]
login_with_github
get dashboard_url
assert_response :success
delete sign_out_path
assert_response :redirect
assert_redirected_to root_url
assert_equal 'Signed out', flash[:notice]
end
test 'google auth success' do
login_with_google_oauth2
assert_response :redirect
assert_redirected_to dashboard_url
assert_match 'Logged in as', flash[:notice]
assert_equal controller.current_user, User.last
assert User.pluck(:email).include?(OmniAuth.config.mock_auth[:google_oauth2][:info][:email])
end
test 'github oauth success' do
login_with_github
assert_response :redirect
assert_redirected_to dashboard_url
email = OmniAuth.config.mock_auth[:github][:info][:email]
name = OmniAuth.config.mock_auth[:github][:info][:name]
assert_equal "Logged in as #{name}", flash[:notice]
assert User.pluck(:email).include?(email)
assert_equal controller.current_user.email, email
end
test 'github oauth failure' do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:github] = :invalid_credentials
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:github]
get '/auth/github/callback'
follow_redirect!
assert_response :redirect
assert_redirected_to root_path
assert_equal 'Failure. Please try again', flash[:alert]
assert_nil controller.current_user
end
test 'github auth failure with no email' do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new(
provider: 'github',
uid: '123545',
info: {
nickname: 'test',
name: 'test',
email: nil,
image: 'https://avatars.githubusercontent.com/u/123545?v=3'
}
)
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:github]
post '/auth/github'
follow_redirect!
assert_response :redirect
assert_redirected_to root_url
assert_equal 'Failure. Please try again', flash[:alert]
end
test 'redirect to previous page after login' do
OmniAuth.config.before_callback_phase do |env|
env['omniauth.origin'] = tags_url
end
login_with_github
assert_response :redirect
assert_redirected_to tags_url
end
test 'redirect to dashboard when origin is blank' do
OmniAuth.config.before_callback_phase do |env|
env['omniauth.origin'] = nil
end
login_with_github
assert_response :redirect
assert_redirected_to dashboard_url
end
end
System tests #
Add a helper to log in as a user via system tests:
# test/application_system_test_case.rb
def login_with_oauth(user)
OmniAuth.config.test_mode = true
OmniAuth.config.add_mock(:github, {
provider: user.provider,
uid: user.uid,
info: { email: user.email }
})
visit '/auth/github/callback'
end
Add a browser test for being able to access the dashboard only as a logged in user:
require 'application_system_test_case'
class StaticTest < ApplicationSystemTestCase
test 'landing_page' do
visit root_url
assert_current_path root_path
end
test 'dashboard' do
user = users(:one)
login_with_oauth(user)
visit dashboard_url
assert_current_path dashboard_path
end
end
AVO authentication #
# config/initializers/avo.rb
config.current_user_method do
User.find_by(id: session[:user_id]) if session[:user_id]
end
That’s it! Now you can use omniauth without devise! FREEDOM! 🕊️
Did you like this article? Did it save you some time?