Back to home

Devise Part 5: OAuth With GitHub And OmniAuth main image

Devise Part 5: OAuth With GitHub And OmniAuth

Moving on into our series with Devise, the next step for us will be to set up OAuth login with Github.

Source code can be found here

Prerequisites

Getting started

We will be working from the source code found here.

# Clone and change into the project $ git clone https://github.com/okeeffed/demo-rails-7-with-devise-series $ cd demo-rails-7-with-devise-series # Continue from part 4 - warning that Redis required $ git checkout 4-seperate-frontends # Add required Gems $ bundler add dotenv-rails --group "development,test" $ bundler add omniauth-github $ bundler add omniauth-rails_csrf_protection

Setup for GitHub app

It is a prerequisite that you have a GitHub app setup for this. There are plenty of posts/guides out there on doing this, so I will be sharing too much detail.

Head to your GitHub developer settings and create a new OAuth app.

After you have created it, make sure to copy down the Client ID and Client secret. We will need to add that to our Rails app environment.

Afterwards, around line 274 of the config/initializers/devise.rb file, we can uncomment the line config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' and update it to config.omniauth :github, ENV.fetch('GITHUB_APP_ID'), ENV.fetch('GITHUB_APP_SECRET'), scope: 'user:email'.

# ... omitted Devise.setup do |config| # ... rest omitted # ==> OmniAuth # Add a new OmniAuth provider. Check the wiki for more information on setting # up on your models and hooks. # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' config.omniauth :github, ENV.fetch('GITHUB_APP_ID'), ENV.fetch('GITHUB_APP_SECRET'), scope: 'user:email' # ... rest omitted end

Inside of a .env file, add the required env vars with the following:

GITHUB_APP_ID=<your_client_id> GITHUB_APP_SECRET=<your_client_secret>

Make sure to add .env to your .gitignore file.

At this point, we need to configure Rails to load in the env vars in development.

Setting up dotenv-rails

We need to update the config/environments/development.rb to make use of dotenv-rails:

Add in Dotenv::Railtie.load to config/environments/development.rb after the require statements.

Updating the User record

We need to configure our User model to make use of the Omniauth capability.

Update the app/models/user.rb class:

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[github] 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] end end end

The from_omniauth method that we added will be called from our callbacks controller.

Running the migrations for OmniAuth

Now that we have configured the User model, then next step is to generate a migration to map the provider and user ID.

# Adding in the OmniAuth migration # @see https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview $ bin/rails g migration AddOmniauthToUsers provider:string uid:string $ bin/rails db:migrate

Setting up our callback controller

At this point, we need to create a new controller. We will do this one manually.

# Create the users folder and add the controller file $ mkdir app/controllers/users $ touch app/controllers/users/omniauth_callbacks_controller.rb

Under app/controllers/users/omniauth_callbacks_controller.rb we add the following:

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController # See https://github.com/omniauth/omniauth/wiki/FAQ#rails-session-is-clobbered-after-callback-on-developer-strategy skip_before_action :verify_authenticity_token, only: :github def github # 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: "Github") if is_navigational_format? else session["devise.github_data"] = request.env["omniauth.auth"].except(:extra) # Removing extra as it can overflow some session stores redirect_to new_user_registration_url end end def failure redirect_to root_path end end

The github method is invoked when the user logs in with GitHub.

Updating our router

We need to ensure that omniauth_callbacks has the controller configured for Devise.

We can do this in config/routes.rb by updating the line devise_for :users to devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }.

# config/routes.rb Rails.application.routes.draw do get 'session/index' devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' } # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html resources :session, only: [:index] resources :home, only: %i[index create] # Defines the root path route ("/") root 'home#index' end

Let's add in an un-styled link for logging in with GitHub at app/views/devise/sessions/new.html.erb to the bottom of the file.

That file should now look like this:

<div class="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"> <div class="max-w-md w-full space-y-8"> <div> <img class="mx-auto h-12 w-auto" src="https://tailwindui.com/img/logos/workflow-mark-indigo-600.svg" alt="Workflow"> <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">Sign in to your account</h2> <%- if devise_mapping.registerable? && controller_name != 'registrations' %> <p class="mt-2 text-center text-sm text-gray-600"> Or <%= link_to "sign up", new_registration_path(resource_name), class: "font-medium text-indigo-600 hover:text-indigo-500" %> </p> <% end %> </div> <%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { data: { turbo: false} }) do |f| %> <div class="mt-8 space-y-6"> <input type="hidden" name="remember" value="true"> <div class="rounded-md shadow-sm -space-y-px"> <div> <%= f.label :email, class: "sr-only", for: "email-address" %> <%= f.email_field :email, id: "email-address", autofocus: true, autocomplete: "email", placeholder: "Email address", class: "appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" %> </div> <div> <%= f.label :password, for: "password", class: "sr-only" %> <%= f.password_field :password, id: "password", autocomplete: "current-password", placeholder: "Password", class: "appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" %> </div> </div> <% if devise_mapping.rememberable? %> <div class="flex items-center justify-between"> <div class="field flex items-center"> <%= f.check_box :remember_me, class: "h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" %> <%= f.label :remember_me, class: "ml-2 block text-sm text-gray-900" %> </div> <div class="text-sm"> <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> <%= link_to "Forgot your password?", new_password_path(resource_name), class: "font-medium text-indigo-600 hover:text-indigo-500" %> <% end %> </div> </div> <% end %> <div class="actions"> <%= f.submit "Log in", class: "actions group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %> </div> </div> <% end %> <%# ADD HERE %> <%- if devise_mapping.omniauthable? %> <%- resource_class.omniauth_providers.each do |provider| %> <%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post, "data-turbo": false %> <% end %> <% end %> <%# END ADD HERE %> </div> </div>

The if devise_mapping.omniauthable? block is what was added as a helper.

Logging in with GitHub

At this stage, we can run bin/dev to get the server going to test out login.

Head to localhost:3000/users/sign_in (or logout if you are currently signed in). At the bottom, you will now see a "Sign in with GitHub" option.

Sign in with GitHub option

Sign in with GitHub option

Click on it and it should take you through the GitHub authorization flow.

Once completed, you will be logged in.

If you run the Rails console with bin/rails c, you can check your users to see what was added:

irb(main):001:0> User.last (1.4ms) SELECT sqlite_version(*) User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]] => #<User id: 3, email: "[REDACTED]", created_at: "2022-03-07 23:05:24.557261000 +0000", updated_at: "2022-03-07 23:05:24.557261000 +0000", provider: "github", uid: "[REDACTED]">

Summary

With this part, we introduced some simple steps to setting up our Rails + Devise app to make use of OAuth for authenticating a user with their GitHub accounts.

In the next part, we will be adding reCAPTCHA to our app to help add a layer of protection against spam.

Resources and further reading

Photo credit: socialcut

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on workingoutloud.dev, Den Dribbles and LandPad .

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.