Skip to content

A tutorial application for the OmniAuth authentication gem. (Babl is Another weBLog)

Notifications You must be signed in to change notification settings

joshrowley/babl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

45 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Further resources for OmniAuth:

Here are the resources that helped me put together the presentation:

[Good tutorial using Facebook](“net.tutsplus.com/tutorials/ruby/how-to-use-omniauth-to-authenticate-your-users/)

[Great tutorial from Intridea](blog.railsrumble.com/2010/10/08/intridea-omniauth/)

[Michael Bleigh, the maintainer of omniauth, discussing OmniAuth](“www.intridea.com/blog/2011/5/31/omniauth-from-the-ground-up)

Ryan Bates also has a very good [screencast](railscasts.com/episodes/241-simple-omniauth-revised) about using omniauth with Twitter, although you’ll need a subscription. He has a couple more good screencasts about Omniauth and authentication on the site, the entire resource is worth the monthly subscription.

0.0 Introduction

Who am I?

@joshrowley, current student @FlatironSchool, former IT guy

What does this presentation cover?

OmniAuth

  1. What is OmniAuth? What does it do and why should we use it?

  2. Anatomy of OmniAuth How does it work?

  3. Implementing OmniAuth That’s all fine and dandy, but how do I actually write the code?

What does this presentation not cover?

This presentation is meant as an introduction to OmniAuth and a quick start way to learn about how it can help you as a developer. Thus, I will covering the minimum basic concepts in order to authenticate a user. I will not be covering the following:

  1. Other authentication libraries, and how they compare to OmniAuth

    There are a ton of authentication libraries available for Rails developers. I don’t really know enough about them to judge whether these might be better solution for you.

  2. Implementing user permissions for your resources I’m going to show how to get a user authenticated in your, but you’ll need to figure out how you’ll implement permissions for your app’s resources.

  3. Low level examination of how OmniAuth works We’ll take a brief look at the overarching design of OmniAuth, but I won’t go into specific implementation and methods of the OmniAuth module.

1.0 What is OmniAuth? Why should we use it?

1.1 The Authentication Problem

An example

BABL is a blogging platform in development. It’s going to reinvent what blogs can do. The dev team hasn’t gotten far at all. So, far there’s only a Post model and each post has a title and some content.

from db/schema.rb:

create_table "posts", :force => true do |t|
  t.string   "title"
  t.text     "content"
  t.datetime "created_at", :null => false
  t.datetime "updated_at", :null => false
end

Trust me, there’s some innovation coming down the pipeline. The BABL dev team’s main focus is on the Post model, that’s the core of their product.

But pretty soon, they’ll need to implement some type of user authentication system. After all, we can’t have anyone have access to creating, updating, and destroying posts.

It’s crunch time, the BABL dev team needs to showcase their product to potential investors. They still have a ton of work to do on special Post related features, but they won’t have any type of usable product to show if there’s no user login! What are their options?

1.2 Options for adding authentication

1.2.1 Create authentication from scratch

The team could try writing their own authentication system.

This seems like reinventing the wheel though right?. I mean what web application doesn’t have user logins. Surely someone has figured out a pretty damn good way of doing it.

There’s also the possibility that they don’t implement it correctly. There would need to be extensive testing throughout all layers of the application to find any security holes.

This will also take a long time and demo day is soon!

1.2.2 Use existing authentication libraries

Devise, Authlogic, Clearance, Sorcery, Restful Authentication

You can create your own application’s authentication system using open source gems like these.

But, maybe I don’t want to have my users have to remember another set of logins and passwords.

Furthermore, the team is really pressed for time, these authentciation libraries may have rigid requirements, or make rigid assumptions about how our application needs to architected.

The BABL developers need to get it working ASAP, while maintaining flexibility for the future, when perhaps they’ll add their own authentication. That leads them to…

1.2.3 OmniAuth

Written by Michael Bleigh with support from his employer Intridea (dev shop) (3/30/10, first commit, 10/1/10 release) see talk: www.intridea.com/blog/2011/5/31/omniauth-from-the-ground-up talks about the design thoughts and principles

