Lets Devise a Google oAuth Login with Rails

By ozzyaaron

Friday, Aug 21, 2015

Step by Step Google oAuth Login using Devise and Omniauth

We just rolled out a feature allowing our users to login using their Google accounts, nothing revolutionary but certainly not without its pitfalls. Just to be up front we still have a small issue to fix for users that are logged into multiple Google accounts and have to choose a Google account whilst logging in, however these are the steps we took to get to a workable solution.

We found this information difficult or impossible to find and ended up reading much source code and documentation, I’m hoping that this might help a few people in the future and perhaps illicit some improvements from the community!

Login Screen

Assumptions

So let me just say that I’m going to assume that you can install the gems and have an already working solution with Devise. This blog post is about taking your working application using Devise and enabling oAuth login, specifically with Google as the oAuth provider.

I also assume that you know how to setup your own oAuth credentials as this step might vary but is necessary for any oAuth provider.

What We’ll Cover

  1. Setup Devise with OmniAuth and a Google OmniAuth strategy
  2. Information on testing this effectively
  3. Handling failure
  4. Pitfalls

Setting Everything Up

We’re going to be using the https://github.com/zquestz/omniauth-google-oauth2 gem as it is a strategy for a well tested oAuth gem OmniAuth. The documentation for this gem isn’t strictly correct and whilst it works pretty well you’ll need to ignore some instructions.

Devise is the piece you’ll be mainly interacting with as it will essentially hand off to OmniAuth to handle the oAuth process after which authentication, session management and so on becomes Devise’s responsibility. There is some confusing documentation around where configuration goes but it all goes into the Devise configuration. Devise then has an integration with OmniAuth to perform the oAuth process, you configure Devise and it controls OmniAuth for you (kind of).

Configuring Devise

Find your devise initialiser and put something like this in there:

Devise.setup do |config|
  config.omniauth :google_oauth2, ENV["GOOGLE_OAUTH_CLIENT_ID"], ENV["GOOGLE_OAUTH_SECRET"], {
    scope: "email"
  }
end

The scope just tells Google what the scope of your request is. When you login using Facebook and it tells you the app is requesting certain details from you, this is the scope of the request. Here we just want the user’s email address so that’s all we ask for. You can ask for their Google Plus profile but good luck handling that NilClass exception.

The documentation might tell you to do this via OmniAuth configuration directly; that is wrong.

Prepping Your User Records

We currently only support one oAuth provider but extending it is incredibly simple in the future. The short story is that you’ll want to add a way to find users based on the provider type and their unique ID for your user. Every system will tend to have a unique key for a user and oAuth providers are no different, you’ll want to store this mapping for retrieval later.

Essentially you’ll want to do be able to do something like:

User.find_by_oauth({provider: "google", provider_user_id: "2828282AHHD8382", email: "rojer.ramjet@example.com"})

That hash is unlikely to look like that but you hopefully get the gist. Best practice would likely dictate that you have a join table with provider credentials as the data goal.

class User
  has_and_belongs_to_many :oauth_credentials
end

The goal is to be able to store the data such that you can retrieve a user from an oAuth provider’s response.

The Callbacks Controller

The way that oAuth works is that once the provider has reached a point where it should hand-off control then it will callback to your application using the URL you specified when setting up your credentials. For that reason you’ll need a controller to handle this and a route to it.

Personally I couldn’t find a lick of documentation about this step so reading code got me to this:

  devise_for :users, only: :omniauth_callbacks, controllers: { omniauth_callbacks: "users/omniauth_callbacks" }

This is just saying that omniauth_callbacks should be handled by the Users::OmniauthCallbacks controller.

In this controller you’ll want a method for your provider, in our case that means a method google_oauth2.

  def google_oauth2
    user = ::User.from_omniauth(oauth_response)

    if user.persisted?
      flash[:notice] = I18n.t("devise.omniauth_callbacks.success", kind: provider)
      sign_in_and_redirect user, event: :authentication
    else
      session["devise.google_data"] = oauth_response.except(:extra)
      params[:error] = :account_not_found
      do_failure_things
    end
  end

