Editor's Note: After you read this article, check out the Cloudspokes challenge for merging the databasedotcom-oauth2 gem into the databasedotcom gem.

Ruby and Heroku are powerful tools that can be leveraged by Force.com and Database.com developers to build compelling apps for the enterprise. A significant barrier to entry however, is user authentication — something that's inherent and easy to take for granted when using Visualforce. This article explains how to use the databasedotcom-oauth2 gem to remove that barrier, and includes a step-by-step tutorial perfect for anyone new to Ruby and Heroku.

What is databasedotcom-oauth2?

  • an extension of the databasedotcom gem that simplifies authentication and authorization with Force.com or database.com for Ruby web apps via OAuth 2.0
  • a Ruby gem intended to run as Rack Middleware
  • an alternative to using OmniAuth and the corresponding omniauth-salesforce gem.

When and Why Should I Use databasedotcom-oauth2 Instead of OmniAuth?

Many Ruby web apps integrated with Force.com need more than just identification — they also need to interact with Force.com via the databasedotcom gem. Both OmniAuth and databasedotcom-oauth2 provide identification; however, databasedotcom-oauth2 makes the interaction part easier.

Specifically, databasedotcom-oauth2:

  • allows multiple Force.com endpoints (production, sandbox, etc.)
  • supports configuration of scope, display, and immediate OAuth 2.0 parameters
  • supports My Domain
  • maintains an encrypted OAuth 2.0 token in whatever session store you choose (Cookie, Pool, etc)
  • materializes a databasedotcom client upon each request (using the token in session)
  • provides a mixin for your app containing utility methods like unauthenticated?, client, etc.

How Does databasedotcom-oauth2 Work?

In a sentence, the gem works by intercepting and filtering requests.

Take a look at the image below. When an unauthenticated request is made for a protected resource /protected/resource, your app is required to redirect the user to /auth/salesforce (1). (Don't worry about "how" for now, it's covered in the tutorial below.) The /auth/salesforce URL is a signal to databasedotcom-oauth2 to start an OAuth handshake with salesforce (2), specifically the OAuth 2.0 Web Server Flow (the gray box). Once the handshake is complete, databasedotcom-oauth2 will instantiate a Databasedotcom::Client, serialize it, encrypt it, and then finally store it as a session variable (3). Next, the user is redirected to their original request /protected/resource. Before the request reaches your app, however, the Databasedotcom::Client instance is retrieved from it's session variable, decrypted, de-serialized, and made available (4). After your app processes the request and returns a response, Step 3 is once again repeated (5). This is done in case the OAuth token was refreshed. Each subsequent request to /protected/resource will repeat Steps 4 & 5 but not the OAuth handshake.

Db-oauth2-sequence.png

Step-by-Step Tutorial

In this tutorial, you build a simple Ruby web app that utilizes Sinatra and the databasedotcom-oauth2 gem, deploy the app to Heroku, and then lastly for extra credit, re-create the app using Rails instead of Sinatra. If you're new to Ruby and Heroku, don't worry — this guide outlines each and every step required. In fact, this guide is a great way to start learning how to leverage the power of Ruby and Heroku with Force.com.

Prerequisites

Step 1: Create the Project Files

Start by creating the bare-bones project files (later, you update these files iteratively to add more functionality).

  1. Open a command prompt.
  2. Create a new local folder named salesforce.
    mkdir -p /apps/salesforce
    cd /apps/salesforce
    
    Note: The rest of the tutorial assumes the full path of this folder is /apps/salesforce.
  3. In your editor of choice, create a new file named app.rb under the salesforce folder with the contents below (hover over top right corner to copy raw contents without line numbers).
    require "sinatra/base" #include Sinatra web framework
    class App < Sinatra::Base
      # catch-all route, simply return text Hello World
      get '/*' do
        "Hello World!"
      end
    end
  4. Create a new file named config.ru (not config.rb). This file executes upon web server startup and is responsible for starting your app. Note the first line which tells the web server to start on port 5000.
    #\ -w -p 5000
    require './app'
    run App.new
    
  5. Create a new file named Gemfile (the file should NOT have an extension) with the contents below. This file defines the code libraries (or gems, as their known in Ruby) upon which your project depends. Here's a quick description of each gem: thin is the web server, and sinatra is a web framework.
    source "http://rubygems.org"
    gem "thin"
    gem "sinatra"
    gem "databasedotcom-oauth2"
    
  6. Switch back to your command prompt.
  7. Install the bundler gem and project dependencies by executing the commands below.
    gem install bundler
    bundle install
    
  8. Start your app via following command:
    rackup
    
  9. Open a web browser and go to http://localhost:5000. You should see something similar to the following screen.