OmniAuth provides an easy abstract interface with authentication providers.

OmniAuth is flexible and tries to make as little assumptions as possible. It’s built to be able to authenticate with any external or internal authentication system, all you need to do is write a strategy.

The implementation of external provider doesn’t matter as long as you have a strategy.

OmniAuth standardizes the way you request authorization in your application as well as send back your application a cleaned up hash with all your user’s info.

Omniauth gives us an easy way to authenticate users TODAY, while leaving a ton of flexibility to allow our application to authenticate with whatever the future may hold.

2.0: Anatomy of OmniAuth: How does this work?\

So this diagram is easy to follow. My application triggers something (a user clicking on login) that will indicate to OmniAuth that I’m requesting authentication.

Something happens, and then I get back an authentication hash back indicating that the external provider confirmed this persons identity. With this user info that I now can do whatever I want with in my application.

But, at least for me this diagram is a little too abstract, so before we actually get into the actual implementation on how to integrate OmniAuth into our application, let’s take a peek at the “magic”.

2.1: OmniAuth is middleware

OmniAuth is Rack middleware that can be used with any Rack based Ruby applications.

It’s not just limited to Rails, you could use it in a Sinatra application as well like this:

require 'sinatra'
require 'omniauth'

class MyApplication < Sinatra::Base
  use Rack::Session::Cookie
  use OmniAuth::Strategies::Developer
end

In a Rails application, you would add ‘config/initializers/omniauth.rb`

‘omniauth.rb`

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']
  provider :facebook, YOUR_APP_ID, YOUR_APP_SECRET
end

This tells Rails to use OmniAuth as middleware using the facebook and twitter strategies when the application boots.

2.2: Omniauth knows how to authenticate through “strategies”

We’ll take a look at the code a strategy entails in a little bit.

What is a strategy? A bunch of Ruby code!

Specifically, a strategy is class that inherits a bunch of magical stuff from an OmniAuth::Strategies module.

Interaction with the strategy class can be defined into two phases: the request phase, and the callback phase

2.3 The Request Phase

The Request phase is triggered when your applications tells OmniAuth that it needs authentication.

How do you tell OmniAuth this?

The OmniAuth middleware listens for HTTP requests going through Rack on the path ‘/auth/:provider` where `:provider` refers to the specific provider strategy specified in `omniauth.rb` Rack configuration file.

When OmniAuth receives the HTTP request from your web server (e.g. nginx, Apache), it intervenes as a middleman in front of your application, and starts the request phase.

What is the request phase?

The first part of the strategy. All the necessary steps to authenticate with the external provider written in Ruby.

OmniAuth responds to the ‘auth/:provider` HTTP request and executes whatever code it needs to prepare that request for authentication for the proper provider.

OmniAuth acts like a wrapper, accepting whatever necessary information needed for authentication (usually an App ID, and App secret provided by the external provider) and preparing it for the external provider according to the strategy.

In OAuth, this is a redirect to the provider’s auth interface (facebook) OpenID, LDAP, something else, the logic is different, but the abstract concept of what OmniAuth is doing is the same. It’s communicating with the external provider’s authentication API, whatever it may be.

