LJ Archive

At the Forge

Working with OAuth

Reuven M. Lerner

Issue #213, January 2012

Want your users to be able to log in via Facebook, Twitter or Google+? OAuth is the answer!

Like many software developers (and people in general), I'm something of a hypocrite. When I'm working with clients, I tell them how important it is to have secure passwords, how they should use a different password on every site, and how they should force their users to have secure passwords as well. And although I often do use different passwords for different sites and try to change them on a semi-regular basis, the fact is that I can remember only so many different ones and end up reusing a number of them in different variations and at different times. Better (or worse) yet, I don't need to remember these passwords, because my browser can and often will do it for me.

Now, this clearly is a problem, because it means if others crack one of my reused passwords, they might (depending on the password in question) be able to access a number of other systems around the world, from sites where I shop to servers I help operate.

Passwords have other problems associated with them as well. When you register with a Web site, how do you know that you can trust the site with your identity? Do you really want to create another account? You've already registered with many different sites through the years. Why not just use one of those logins to authenticate your session at another site? (Of course, you could argue that there's no reason to trust certain sites, such as Facebook, but that's another discussion entirely.)

This idea, that you shouldn't need to register with new sites and instead should be able use your existing registration information from other sites, has gained popularity in the last few years. A protocol known as OAuth makes it possible to put this into effect, letting any site be a “consumer” (that is, use another site's authentication credentials) or a “provider” (that is, make its user-registration information available to other applications). This means that although people normally might use OAuth to log in via Twitter or Facebook, there's no reason why your own applications could not be providers as well. OAuth itself is silent on this matter, allowing each consumer application to decide which providers it will and won't allow.

This column focuses on Web applications, so I'm not going to discuss the OAuth protocol itself. For that, I suggest reading the appropriately named “A Primer to the OAuth Protocol” by Adrian Hannah in the June 2011 issue of LJ (see Resources).

A number of alternatives have been proposed through the years, but none of them really have taken off. One of the more intriguing suggestions, OpenID, is something I covered several years ago in this column, and I'd still like to see it succeed, but as a speaker mentioned at a conference I recently attended, OpenID was neither clear nor appealing to anyone outside the software development community.

This article shows how to include OAuth in your own applications. OAuth is platform-neutral, meaning you can use it from any language you like. As regular readers of this column know, I'm a big fan of the Ruby language and the Ruby on Rails framework for Web development, so I demonstrate OAuth in that way here. Implementations for many other languages and frameworks exist as well, so if you're not a Ruby developer, consider looking for the appropriate library for your favorite language.

OAuth Basics

As I mentioned previously, I'm not going to describe the OAuth protocol in detail. Rather, I want to describe how it works from the perspective of a Web application.

First and foremost, you need to decide which providers you are interested in using and then register with each of them. Different applications will want to use different providers. For example, GitHub, a commercial service that hosts Git repositories, supports OAuth. If your users are likely to have GitHub accounts, it might be worthwhile to have it as a provider. But, if you're creating a consumer shopping application, the odds are pretty good that most of your users won't have GitHub accounts. Thus, you also should consider another provider, such as Facebook or Twitter.

Registering with a provider gives you a unique ID and a secret, which you can think of as a user name and password for your application. These will allow your application to connect to the provider.

When users want to authenticate to your non-OAuth site, they typically have to enter a user name and password. But if your system uses OAuth, the user name and password (or any other system) is handled by the provider, rather than by the consumer (that is, your application). So long as the provider agrees that you're who you say you are, your application will accept that claim.

The authentication process begins when users indicate they want to log in via a third-party provider. Users are redirected to a URL on the provider's site, where users are asked to authenticate. In some cases, this can happen in one step, passing the application's ID; in other cases, it requires two HTTP transactions: one to get a request token and a second to perform the actual authentication. Regardless, the result of authentication is an HTTP request from the provider to your application. That request will include several additional parameters, including a code generated by the provider.

Finally, your application must make a call to the provider, combining everything you have used so far—the application's unique ID for this provider, the secret and the code you got from the provider after the user's authentication. If all goes well, the server will respond by requesting your URL again, indicating (via passed parameters) that you have authenticated this user.

