How to do Google Apps SSO in Ruby

March 5, 2013 Will Read

Google has a ton of APIs, and a fistful of authentication methods to match – everything from 3-legged OAuth2 to proprietary protocols like AuthSub. I’m in the middle of building out a One-click installable Google App for Enterprise/Education (ya know, when your company uses their domain but you get all the niceness of GMail and related Google tools) but I found the Ruby documentation lacking and even misleading. Furthermore, the existing gems that are available are too often focused on non-Google Apps development or return you an unparsed Atom feed, barely giving you any functionality beyond making a Net::HTTP request yourself. Despite all this, I’ve persevered, and what follows is the summation of my findings.

First off, make a new app in the Google Apps Marketplace. The documentation on this is pretty decent. It involves creating a couple of XML files. The openIdRealm is relevant. I set mine to my production URL and had no trouble in development (localhost:3000). What is important though is that your openIdRealm is different from the domain that you’re using to install the app. Initially, I was using loopb.ac as my openIdRealm (and thus my production URL), and it was the same domain I was using for GMail. I ran in to timeout issues when I went to production. I changed my openIdRealm and production URL to app.loopb.ac and things started working smoothly again.

Once you’ve got an app (no need to submit it for approval yet) you can get your OAuth credentials.

OAuth credentials

This is where you get the OAuth credentials for your app

After you click the link, you’ll want to grab the “Consumer Key” and the “Consumer Key Secret”.

You’re going to be using 2-legged OAuth. This is a bit different than if you’ve used Twitter for SSO. In Twitter’s case you’re most likely using 3-legged OAuth to act on behalf of the individual user. In the case of Google Apps, you’re asking for privileges when the domain admin (aka the IT person at example.com) when s/he installs your app for all of the users in the domain. That way, you don’t have to make each user in the domain decide if it is okay for your app to read their calendar appointments.

You’re going to need two gems, oauth, and omniauth-google-apps. I didn’t seem to need the oauth gem till I deployed to Heroku.

gem 'oauth'
gem 'omniauth-google-apps'

Then, configure omniauth. Make a new file, config/initializers/omniauth.rb with the following contents:

require 'openid/store/filesystem'
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :google_apps, :store => OpenID::Store::Filesystem.new('./tmp')
end

The store option is… optional. But if you’re going out to Heroku you’ll want to include it and make sure that the path is ./tmp so as to work with Heroku’s read-only file system.

You can implement a pretty standard looking ApplicationController. See the gist at the end for an example. Basically to sign in you’ll need to redirect the user to /auth/google_apps. You’ll also want a callback endpoint for Google to hit. In your routes.rb:

match "/auth/:provider/callback" => "sessions#create"

Fill in your sessions controller to handle the information in the params and sign the user in. There’s an example in the gist. Next, we’ll need a client class to talk to Google’s APIs. Nothing too unexpected here, but this is where the ID and Secret are used – since the domain admin approved your app, you use the app’s credentials, not the user’s to access information. All of Google’s APIs that I’ve needed return Atom feeds, so this’ll parse them in to REXML docs, or raise a 500 if something went wrong.


#lib/google/client.rb
require 'oauth'
require 'rexml/document'

module Google

  class Client
    attr_accessor :version

    def self.get(base, query_parameters, version = '2.0')
      make_request(:get, url(base, query_parameters), version)
    end

    private

    def self.make_request(method, url, version)
      oauth_consumer = OAuth::Consumer.new(GOOGLE_APP_ID, GOOGLE_APP_SECRET)
      access_token = OAuth::AccessToken.new(oauth_consumer)

      response = access_token.request(method, url, {'GData-Version' => version})
      if response.is_a?(Net::HTTPFound)
        return make_request(method, response['Location'], version)
      end
      return unless response.is_a?(Net::HTTPSuccess)
      feed = REXML::Document.new(response.body)
      throw :halt, [500, "Unable to query feed"] if feed.nil?
      feed
    end

    def self.url(base, query_parameters={})
      url = base
      unless query_parameters.empty?
        url += '?'
        query_parameters.each { |key, value| url += "#{CGI::escape(key)}=#{CGI::escape(value)}&" }
        url.chop!
      end
      url
    end
  end
end

Great, so how about we get a user’s private contacts? No problem! Here’s the Contact class that uses our client:


#lib/google/contact.rb
module Google
  class Contact
    attr_accessor :full_name, :first_name, :last_name, :email, :company, :title, :notes

    def self.all(email)
      feed = Google::Client.get("https://www.google.com/m8/feeds/contacts/default/full", {
          'xoauth_requestor_id' => email
      }, '3.0')

      feed.elements.collect('//entry') do |entry|
        new(
            :full_name => entry.elements["gd:name"].elements["gd:fullName"].text,
            :first_name => entry.elements["gd:name"].elements["gd:givenName"].text,
            ...
        )
      end
    end

    def initialize(options = {})
      @full_name = options[:full_name]
      @first_name = options[:first_name]
      ...
    end
  end
end

Modstly a lot of boilerplate to pull out the meaningful attributes form the XML. The one thing to notice is the query param we pass to the client,

'xoauth_requestor_id' => email

In the case of a lot of the Google Data APIs, we need this parameter so the API knows who’s contacts we’re going after. For example, a Calendar class would look almost identical save the differing XML fields. Don’t get too comfortable though, what you probably want is to get a list of all users in this domain, not the personal contacts of a particular user. In that case you’ll want to use the Google Provisioning API. At first read of the docs, you see that it too supports the 2-legged auth that we used for contacts, but if you make a carbon copy you’re going to get an “Invalid Token” error. Uhhh, what? Turns out, the xoauth_requestor_id is not only not needed, it’ll cause problems if you include it. The email address isn’t required at all, which makes sense as you’re getting all the users in a domain and don’t really care who the current user is:


#lib/google/user.rb
module Google
  class User
    attr_accessor :login, :first_name, :last_name
    def self.all(domain)
      feed = Google::Client.get("https://apps-apis.google.com/a/feeds/#{domain}/user/2.0", {
          'get' => 'all'
      })
      feed.elements.collect('//entry') { |e| new_from_entry(e) }
    end

    def initialize(options ={})
      ...
    end

    private

    def self.new_from_entry(entry)
      new(
          ...
      )
    end
  end
end

There you have it. Hopefully I’ve saved you the countless hours of reading documentation, Googling for “Google”, and beating your head against any vertical surface as I did. Happy App development!

See the full gist here. Comments and pull requests welcome!

About the Author

Biography

Previous
Big Data Analytics for Network Security Monitoring
Big Data Analytics for Network Security Monitoring

After years of enterprise security breaches, one would think companies have learned much and are improving ...

Next
Tracker Ecosystem: Tracker Tracker – cross project visibility and panel customization
Tracker Ecosystem: Tracker Tracker – cross project visibility and panel customization

Tracker Tracker is an open source web app that allows you to see and work with stories from across multiple...