In the case of Facebook, OmniAuth sends along the App ID and App secret that were taken in as arguments to the strategy along with the site requesting authentication, the callback path, and a failure path (‘/auth/failure`).

2.4 The Callback Phase

The external provider then processes the authentication request (e.g. Facebook login), and sends back to the callback path whatever raw info it needs to indicate that the user passed authentication. OmniAuth then triggers the callback phase.

The external provider sends an HTTP request back to ‘/auth/:provider/callback`.

Again, OmniAuth intervenes on this HTTP request at the Rack layer since it recognizes the ‘/auth/:provider/callback` path as corresponding to a strategy it has in it’s ‘omniauth.rb` file

OmniAuth receives the raw response from Facebook’s authentication system and then uses the strategy again to figure how to parse and format the response into the standard authentication hash that our application is expecting.

Once it creates the authentication hash, it stores it in a Rack request object, accessible in request.env.

OmniAuth will then pass the request along to your application to a ‘/auth/:provider/callback` route. Again, this hash will always contain the keys `provider`, `uid`, `info`, and `info. Every strategy must return this hash and because of this we can easily add multiple providers since our application knows what to expect no matter what strategy we use.

2.5 Your application’s logic

Great! Now this authentication hash has a way into your application, and you can feel free to do what you want with it.

Most likely, you’ll want to add a SessionsController that the callback phrase routes to that will find or create the User in your application.

Everytime a user logs in with the same credentials through a provider, the provider should return an authentication hash with the same ‘uid`, so each time the user comes back it should find the same user, and associated permissions specific to your application.

2.6 Peeking at some strategies

Let’s take a quick look at some of the available strategies, and we can see that there are methods defined in the provider’s class that link up with the two phases (request/callback).

For example, in the Facebook strategy:

github.com/mkdynamic/omniauth-facebook/blob/master/lib/omniauth/strategies/facebook.rb

module OmniAuth
  module Strategies
    class Facebook < OmniAuth::Strategies::OAuth2

    ...

    def request_phase

      ...

    end

Example from GitHub

module OmniAuth
  module Strategies
    class GitHub < OmniAuth::Strategies::OAuth2
      ...

      def request_phase
        super
      end
...

3.0: Implement OmniAuth!!!

3.1: Getting the authentication hash from Facebook.

3.1.0: Setup your application with the provider

In the case of this Facebook example, I’m going to want to go to Facebook’s Developer page, register my application, and grab the APP ID and APP SECRET values from my developer page to put into the OmniAuth Rack configuration block.

3.1.1: Add the provider’s gem to your Gemfile

# gem 'omniauth'
gem 'omniauth-facebook'

The omniauth gem contains the modules that all the other provider gems inherit from, however you usually don’t need to include it in your Gemfile since most provider gems require it as a dependency.

3.1.2: Run bundle install to load OmniAuth into your application environment

$ bundle install

3.1.3: Create omniauth config file, so that OmniAuth middleware starts up with your app.

$ touch config/initializers/omniauth.rb

‘omniauth.rb`

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :facebook, 'APP_ID', 'APP_SECRET'
  provider :twitter, 'CONSUMER_KEY', 'CONSUMER_SECRET'
  provider :linked_in, 'CONSUMER_KEY', 'CONSUMER_SECRET'
end

Let’s run the server

$ rails server

And, now when we go to ‘/auth/facebook`, OmniAuth redirects us to Facebook to login.

Seriously, that’s how easy it is to set up OmniAuth. At least the middleware part.

So, now that we’ve just set up the OmniAuth middleware, what error will we expect when we try to start the callback phase after authenticating with Facebook?

Missing routes! But which route is going to be missing.

OmniAuth sending back a Rack request with the authentication hash to our application to ‘/auth/facebook/callback`.

So, OmniAuth has already kicked in as middleware, and is routing ‘auth/facebook` to Facebook’s authentication, and returning the Facebook hash. We’re seriously done with the OmniAuth part, now we just need to build our application to do something meaningful with this.

We need to define a route and a controller that will take the hash in and create or find a user in our application.

Conventionally, this is done using a SessionsController, with a route pointing to ‘sessions#create’. In that create method, we should have logic in our application that should either find a User of our application if that person has already authenticated before with that provider, or create a new user.

3.1.4 Create the route that will respond to the callback message from OmniAuth

‘routes.rb`

match '/auth/:provider/callback', :to => 'sessions#create'

Great, so I’ve made the route. What do I need to do next?

Create a controller method that responds to that route.

3.1.5 Create a sessions controller

So I’m going to generate a sessions controller.

$ rails g controller sessions

And then, write the create method:

3.1.6 Now to define the sessions#create method:

I’m just going to write something very simple for now, just to show that we are in fact now authenticating with Facebook.

I’ll just render to text the request incoming from Rack:

in ‘/app/controllers/sessions_controller.rb`

class SessionsController < ApplicationController
  def create
    render :text => request.env['omniauth.auth'].inspect
  end
end

3.1.7: Verify that OmniAuth is returning a hash

So now we can see that OmniAuth should be running as middleware and returning a hash.

$ rails server

OmniAuth will do its magic based on the strategy given by the provider you specify in ‘omniauth.rb`.

OmniAuth responds to the ‘/auth/:provider` request from the web, looks at the strategy associated with the path, and follows the directions.

In the case of Facebook & Oauth2, OmniAuth redirects to Facebook for the user to login.

Once the user entered correct password, Facebook sends back a request to ‘/auth/:provider/callback` with the information about the user.

OmniAuth, once again receives the request from your web server, before it hits your Rails application. It does whatever magic it needs to do as specified in the strategy’s callback phase, constructs the authentication hash, adds it to a Rack request object and then passes it along to your Rails application using the same ‘/auth/:provider/callback` route.

And, now if we go to the browser and go to ‘/auth/facebook`, the returned page should have the authentication hash in the source.

So, we can compare this with our spec of what the returned hash should look like and it should contain all our required info: ‘provider`, `uid`, `info`.

Note that in the latest version of OmniAuth, the hash is returned as a hashlike object called a Hashie Mashie. This object behaves pretty much exactly like a hash, except it also creates methods that allow us to access the hashes key, so that the following two syntaxes would return the same thing:

auth_hash['info']['name']
auth_hash.info.name

3.2: Using that hash in our application

Now we as developers have the hash, time to decide what to do with it. We’re going to want to persist data about newly authenticated users, because they will now have blog posts that need can be associated with users.

Thus, a User model is born!

What do I want this User model to store. Well, what’s great about OmniAuth is that I don’t need to worry about whether some of my providers return a different type of object that the others. I always know that my hash is going to have the following information

auth.provider #=> identifies the auth service auth.uid #-> unique id for user on auth servcie auth.info.name #=> a name for the user

The ‘uid` is going to be the unique identifier for that user in the context of the external provider, so that should be a column in our User model, so that when the user logs in again, it should pull up the same user.

User schema

id name email provider uid

Wait a second though, this model schema is going to cause issues for us.

Can anyone predict the problem we’re going to have? How is this going to affect us in the future?

Things get dicey once you want to allow users to authenticate using multiple providers, and not just one. When the same person authenticates with a different provider, another User entry will be created, and what we want is for the multiple authentications to point to the same user record and thus will have the correct associations with other resources in our application. You’ll have two entries in the users table that refer to the same person.

The database is not normalized in this way, so we’ll want to break out authorizations into its own model, where User ‘has_many :authorizations` and Authorization `belongs_to :authorization`.

So, let’s build those models:

3.2.1 Build User and Authorization models

$ rails g model User name:string email:string
$ rails g model Authorization provider:string uid:string user_id:integer

3.2.2 Migrate the database

$ rake db:migrate

3.2.3 Add the association between User and Authorization

user.rb

class User << ActiveRecord::Base
  has_many :authorizations
end

authorization.rb

class Authorization
  belongs_to :user
end

3.2.4: Validate authorization’s atrributes presence and uniqueness.

I’m also going to throw some validations on here, so that authorizations must be have an associated user (‘user_id`), an external unique ID (`uid`) , and which `provider` it’s associated with, as well as validate the uniqueness of the ‘uid` in the scope of the given provider. When we start having multiple providers, we have to ensure that two providers could have the same `uid`.

authorization.rb

class Authorization
  ...

  validates_presence_of :user_id, :uid, :provider
  validates_uniqueness_of :uid, :scope => :provider

end

3.2.5: Validate uniqueness of email

I’ll also validate the uniqueness of the ‘:email` attribute of my User model. I’ll be using ‘email` as a unique identifier for a certain person, so that once I start adding multiple providers, new authorizations will check the email against the users table to see whether this is and existing user’s additional authentication, or if this is a completely new person.

One thing to note is that Twitter does not return a user’s email address, so you’ll have to add something into the sessions controller that will prompt new authorizations for an email address to check against the users table.

user.rb

class User
  ...

  validates_uniqueness_of :email
end

3.2.6: Write 1st iteration of sessions create logic

  1. When a user signs in, look for existing Authorizations for that

external account.

  1. Create a user if no authorization is found.

  2. Add an authorization to an existing user if the user is already logged in.

‘sessions_controller.rb`

