Skip to content

Latest commit

 

History

History
372 lines (282 loc) · 11.2 KB

omniauth.md

File metadata and controls

372 lines (282 loc) · 11.2 KB

Authentication w/ OmniAuth

OmniAuth is a gem that standardizes the authentication process using 3rd party providers. OmniAuth supports a ton of providers, a quick search shows over 150, some of the popular providers are:

  • Twitter
  • Facebook
  • Github
  • Heroku
  • LinkdIn
  • Netflix
  • Paypal

In this lesson we will be creating a rails application that uses OmniAuth to authenticate new and returning users using Github.

OmniAuth

The OmniAuth gem provides pretty much everything you need to use these services, starting with adding a couple routes to your application:

  • /auth/:provider
  • /auth/:provider/callback

In both of these examples :provider is a named parameter that will equal the name of the service we are using, in this example it will be github. These two routes in our application are how we start and end the authentication interaction with the provider, sending the user to /auth/github, will start the authentication process, when Github is finished authenticating them, they will be sent back to our application to the route /auth/github/callback

OmniAuth Dance

Getting started

Enough with talking, lets implement this into a quick application:

rails new omni -T -d postgresql
cd omni
atom .

Open the Gemfile and add two gems

  gem "omniauth"
  gem "omniauth-github"
  group :development do
    gem "better_errors" # These two are just for debugging
    gem "binding_of_caller"
  end

Then bundle install, you notice that we have a specific gem for Github. Each provider has a small gem to add the functionality for that specific provider.

Configuration

Each provider requires you to provide some credential for your application, so they can keep track of which website is authorizing which user. We can login to Github and register a new "application" here https://github.com/settings/applications/new

Github Application Registration

After registration you will be given a client id and a client secret:

Github Application Credentials

Note: These are essentially passwords to your Github account, keep them safe, never post them in public places or commit them in git.

Now that we have our credentials, we can configure our rails application, we will do this in an initializer file, initializers are files that run as a rails application starts, any file inside of config/initializers/ are loaded.

touch config/initializers/omniauth.rb

Inside of this new file, add this configuration:

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :github, YOUR_CLIENT_ID, YOUR_CLIENT_SECRET
end

We don't want to commit our id or secret to github, so we can use what is called and environment variable. Basically there is a constant in called ENV which is a hash of data loaded when ruby was loaded.

We can use a gem called dotenv-rails to help us manage our environment variables.

#Gemfile
gem 'dotenv-rails', :groups => [:development, :test]
#.env
GITHUB_CLIENT_ID: fd6XXXXXXXX
GITHUB_CLIENT_SECRET: y6wXXXXXXX

This will load these key/values as environment variables in our rails app, so now the config/initializers/omniauth.rb can be changed to:

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"]
end

Whew... done with configuration

Implementing Users

Boot your rails app and go to /auth/github. Amazingly, this sends us to Github to login and grant permission! When we proceed we end up back at localhost:3000/auth/github/callback and we can see a bunch of information in the url parameters. Let's add this route to our routes.rb so we can see what the response has to offer:

get "/auth/:provider/callback", to: "sessions#create"

I've made the route point to a sessions controller to the create action, you could send it anywhere, but creating a new session for our user makes sense, so we'll call it sessions. Let's create this controller and action

touch app/controllers/sessions_controller.rb

Now build this controller with a create action:

class SessionsController < ApplicationController

  def create
    auth_hash = request.env['omniauth.auth']
    raise
  end

end

You can see, we're assigning a variable to request.env['omniauth.auth'], this is information stored in the headers of the HTTP request, don't worry about how it gets there, it's a bit of magic. This data is a hash, you can see all of the likely keys this hash will have in the OmniAuth README, but the key/values returned varies by provider, although, Github will return the following important keys.

auth_hash["uid"] # The unique ID of the user from the providers database
auth_hash["info"]["name"]
auth_hash["info"]["email"]
auth_hash["info"]["image"]
auth_hash["info"]["nickname"]

We can use the above information returned by Github to create a user.

Creating Users

We'll create a user model and use TDD to implement the OmniAuth creation of a user

# Gemfile

group :development do
  gem 'rspec-rails'
end
    rails g rspec:install
    rails g model user username:string email:string uid:string provider:string avatar_url:string
    rake db:migrate
    rake db:test:clone

Next we'll add some configuration and specs to test the creation of users with OmniAuth:

# /spec/spec_helper.rb