Actually, one additional piece of information is handed to the OAuth provider, namely the “scope”, which you can think of as a set of permissions. Many providers offer different amounts of information about a person. For example, Facebook could provide you with my name and e-mail address, information about my friends or even the ability to read my chat and inbox messages.

Not all users will want to allow your application the full set of Facebook permissions. A good general rule of thumb is that you should ask for the minimum set of permissions from the provider that will allow your application to work. If your application works on your Facebook inbox, it obviously will need permission to work with the inbox. If not, there's no reason to grant it such permission. The permissions you request in the “scope” parameter will be displayed to the user as part of the authentication dialog, such that he or she will not be surprised by the degree to which the consumer application can access private data.

Setting Up Devise

If the above flow seems a bit complicated, that's because it is. Instead of a simple HTML form that is submitted to a controller on your own server, you're now having a conversation with a remote server that requires three or four different HTTP transactions, each with its own set of parameters. But, it's pretty clear to me that for all of its complexity and added overhead, OAuth is a good way to go for many Web sites.

Fortunately, as is often the case with open-source software and network standards, freely available libraries make the integration of such functionality fairly easy. For example, let's assume I want to create a “hello, world” application. Anonymous users get a plain-old “hello, world”, but registered users get a premium version of “hello, world”, so there's a big incentive for people to register and sign in.

I have been using the devise library for user registration and authentication for some time now, and I've found it to be quite easy to use as well as flexible. I'm not the only one who has found it to be flexible though. A large number of plugin modules are available for devise, making it possible to change the way authentication is done. One of these plugins is called (somewhat confusingly) OmniAuth, which makes it possible to authenticate against a variety of sites and protocols, including OAuth.

At the shell prompt, I first created three databases in PostgreSQL:

createdb helloworld_development
createdb helloworld_test
createdb helloworld_production

I then created a simple application using PostgreSQL:

rails new helloworld -d postgresql

Next, I modified the Gemfile such that it would use devise, by adding the following line:

gem 'devise'

Then, I invoked:

bundle install

to ensure that the Ruby gem was installed in my application. Next, I installed devise into my application:

rails g devise:install

With that in place, I created a new “user” model, using the generator that comes with devise:

rails g devise user

I then created a new route, so that I can get to the home controller:

root :to => 'home#index'

Realizing I didn't yet have a home controller, I then added it:

rails g controller home

Next, I removed public/index.html to ensure that it doesn't take over from my application.

Then, I made a slight change to the database configuration file (config/database.yml). I created the database as the “reuven” user, which doesn't require a password on my local machine, but Rails (reasonably) assumes that I want to have a unique database user for this application. So I modified config/database.yml, such that the “user name” provided was changed to “reuven”. I was able to test that database access worked correctly by using the shortcut to enter the psql database client:

rails db -p

With all of that in place, I ran the migrations:

bundle exec rake db:migrate

That created my user model in the database.

Next, I modified app/views/hello/index.html.erb, such that it gives a quick “Hello” response to anyone who invokes it and added the following route in config/routes.rb:

match '/hello' => 'hello#index'

I realize that this seems like an awfully long set of steps just so you can play with OAuth. But if you've been doing Rails development for any length of time, this shouldn't faze you too much, as much of it becomes second nature.

Adding Authentication

Now that I have a basic working application, I'm going to force people to log in. Because I'm using devise, I can add a before filter to any controller that forces users to log in if they haven't done so already. I just add the following inside the controller's classes for people to be signed in:

before_filter :authenticate_user!

I put this line at the beginning of the class definition of HelloController. And sure enough, the next time I went to / on my server, I got a registration form.

So far, so good, but say you don't want to have a devise registration form. Rather, you want people to sign in via a third-party application. For now, let's say you want people to log in via Facebook. How do you accomplish that?

Well, the first thing you need to do is ensure that you're on a “real” URL, on a publicly available server. It's nice to develop on your own local computer, using NAT and a router, but remember that OAuth requires that the server will send a message to use via HTTP. If your server is unreachable from the outside world, this will be a problem.