def create

  auth = request.env['omniauth.auth']

  if @authorization = Authorization.find_by_provider_and_uid(auth.provider, auth.uid)
  else
    user = User.find_or_create_by_email(:email => auth.info.email)
    user.name = auth.info.name,
    @authorization = user.authorizations.build(:provider => auth.provider, :uid => auth.uid)
    user.save
  end

end

Let me clear my db real quick just to show you this logic is working.

Now, when a user authenticates for the first time, it should create a new user.

When the same user comes to authenticate, since I already have their authorization information in the database, it should find the authorization and find the user from the belongs_to association.

3.2.7: Creating a session with a method in the Application Controller

Now we have @authorization, what do we do with it? Well, we’ll want to create a session so that as the user browses through the app, he’ll stay logged in until he logs out.

The user’s session should persist across the entire application, so I’ll define a method ‘current_user` in the application controller that’s inherited by every single controller in my application that will tell give me the user who is currently logged in. In addition, to the getter ‘current_user` method, I’ll define a setter method ‘current_user=` that my sessions controller will use to set set the user once they’ve authenticated:

First, I’ll put the expected method in my controller and then define it in the application controller:

in ‘sessions_controller.rb`

def create
    ...
    user.save
  end

  self.current_user = @authorization.user
end

We’ll put whether or not the user is logged in into the Application Controller,whose methods are inherited by every controller in our application.