## The following goes inside the `Rspec.configure` block:
config.before(:suite) do
  # Once you have enabled test mode, all requests
  # to OmniAuth will be short circuited
  # to use the mock authentication hash.
  # A request to /auth/provider will redirect
  # immediately to /auth/provider/callback.

  OmniAuth.config.test_mode = true

  # The mock_auth configuration allows you to
  set per-provider (or default) authentication
  hashes to return during testing.

  OmniAuth.config.mock_auth[:twitter] = OmniAuth::AuthHash.new({:provider => 'github', :uid => '123545', info: {email: "[email protected]", nickname: "Bookis"}})
end
# /spec/models/user_spec.rb

require 'spec_helper'

describe User do
let(:user) { User.new(
    email:    "[email protected]",
    username: "Bookis",
    uid:      "1234",
    provider: "github")
  }

  describe "validations" do
    it "is valid" do
      expect(user).to be_valid
    end
    it "requires an email" do
      user.email = nil
      expect(user).to be_invalid
    end

    it "requires a username" do
      user.username = nil
      expect(user).to be_invalid
    end

    it "requires a uid" do
      user.uid = nil
      expect(user).to be_invalid
    end
    it "requires a provider" do
      user.provider = nil
      expect(user).to be_invalid
    end
  end


  describe ".initialize_from_omniauth" do
    let(:user) { User.find_or_create_from_omniauth(OmniAuth.config.mock_auth[:twitter]) }

    it "creates a valid user" do
      expect(user).to be_valid
    end

    context "when it's invalid" do
      it "returns nil" do
        user = User.find_or_create_from_omniauth({"uid" => "123", "info" => {}})
        expect(user).to be_nil
      end
    end
  end
end

To fix the rspec failures we would add the following to our app/models/user.rb

class User < ActiveRecord::Base
  validates :email, :username, :uid, :provider, presence: true

  def self.find_or_create_from_omniauth(auth_hash)
    # Find or create a user
  end

  def self.create_from_omniauth(auth_hash)
    # Create a user
  end
end

Receiving the request

Now we have the functionality to initialize a user using the hash that will be return from the provider request, lets TDD the sessions controller create action

Terminal

mkdir spec/controllers
touch spec/controllers/sessions_controller_spec.rb

config/routes.rb, adding a root_path route.

root to: "users#show"
# spec/controllers/sessions_controller_spec.rb
require 'spec_helper'

describe SessionsController do
  describe "GET 'create'" do
    context "when using github authorization" do
      context "is successful" do
        before { request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter] }

        it "redirects to home page" do
          get :create, provider: :github
          expect(response).to redirect_to root_path
        end

        it "creates a user" do
          expect { get :create, provider: :github }.to change(User, :count).by(1)
        end

        it "assigns the @user var" do
          get :create, provider: :github
          expect(assigns(:user)).to be_an_instance_of User
        end

        it "assigns the session[:user_id]" do
          get :create, provider: :github
          expect(session[:user_id]).to eq assigns(:user).id
        end

      end

      context "when the user has already signed up" do
        before { request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter] }
        let!(:user) { User.find_or_create_from_omniauth(OmniAuth.config.mock_auth[:twitter]) }

        it "doesn't create another user" do
          expect { get :create, provider: :github }.to_not change(User, :count).by(1)
        end

        it "assigns the session[:user_id]" do
          get :create, provider: :github
          expect(session[:user_id]).to eq user.id
        end
      end

      context "fails on github" do
        before { request.env["omniauth.auth"] = :invalid_credential }

        it "redirect to home with flash error" do
          get :create, provider: :github
          expect(response).to redirect_to root_path
          expect(flash[:notice]).to include "Failed to authenticate"
        end
      end

      context "when failing to save the user" do
        before {
          request.env["omniauth.auth"] = {"uid" => "1234", "info" => {}}
        }

        it "redirect to home with flash error" do
          get :create, provider: :github
          expect(response).to redirect_to root_path
          expect(flash[:notice]).to include "Failed to save the user"
        end
      end
    end
  end
end
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def create
    auth_hash = request.env['omniauth.auth']
    if auth_hash["uid"]
      @user = User.find_or_create_from_omniauth(auth_hash)
      if @user
        session[:user_id] = @user.id
        redirect_to root_path
      else
        redirect_to root_path, notice: "Failed to save the user"
      end
    else
      redirect_to root_path, notice: "Failed to authenticate"
    end
  end

end

Showing the User

Next we'll quickly create a show page to redirect to after we login:

rails g controller users show

app/controllers/application_controller.rb

    def current_user
      @current_user ||= User.find(session[:user_id]) if session[:user_id]
    end
    helper_method :current_user

app/views/users/show.html.erb

<%= image_tag current_user.avatar_url %>
<h1><%= current_user.username %></h1>
<b><%= current_user.email %></b>

Resources

Environment variables on Heroku