The second thing is that you need to register with Facebook in order for this to work. Facebook registration means creating a new application, just as if you were building an application on the Facebook platform itself. I went to https://developers.facebook.com/apps, clicked on create new app and entered “helloworld” for the app name and “atfhelloworld” as the namespace. A few moments later, I was looking at the page for my brand-new application, with both an ID and a secret.

I then went to the text field labeled, “I want to allow people to log in through Facebook”, and entered the public URL for my application, which I intend to set up: http://www.lerner.co.il:2222/.

Now that Facebook knows where it can and should go, you need to make some configuration changes on your server. After all, while you're using devise, you haven't configured OAuth at all.

The first thing you need to do is tell your Rails application to load the gems that will be necessary for Omniauth, OAuth and OAuth's connection to Facebook, by putting the following in your Gemfile:

gem "oa-oauth", :require => "omniauth/oauth"
gem 'omniauth-facebook'

Notice that you need the :require parameter, because the name of the gem isn't identical to the location of the file that defines the library of the same name.

Next, open up your User model (app/models/user.rb), and add :omniauthable to the long list of descriptions that you give your user model. By default, the list looks like this:

devise :database_authenticatable, :registerable,
       :recoverable, :rememberable, :trackable, :validatable

But, after you're done, it looks like this:

devise :database_authenticatable, :registerable,
       :recoverable, :rememberable, :trackable, :validatable,
       :omniauthable

Now, tell Omniauth your ID and secret for connecting to Facebook, putting them in the devise configuration file, config/initializers/devise.rb. Omniauth is a separate gem, but you're using it within devise, so the configuration goes into that file. Omniauth actually is a common and popular gem to use with devise, which is why the following comments are placed inside this configuration file by default:

# ==> 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'

I added a line here for Facebook:

config.omniauth :facebook, '149179328516589',
    'XXXXXX59862dede3791430ffXXXXXXXX',
    :scope => 'user_about_me,user_education_history,read_friendlists'

Notice how the scope is Facebook-dependent; each provider can set its own list of scopes, according to what information it has to offer.

