Using GoogleApps as your 'ActiveDirectory'

GoogleApps login for your internal apps

We use hosted services for many of our critical tools at Cloud 66: source control, ticketing (github), Helpdesk (HelpScout), sales (Close.io), transactional emails (Mandrill, Customer.io), mailing lists (Mailchimp) and I guess some more I can't remember now.

We also have 4 custom internal websites that we use for various things.

Recently we moved our entire login and credentials for those systems to GoogleApps. This has great benefits for the teams (less passwords to remember, less 2fa codes), for the admins (automatic granular provisioning of new team members and the ones who move or leave) and a better overview of who's got access to what for everyone.

Here is how we improved it:

What do we have?

Our internal websites are usually either in Rails or Sinatra. In most cases we use devise for authentication, and in some cases we need to have two factor authentication for our internal systems that can be accessed via public internet. Our internal systems were using rotp as their 2fa provider.

Here is a summary:

  • 4 internal websites
  • Powered by Rails or Sinatra
  • Using Devise for user management
  • Using ROTP for 2fa
  • Used by different teams from 2 to 9 members with overlaps between members.

What was the issue?

Although this system was working, we had the following issues:

  • Too many logins / credentials to remember, always increasing risks.
  • Too many 2fa accounts in our Google Authenticators
  • Setup of each new team member when they joined and removing them if they left.
  • Repeat of the same dev work for credentials, roles and access control on each system.

Our GoogleApps setup

Like many others, we use GoogleApps for our email, calendar and some document sharing. This means everyone at Cloud 66 has a GoogleApps account and they are members of different Groups like support, sales, marketing, engineering, ops, etc.

Replacing our setup (Devise + ROTP) allowed us to achieve the following:

  • Enforce 2fa only on certain teams: for example Ops and Devs can be forced to have 2fa.
  • Grant or deny access to team members by just adding them to the right GoogleApps group.
  • Solve all of the problems mentioned above.

Let's get started

Here is a summary of how to switched over:

  • Make sure everyone on your team is a member of the correct Groups.
  • Enforce 2fa on the right groups.
  • Setup GoogleApps access for your application.
  • Replace your login code with GoogleApps login.

Here is the detail of each setup:

Put everyone in the right Groups

If you are using GoogleApps, you have access to Groups. You can setup internal groups and put each member in the right groups, even with heirarchies.

Enforce 2fa

This step is optional. If you want some members of your team to have a higher level of security (for example if they deal with sensitive information, or they should be able to login from the public internet), you can enable 2fa for those groups:

This article about Enforcement tells you all you need to know about enabling 2fa for your groups.

Setup GoogleApps for logins

This was the trickiest part to do. To get the system working, you need to do three things:

  1. Allow your web application to authenticate users in GoogleApp.
  2. Setup a 'service account' so you can retrieve user's team memberships in GoogleApp.

Making your internal app Oauth2 friendly

If you use devise, you can simply use a gem like omniauth-google-oauth2 to add support for Google Oauth2 to your app.

If you are not using devise (which you should!), or on Sinatra you can still use omniauth-google-oauth2 alogside omniauth directly to get it to work.

On Rails, Devise

Add the following line to your Gemfile and run bundle install

gem 'google-api-client'  
gem 'omniauth-google-oauth2'  

omniauth-google-oauth2 provides the Devise strategy needed to use Google as an oauth provider. google-api-client is used to retrieve GoogleApps memeberships via their API.

Add your GoogleApp Client ID and Client Secret (see the next step) to your devise.rb file:

config.omniauth :google_oauth2, 'CLIENT_ID', 'CLIENT_SECRET', {}  

Make sure you have a custom omniauth callback controller. Add this to your routes.rb:

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

Create a file under app/controllers/users called omniauth_callbacks_controller.rb.

Here is what it can look like:

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController  
  def google_oauth2
      @user = User.find_for_google_oauth2(request.env["omniauth.auth"])

      if @user.nil?
        redirect_to home_path
        return
      end

      if @user.persisted?
        Rails.logger.info "User is persisted (existing)"
        flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Google"
        sign_in @user, :event => :authentication
        redirect_to home_path
      else
        Rails.logger.info "User is new"
        # create a new user...
      end
  end
end  

The google_oauth2 action is called by Google when the user is authenticated (successfully) with a payload containing the authentication token and more information like name and email address (this depends on the scope of your request. See Google OAuth2 scopes for more information).

We get the payload and try to find (or create) our user. As you can see this is happening in the find_for_google_oauth2 of the User model:

    def self.find_for_google_oauth2(access_token, signed_in_resource=nil)
        data = access_token.info

        user = User.where(:email => data["email"]).first

        # not found, but it's a ourdomain.com email
        domain = data["email"].split("@").last
        if domain == 'ourdomain.com'
            access, err = check_membership(data["email"], 'engineering@ourdomain.com')
            unless access
                Rails.logger.error "Not a member of the engineering group #{err.message}"
                return nil
            end

            name = data["name"] || data["email"]
            unless user
                Rails.logger.info "New user"
                user = User.new(name: name, email: data["email"], password: Devise.friendly_token[0,20])
                user.save!
            else
                user.oauth_token = access_token.credentials['token']
                user.save!
            end

            return user
        else
            Rails.logger.info "Non ourdomain.com user"
            return nil
        end
    end

This method:

  1. Checks user email’s domain. Let’s say you only allow ourdomain.com users to have access.
  2. Creates a new user if the email domain is right, but it’s not found. This allows automatic provisioning of new users.
  3. Checks user’s team membership and let’s them in if they are a member of a certain group.