Hello world.png

Step 2: Configure Environment Variables

In this section, you populate several environment variables that the databasedotcom-oauth2 gem requires.

  1. Update the file app.rb to the following so that the app checks for presence of the three environment variables. You haven't populated them yet but will soon.
    require "sinatra/base"
    class App < Sinatra::Base
      # check for existence of environment variables
      VARS=%w(TOKEN_ENCRYPTION_KEY CONSUMER_KEY CONSUMER_SECRET)
      VARS.keep_if{|var| ENV[var].nil? || ENV[var].empty?}
      fail "Environment Variables required: #{VARS.join(',')}" if(!VARS.empty?)
      get '/*' do
        "Hello World!"
      end
    end
    
  2. Go back to the command prompt and stop the web server (CTRL+C).
  3. Restart your app.
    rackup
    

    The expected result is the following error.

    /apps/salesforce/app.rb:6:in `<class:App>': Environment Variables required: ENCRYPTION_KEY,CONSUMER_KEY,CONSUMER_SECRET (RuntimeError)
  4. Log in to a Force.com sandbox or Developer Edition (DE) org and continue to the next step. If you don't have an org, create a free Force.com DE org, then use the confirmation email to sign in and set your initial password.
  5. Click <your name> | Setup to move to the Force.com Setup environment.
  6. Click App Setup | Develop | Remote Access | New to configure a new remote access configuration.
  7. Fill in required fields with the following values, then click Save:
    - Application: localhost:5000
    - Callback: http://localhost:5000/auth/salesforce/callback
    - Contact Email: <your email>
  8. Make note of Consumer Key and Secret in the Authentication section.
  9. Go back to the command prompt and execute the following commands (replace <REPLACE ME> with appropriate values).
    export CONSUMER_KEY=<REPLACE ME>
    export CONSUMER_SECRET=<REPLACE ME>
    
    Note: Keep this window open.
  10. Next, generate a random encryption key.
    ruby -ropenssl -rbase64 -e "puts Base64.strict_encode64(OpenSSL::Random.random_bytes(16).to_str)"
    
  11. Set the TOKEN_ENCRYPTION_KEY environment variable to the output of the previous command.
    export TOKEN_ENCRYPTION_KEY=<REPLACE ME>
    
  12. Restart your app and note that the errors no longer exist.
    rackup
    
  13. Log out of your Force.com org (this is important): Click <your name> | Logout

Step 3: Add OAuth 2.0 Authentication and Login

  1. Update app.rb to the following (see inline comments for additions):
    require "sinatra/base"
    require "base64"                # needed to decode encryption key
    require "databasedotcom-oauth2" # adds class Databasedotcom::OAuth2::WebServerFlow
    
    class App < Sinatra::Base
    
      # check for existence of environment variables
      VARS=%w(TOKEN_ENCRYPTION_KEY CONSUMER_KEY CONSUMER_SECRET)
      VARS.keep_if{|var| ENV[var].nil? || ENV[var].empty?}
      fail "Environment Variables required: #{VARS.join(',')}" if(!VARS.empty?)
    
      # Following are Rack Middleware.Basically they intercept all 
      # requests & responses and modify them accordingly.
      use Rack::Session::Cookie # use cookie as persistent store for session
      use Databasedotcom::OAuth2::WebServerFlow,
        "token_encryption_key" => Base64.strict_decode64(ENV['TOKEN_ENCRYPTION_KEY']),
        "display" => "touch", # will force salesforce login to be optimized for touch
        "endpoints" => {"login.salesforce.com" => {"key" => ENV['CONSUMER_KEY'], 
                                                   "secret" => ENV['CONSUMER_SECRET']}}
    
      get '/*' do
        # check if client variable is not nil; if it isn't, then user has logged in.
        if env['databasedotcom.client']
          "Woo-hoo - You're logged in!"
        else
          # Note the /auth/salesforce URL below will be intercepted by the 
          # databasedotcom-oauth2 gem redirected to the salesforce.com login.
          <<-RESPONSE
            You're not logged in. Click <a href="/auth/salesforce">here</a> to login.
            RESPONSE
        end
      end
    end
    
  2. Restart your app:
    rackup
    
  3. Open a web browser and go to http://localhost:5000. You should see something similar to the following screen. Not logged in.png
  4. Click the link to login. The salesforce login screen (optimized for touch devices) appears. Enter your credentials and click Login. Touch login.png
  5. Your browser should automatically redirect back to your app and the following should appear: Logged in.png

Step 4: Add Logout

It's great that your app can authenticate users, but what about logout?

  1. Update app.rb to following:
    require "sinatra/base"
    require "base64"                # needed to decode encryption key
    require "databasedotcom-oauth2" # adds class Databasedotcom::OAuth2::WebServerFlow
    
    class App < Sinatra::Base
    
      # check for existence of environment variables
      VARS=%w(TOKEN_ENCRYPTION_KEY CONSUMER_KEY CONSUMER_SECRET)
      VARS.keep_if{|var| ENV[var].nil? || ENV[var].empty?}
      fail "Environment Variables required: #{VARS.join(',')}" if(!VARS.empty?)
    
      # Following are Rack Middleware.Basically they intercept all 
      # requests & responses and modify them accordingly.
      use Rack::Session::Cookie # use cookie as persistent store for session
      use Databasedotcom::OAuth2::WebServerFlow,
        "token_encryption_key" => Base64.strict_decode64(ENV['TOKEN_ENCRYPTION_KEY']),
        "display" => "touch", # will force salesforce login to be optimized for touch
        "endpoints" => {"login.salesforce.com" => {"key" => ENV['CONSUMER_KEY'], 
                                                   "secret" => ENV['CONSUMER_SECRET']}}
    
      # Clears rack session.
      get '/logout' do
        env["databasedotcom.client"].logout unless env["databasedotcom.client"].nil?
        redirect to("/")
      end
    
      get '/*' do
        # check if client variable is not nil; if it isn't, then user has logged in.
        if env['databasedotcom.client']
          <<-RESPONSE
            You're logged in. Click <a href="/logout">here</a> to logout.
            RESPONSE
        else
          # Note the /auth/salesforce URL below will be intercepted by the 
          # databasedotcom-oauth2 gem redirected to the salesforce.com login.
          <<-RESPONSE
            You're not logged in. Click <a href="/auth/salesforce">here</a> to login.
            RESPONSE
        end
      end
    end
    
  2. Restart your app:
    rackup
    
  3. Reload http://localhost:5000 in your browser. If you successfully logged in during the previous step, you should see something similar to the following screen without having to log in again, even though your your app was restarted. That's because your OAuth token is stored as an encrypted cookie. Logout.png
  4. Click the logout link and you should see: Not logged in.png

Step 5: Utilize Mixin

Now add several helper methods to the app via the Mixin that the databasedotcom-oauth2 gem provides. If you're unfamiliar with Mixins, it's basically a inheritance concept that distinctly differs from extending another class. See this article for more info.

  1. Update app.rb to the following:
    require "sinatra/base"
    require "base64"                # needed to decode encryption key
    require "databasedotcom-oauth2" # adds class Databasedotcom::OAuth2::WebServerFlow
    
    class App < Sinatra::Base
    
      # check for existence of environment variables
      VARS=%w(TOKEN_ENCRYPTION_KEY CONSUMER_KEY CONSUMER_SECRET)
      VARS.keep_if{|var| ENV[var].nil? || ENV[var].empty?}
      fail "Environment Variables required: #{VARS.join(',')}" if(!VARS.empty?)
    
      # Following are Rack Middleware.Basically they intercept all requests & responses
      # and modify them accordingly.
      use Rack::Session::Cookie # use cookie as persistent store for session
      use Databasedotcom::OAuth2::WebServerFlow,
       "token_encryption_key" => Base64.strict_decode64(ENV['TOKEN_ENCRYPTION_KEY']),
       "display" => "touch", # will force salesforce login to be optimized for touch
       "endpoints" => {"login.salesforce.com" => {"key" => ENV['CONSUMER_KEY'], 
                                                  "secret" => ENV['CONSUMER_SECRET']}}
    
      # mixes in client, me, authenticated?, etc.
      include Databasedotcom::OAuth2::Helpers
    
      # Clears rack session.
      get '/logout' do
        client.logout if authenticated?
        redirect to("/")
      end
    
      get '/*' do
        # check if user is authenticated; authenticated? and me are added 
        # to this app via the Databasedotcom::OAuth2::Helpers Mixin
        if authenticated?
          <<-RESPONSE
            You're logged in as #{me.username}. Click <a href="/logout">here</a> to logout.
            RESPONSE
        else
          # Note the /auth/salesforce URL below will be intercepted by the 
          # databasedotcom-oauth2 gem redirected to the salesforce.com login.
          <<-RESPONSE
            You're not logged in. Click <a href="/auth/salesforce">here</a> to login.
            RESPONSE
        end
      end
    end
    
  2. Restart your app:
    rackup
    
  3. Reload http://localhost:5000 in your browser. Repeat the login flow. When you're done, your browser should automatically redirect back to your app, which now displays your username: Username.png

Step 6: Deploy to Heroku

Now that you have a basic app, why not deploy it to Heroku?

  1. Sign up for Heroku (free) if you haven't done so already.
  2. Let Heroku know who you are. From the command prompt, run the following command and enter your credentials.
    heroku login
    
  3. List the rack-ssl gem as a dependency for your project. rack-ssl is necessary to make your Heroku app accessible over https. Update your Gemfile to:
    source "http://rubygems.org"
    gem "rack-ssl"
    gem "thin"
    gem "sinatra"
    gem "databasedotcom-oauth2"
    
  4. Install the rack-ssl gem via bundler. Switch to your command prompt, stop the web server (CTRL+C), and execute:
    bundle install
    
  5. Update app.rb to use rack-ssl:
    require "sinatra/base"
    require "base64"                # needed to decode encryption key
    require "databasedotcom-oauth2" # adds class Databasedotcom::OAuth2::WebServerFlow
    require "rack/ssl" unless ENV['RACK_ENV'] == "development" # only utilized when deployed to heroku
    
    class App < Sinatra::Base
    
      # check for existence of environment variables
      VARS=%w(TOKEN_ENCRYPTION_KEY CONSUMER_KEY CONSUMER_SECRET)
      VARS.keep_if{|var| ENV[var].nil? || ENV[var].empty?}
      fail "Environment Variables required: #{VARS.join(',')}" if(!VARS.empty?)
    
      # Following are Rack Middleware.Basically they intercept all requests & responses
      # and modify them accordingly.
      use Rack::SSL unless ENV['RACK_ENV'] == "development"  # only utilized when deployed to heroku
      use Rack::Session::Cookie # use cookie as persistent store for session
      use Databasedotcom::OAuth2::WebServerFlow,
        "token_encryption_key" => Base64.strict_decode64(ENV['TOKEN_ENCRYPTION_KEY']),
        "display" => "touch", # will force salesforce login to be optimized for touch
        "endpoints" => {"login.salesforce.com" => {"key" => ENV['CONSUMER_KEY'], 
                                                   "secret" => ENV['CONSUMER_SECRET']}}
    
      # mixes in client, me, authenticated?, etc.
      include Databasedotcom::OAuth2::Helpers
    
      # Clears rack session.
      get '/logout' do
        client.logout if authenticated?
        redirect to("/")
      end
    
      get '/*' do
        # check if user is authenticated; authenticated? and me are added 
        # to this app via the Databasedotcom::OAuth2::Helpers Mixin
        if authenticated?
          <<-RESPONSE
            You're logged in as #{me.username}. Click <a href="/logout">here</a> to logout.
            RESPONSE
        else
          # Note the /auth/salesforce URL below will be intercepted by the 
          # databasedotcom-oauth2 gem redirected to the salesforce.com login.
          <<-RESPONSE
            You're not logged in. Click <a href="/auth/salesforce">here</a> to login.
            RESPONSE
        end
      end
    end
    
  6. Create a git repository for your app and commit. Switch to your command prompt and execute:
    git init
    git add .
    git commit -m "init"
    
  7. Create a Heroku app by executing the command below. Note the URL of the app that's created, which should be something like <code>http://empty-leaf-2058.herokuapp.com/</code>.
    heroku create
    
  8. Deploy the app to Heroku:
    git push heroku master
    
  9. Open a web browser and go to your Heroku app's URL. You should see ... an error? Yes! Sometimes, issues arise when moving an app from a local environment to Heroku. This is a great opportunity to learn some debugging tips. Heroku error.png
  10. To debug the issue, switch to your command prompt and execute:
    heroku logs
    
  11. In the log, about 15 lines up, you should see something similar to the following: 2012-07-12T22:23:48+00:00 app[web.1]: /app/app.rb:11:in `<class:App>': Environment Variables required: TOKEN_ENCRYPTION_KEY,CONSUMER_KEY,CONSUMER_SECRET (RuntimeError) Ahh, that error means there's no environment variables over on Heroku. But before getting that straightened out, you need to first generate a new remote access configuration in your org because the OAuth callback URL for the app running on Heroku is unique.
  12. Repeat Steps 2.4-2.8 above, but this time, use the following values when you create the new remote access configuration:
    - Application: <REPLACE ME>.herokuapp.com
    - Callback: https://<REPLACE ME>.herokuapp.com/auth/salesforce/callback
    - Contact Email: <your email>
  13. Using the Consumer Key and Secret for the new remote access configuration, execute the following commands to set the remote environment variables on Heroku.
    heroku config:add CONSUMER_KEY=<REPLACE ME>
    heroku config:add CONSUMER_SECRET=<REPLACE ME>
    
  14. Next, generate a random encryption key:
    ruby -ropenssl -rbase64 -e "puts Base64.strict_encode64(OpenSSL::Random.random_bytes(16).to_str)"
    
  15. Set the TOKEN_ENCRYPTION_KEY environment variable to the output of the previous command:
    heroku config:add TOKEN_ENCRYPTION_KEY=<REPLACE ME>
    
  16. Access your app again. You should see something like following: Heroku not logged in.png And you're done! Congratulations, you've built a simple Ruby app and deployed it to Heroku.