Finally, you need to modify your application such that when the user wants to log in, you go to Facebook, rather than to the default registration and login forms. For this to work, you need at least one page without authentication enabled. (As a general rule, you don't want to authenticate every page on a system—after all, users should be able to see the home page without logging in, right?)

Thus, I changed the before_filter in HelloController to read:

before_filter :authenticate_user!, :except => :index

In other words, users need to be authenticated except for your home (default) page, index. I also added a new route to config/routes.rb:

match '/goodbye' => 'hello#goodbye'

This means that your “hello” controller responds to a second action, named “goodbye”. And, according to the rules you have established, users who want to see “goodbye” need to log in. Sure enough, after putting in these changes, going to /hello results in a “hello!” being displayed, without any need to register or log in. But, going to /goodbye redirects you to the devise login form.

Now that users can come to your home page without being logged in, you can provide them with the option to log in, either using your regular registration system or via Facebook. The easiest way I've found to do this is to modify the default layout for the application, in app/views/layouts/application.html.erb. I added the following:


<%= link_to "Sign up", new_user_registration_path %> &bull;
<%= link_to "Sign in", new_user_session_path %> &bull;
or <%= link_to "Sign in with Facebook",
user_omniauth_authorize_path(:facebook) %>

In other words, users now can sign up (register) with your local registration system or log in directly using their Facebook accounts. The user_omniauth_authorize_path(:facebook) helper was created automatically by devise when you defined your model as :omniauthable. This is why, by the way, you can have only a single model defined as :omniauthable in devise. It normally isn't an issue, although I have seen systems in which there were two different login schemes, one for regular users and one for administrators.

If the user decides to log in as usual, nothing changes. But if the user clicks on “sign in with Facebook”, the whole OAuth system kicks into gear, which means that if you're testing your software on a computer or server that's not identical to what you told Facebook, you'll receive an error indicating that the redirection URL didn't work correctly.

When I clicked on the “sign in with Facebook” link, my browser was redirected to a Facebook page which told me, very simply, that the “helloworld” application would like to get information about me and about my education. This reflected at least part of the scope that I had defined back in devise.rb:

:scope => 'user_about_me,user_education_history,read_friendlists'

But, what about reading my list of friends? Did Facebook somehow forget? Not exactly. After signing in on that initial page, Facebook then gave me a new page, indicating that:

To enhance your experience, helloworld would also like permission
to:

Access my custom friend lists

I clicked on the “Allow” button, and all seemed to be going well, until I got an error message in my browser:

The action 'facebook' could not be found for
Devise::OmniauthCallbacksController

This is because I still need to do two things. First, I need to update my route, such that the Facebook callback will be handled by devise. I can do this by modifying config/routes.rb, such that the original route:

devise_for :user

is replaced by:

devise_for :user, :controllers => {
  :omniauth_callbacks => "users/omniauth_callbacks"
}

This means that when Omniauth is invoked for a callback, I want the users/omniauth_callbacks controller to take care of this. The second thing I must do is define this controller, by putting omniauth_callbacks.rb inside of app/controllers/users, a subdirectory that I must create. I took the definition of this controller straight from the devise documentation:

class Users::OmniauthCallbacksController <
 ↪Devise::OmniauthCallbacksController
  def facebook
    @user = User.find_for_facebook_oauth(env["omniauth.auth"],
    ↪current_user)

    if @user.persisted?
      flash[:notice] = I18n.t "devise.omniauth_callbacks.success", 
      ↪:kind => "Facebook"
      sign_in_and_redirect @user, :event => :authentication
    else
      session["devise.facebook_data"] = env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end
end

In other words, I want to find a user in my database based on the Facebook OAuth information I got back. If I already have such a user in the system, then I sign in as that user. Otherwise, I ask the person to register as a new user.

But of course (and yes, this is the last step), I need to define the method User.find_for_facebook_oauth. Again, the devise documentation offers a solution, in that you can define this class method as follows (with some modifications from the original):

def self.find_for_facebook_oauth(access_token, 
 ↪signed_in_resource=nil)
  data = access_token['extra']['raw_info']

  if user = User.find_by_email(data["email"])
    user
  else # Create a user with a stub password.
    u = User.new(:email => data["email"])
    u.password = Devise.friendly_token[0,20]
    u.save!
    u
  end
end

When you try to log in again via Facebook, you're thus taken to the URL from your OAuth handler, and...well, on my system, I get an error, indicating that my new User instance was not created in the database, because I violated one or more of the validations on my Rails User model. Which ones? The e-mail address and password cannot be blank.

How can this be? Well, you can see that if the user does not exist, then you try to create it with the e-mail address that you got from Facebook, stored in the “data” hash that this method creates:

u = User.new(:email => data["email"])
u.password = Devise.friendly_token[0,20]
u.save!

In order to better understand this, I added the following line to the find_for_facebook_oauth method, just after defining the “data” hash:

logger.warn data.to_yaml

And sure enough, when I tried to log in via Facebook again, I got a lot of information about myself in the controller, but not my e-mail address. That's because the scope that I defined in config/initializers/devise.rb didn't include “email”. You can update that definition:

:scope => 'email,user_about_me,user_education_history,read_friendlists'

Then you have to restart the server (since initializers are read only at startup), and click on the “sign in with Facebook” link again. This time, you're brought back to the Facebook permissions page; Facebook wants to know this time if it's okay to pass the user's e-mail address to the application. I click on the “Allow” button, and then get an error.

Notice that you're basically giving the user a random password here. That's okay in my book, since you don't care about the password for these users, given that they'll be signing in via Facebook in any event.

Conclusion

OAuth has a lot of good things going for it, which benefit both Web-based applications and their users. As you can see, it can be a tad complicated to set up, but once you get the hang of it, adding such functionality to your own applications can be quite straightforward. And, once you've allowed users to sign in via Facebook, you can add additional providers, simply by including additional provider-specific gems, adding a controller class for that provider, and then adding a provider-specific method on the user object. Omniauth, and the various provider-specific gems on which it depends, continues to track the implementation details of OAuth, allowing you to concentrate on your application, rather than the OAuth parts of it.

Reuven M. Lerner is a longtime Web developer, architect and trainer. He is a PhD candidate in learning sciences at Northwestern University, researching the design and analysis of collaborative on-line communities. Reuven lives with his wife and three children in Modi'in, Israel.

LJ Archive