This is all pretty basic except the gotchya around removing rubbish from the oAuth response before injecting the necessities into the session for Devise.

First we’ll attempt to find the user based on the oAuth response. What we’re interested in here is the provider, email address and Google’s User ID field in the oAuth response. It’s up to you how you’d like to handle this but I’d suggest that the method can fallback to a lookup on email which is available in the oAuth response. Then you will want to update the user with the details in the response so you can do straight lookups from the oAuth data in the future. At this point we’re just getting into basic oAuth which is outside the scope of this post.

The main takeaway here is that you will need to strip the junk out of any oAuth provider’s response (Twitter also has a huge response) otherwise you’ll likely hit cookie limits on subsequent login attempts via oAuth. I’ll cover how to test this later.

Automated Testing

At first performing automated testing of an oAuth solution seems difficult, and it would be if it weren’t for OmniAuth being so great. It really is as simple as stepping through the process and recording the response from your provider. OmniAuth then allows you to stub a response:

  before do
    OmniAuth.config.test_mode = true

    OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new({
      provider: provider,
      uid: uid,
      info: {
        name: "Google User",
        email: google_email,
        first_name: "Google",
        last_name: "User",
        image: ""
      },
      credentials: {
        token: "token",
        refresh_token: "another_token",
        expires_at: 1354920555,
        expires: true
      },
      extra: {
        id_token: 1000.times.map { "string" }.join, # this huge chunk is used to test for CookieOverflow exception
        raw_info: OmniAuth::AuthHash.new(
          email: "test@example.com",
          email_verified:"true",
          kind:"plus#personOpenIdConnect",
          name:"Test Person",
        )
      }
    })
  end

  after do
    OmniAuth.config.test_mode = false
  end

What this means is that your callback controller will receive this stubbed response after the fake oAuth callback phase has finished. I’ve cut out some of the detail but these are the essentials of a Google oAuth response.

Wait, why are we adding a huge string into the extra component? Well that simulates a large response from the provider and we have a test to check that cookie size doesn’t explode from subsequent oAuth attempts. You’ll have seen in our callback controller we completely strip extra from the oAuth response.

Handling Failure

The next thing you might run into is how to handle special cases in failure. For this Devise allows you to provide what is essentially an error handler. These errors are really Devise errors at this point and are outside the oAuth process. Things you might handle here would be a user that can oAuth properly but their local user account is not able to login - for example an unconfirmed user.

Devise.setup |config|
  config.warden do |manager|
    manager.failure_app = AuthenticationFailureController
  end
end

This also allows you to handle failures in different ways. In our case one thing we do is automatically send unconfirmed users a new confirmation email rather than have them click a link again.

Pitfalls

Hopefully this covers the main issues that we ran into, the lack of correct documentation was the main pitfall. After we figured out that the configuration was done via Devise and that the callback route needed to be defined in the way we did then it worked pretty well.

One issue is that during development you might want to attach your failure handler outside of an initialiser. You might notice that the initialiser will hold a reference to the class when it is evaluated. Just because of the way Rails boots, once it has started you won’t get changes to your failure handler loaded. This makes tests important!

Also failure is handled differently in development to other environments so you’ll want to directly specify a failure handler if you want to test this stuff as if it is real life. There are other small changes in how the oAuth gems behave in development which has made debugging problematic.

Post Mortem

After watching the logs from the instrumentation it was easy to see that users were being redirected back to the Google Account Chooser after logging in. The fix was to store the redirect path in session rather than use Devise’s standard session location. It seemed to overwrite it and after toying with Devise’s quirks for quite a while I decided to self-manage this part of the flow. After that it was simple and everything works beautifully.

We’ve since started working on our own OAuth2 provider that houses other OAuth strategies and the above solution has worked really well.

Le End

I hope this helped someone out there as it took us a while to piece together how to get this working. We now have a well tested and robust oAuth login solution and hope to add more providers really soon. So far not an exception or bug report raised.