Extra Credit: Re-create using Rails instead of Sinatra!

In this section, you'll quickly re-create the above app using Rails instead of Sinatra. This section assumes you have a basic understanding of Rails, and Rails is already installed on your local machine.

  1. Open a command prompt and execute the following commands. This will create a new Rails app named salesforce-rails.
    cd /apps
    rails new salesforce-rails
    cd salesforce-rails
    
  2. Generate a single "welcome" controller.
    rails generate controller Welcome index
    
  3. Replace app/controllers/welcome_controller.rb with:
    class WelcomeController < ApplicationController
      include Databasedotcom::OAuth2::Helpers
      
      def index
        @authenticated = authenticated?
        @me = me if authenticated?
      end
    
      def logout
        client.logout if authenticated?
        render "index"
      end
    end
    
  4. Replace app/views/welcome/index.html.erb with:
    <% if @authenticated%>
    You're logged in as <%= @me.username%>. Click <a href="/logout">here</a> to logout.
    <% elsif%>
    You're not logged in. Click <a href="/auth/salesforce">here</a> to login.
    <% end%>
    
  5. Remove the default home screen by executing the following command.
    rm public/index.html
    
  6. Replace config/routes.rb with below code. Essentially this means all requests will be routed to the index and logout properties on the welcome controller.
    SalesforceRails::Application.routes.draw do
      #only did symbol this way because of syntax highlighter copy/paste issue
      root 'to'.to_sym => 'welcome#index'
      match "logout" => "welcome#logout"
      match "*rest" => "welcome#index"
    end
    
  7. Add following to Gemfile:
    gem "databasedotcom-oauth2"
    
  8. Insert the following code inside of the Application class' definition contained in config/application.rb. Also, please execute Steps 2.4 thru 2.11 if you have not previously done so.
    VARS=%w(TOKEN_ENCRYPTION_KEY CONSUMER_KEY CONSUMER_SECRET)
    VARS.keep_if{|var| ENV[var].nil? || ENV[var].empty?}
    fail "Environment Variables required: #{VARS.join(',')}" if(!VARS.empty?)
    require "base64"
    require "databasedotcom-oauth2"
    config.middleware.use Databasedotcom::OAuth2::WebServerFlow,
      "token_encryption_key" => Base64.strict_decode64(ENV['TOKEN_ENCRYPTION_KEY']),
      "display" => "touch", # will force salesforce login to be optimized for touch
      "endpoints" => {"login.salesforce.com" => {"key" => ENV['CONSUMER_KEY'], 
                                                 "secret" => ENV['CONSUMER_SECRET']}}
    
  9. Start the rails web server on port 5000 by executing the following command.
    $ rails server -p 5000
    
  10. Open a web browser and go to http://localhost:5000. You should see something similar to the following screen. Not logged in.png
  11. Click the link to login. The salesforce login screen (optimized for touch devices) appears. Enter your credentials and click Login. Touch login.png
  12. Your browser should automatically redirect back to your app and the following should appear: Username.png
  13. Double congratulations!! You've just re-built a Ruby web app to use Rails instead of Sinatra!

References

About the Author

Richard Vanhook is a Technical Solution Architect at salesforce.com with ten years experience building enterprise applications.