The code to check group membership is here (add it to a file under your lib folder):

require 'google/api_client'  
require 'google/api_client/client_secrets'  
require 'google/api_client/auth/installed_app'

def check_membership(email, group)  
    client = Google::APIClient.new(:application_name => 'My Internl App', :application_version => '1.0')
    key = Google::APIClient::KeyUtils.load_from_pkcs12('PATH_TO_P12', 'notasecret')

    client.authorization = Signet::OAuth2::Client.new(
      :token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
      :audience => 'https://accounts.google.com/o/oauth2/token',
      :scope => 'https://www.googleapis.com/auth/admin.directory.group.readonly',
      :issuer => 'SERVICE_ACCOUNT',
      :signing_key => key,
      :person => 'admin@ourdomain.com')
    client.authorization.fetch_access_token!

    begin
        result = client.execute!(:http_method => :get, :uri => "https://www.googleapis.com/admin/directory/v1/groups/#{group}/members/#{email}", :authenticated => true)
        return true, nil
    rescue => exc
        return false, exc
    end
end  

See below as how to create a service account and get the P12 key.

This code uses a service account to check membership of our team members and returns a true or false (as well as a potential exception) based on that. You can improve this by fetching the token once and storing it alongside the refresh token somewhere safe in your app instead of fetching a new one every time.

 On Sinatra, no Devise

If you use Sinatra or not using devise, you can still use this method more or less the same way.

Add the following lines to your Gemfile and run bundle install

gem 'google-api-client'  
gem 'omniauth'  
gem 'omniauth-google-oauth2'  

Create a new action in your app to receive the callback from Google (I'm using DataMapper in this example instead of ActiveRecord)

    get '/auth/:provider/callback' do
        auth = request.env['omniauth.auth']
        data = auth.info

        domain = data["email"].split("@").last
        if domain == 'ourdomain.com' 
            email = data['email']
            user = User.first(:email => email)
            if user.nil?    
                # find the group
                ops, err = check_membership(email, 'ops@ourdomain.com')
                devs, err = check_membership(email, 'devs@ourdomain.com')

                if ops
                    group = :ops
                elsif devs
                    group = :devs
                else
                    group = :everyone
                end

                # create the user

                group = :everyone
                user = User.create(:email => email, :group => group)
            else

                # there is a user. update it
                ops, err = check_membership(email, 'ops@ourdomain.com')
                devs, err = check_membership(email, 'devs@ourdomain.com')

                if ops
                    group = :ops
                elsif devs
                    group = :devs
                else
                    group = :everyone
                end

                user.group = group
                user.save!
            end

            # login (do whatever login means in your app)
            redirect '/'
        else
            flash[:error] = "Invalid login"
            redirect '/'
            return
        end
    end

This example works slightly differently as it checks the membership and creates or updates the user found with the membership of the user in a hierarchy (ops > devs > everyone).

It is up to your app to decided what to do in each case. The check_membership method is the same as with Rails.

Set your GoogleApp Oauth2 provider client id and client secret:

    use Rack::Session::Cookie, :key => 'SOME_NAME',
                               :path => '/',
                               :expire_after => RELOGIN_IN_SECONDS,
                               :secret => 'SESSION_SECRET'

    use OmniAuth::Builder do
      provider :google_oauth2, 'GOOGLE_CLIENT_ID','GOOGLE_CLIENT_SECRET', { provider_ignores_state: ENV['RACK_ENV'] == 'development' }
    end

Note that we are setting provider_ignores_state to falst only for development environment. This allows us to run the app in development without getting the CRSF error while in production the app is still secure.

Now you can replace your login form with a button like this:

<a href="/auth/google_oauth2" class="button expand">Sign in with Google</a>  

Now let's move on to creating the GoogleApp Oauth2 web application:

Setting up your GoogleApps

Setting up a web application access for GoogleApps is easy.

  1. Head to your Google console and choose (or create and choose) a project.
  2. Under APIs & Auth, choose Credentials
  3. Click on the Create new Client ID
  4. Select Web Application, enter your internal application's domain (https://ourdomain.com) and it's OAuth2 callback URL to https://ourdomain.com/auth/google_oauth2/callback
  5. Click on Create client ID

Now you should have the Client ID and Client Secret of a new app.

Creating the Service Account

For our app to check a user's memberships in our domain, we need to create a service account and grant it some admin rights. Here is how:

  1. First, head to your Google console and select your project (same as above will work).
  2. Now under APIs and Auth select Credentials
  3. Click on the Create new Client ID
  4. Select Service Account and click on Create client ID

This will create a service account (an account that can be used between the servers - your servers and Google's). You now need to generate and download a P12 file (there is a button right there for that!). Store this file safely next to your app (don't commit it in your repository). If you are using Cloud 66 to deploy your internal app, you can use cx upload to push the file to your server. Make sure this file is readable by your web application process (on Cloud 66 deployed apps, it's nginx:nginx).

The path of this file should be used in your check_membership method above.

Once you have the service account, you need to grant it the right access rights:

Head to your GoogleApps management console: https://google.com/a/ourdomain.com and select Security.

Now select Advanced Security and then select Manage API client.

Here enter your service account's Client ID and https://www.googleapis.com/auth/admin.directory.group.readonly as scope.

One important note on check_membership method is the :person parameter. Make sure the value for that is set to a user account with administrator rights on your domain under GoogleApps.

Summary

This article shows how to replace your internal applications' login and access right management with GoogleApps if you use GoogleApps for other things like email and calendars. This should make your life easier in managing your team members' access rights as well as your team members' login to the internal systems is made much simpler.

Try Cloud 66 for Free, No credit card required