Now, how do I persist a user’s presence with every later request they enter?

We’ll use the sessions, and Rails will automatically pass the data we put into the sessions hash using hidden forms and cookies.

I’ll also make ‘current_user` a helper method, which makes the method avaialable in my views. That way in my application view I can call the current user and display their name using some logic later.

in ‘application_controller.rb`

class ApplicationController < ActionController::Base
  protect_from_forgery

  def current_user
    User.find_by_id(session[:user_id])
  end

  def current_user=(user)
    session[:user_id] = user.id
  end

  helper_method :current_user

end

The home stretch, all the heavy lifting is done, let’s just wire everything up.

3.2.8 Set redirect for sessions#create

So now to get the controller wired up with my view, instead of rendering the text from the authentication hash, I’ll want to redirect to my home page.

‘sessions_controller.rb`

def create
  ...
  redirect_to root_url
end

3.2.9 Set up login and logout buttons

I’ll add add to my logout button a link to a ‘/logout` route.

I’ll also add a little bit of logic to either display the signed in user’s name, or prompt the unauthenticated user to sign in using Facebook.

‘application.html.erb`

<div class="btn logout">
  <%= link_to "Logout", "/logout" %>
</div>
<div class="btn login">
  <% if current_user %>
    Signed in as <%= current_user.name %>
  <% else %>
    <%= link_to "Sign in with Facebook", "/auth/facebook" %>
  <% end %>
</div>

3.2.10 Create logout path in routes

‘routes.rb`

match '/logout', :to => 'sessions#destroy'

3.2.11 Define sessions#destroy

‘sessions_controller.rb`

def destroy
  session[:user_id] = nil
  redirect_to root_url
end

3.3 The world is your oyster!

Refactor the code:

  1. Push the logic out of the controller and down to the models.

  2. Cache the current user in an application wide instance variable.

  3. Write the code that responds to ‘auth/failure`

  4. Twitter doesn’t return ‘auth.info.email`, write whatever code

necessary to deal will an already registered user who wishes to add Twitter authentication that is linked to his same ‘uid`

Challenge: Can you write a strategy?

About

A tutorial application for the OmniAuth authentication gem. (Babl is Another weBLog)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages