Problem
You're writing a Ruby application, using the Sinatra web framework, and you want to call the REST API to create, read, update and/or delete Force.com records.
Solution
Prerequisites
The sample uses the OAuth2 library and the Sinatra web application framework to implement a minimal Ruby web application able to obtain an OAuth 2.0 access token and interact with the Force.com REST API. The full source code is available in the Sinatra-Force.com-Heroku GitHub project.
The Force.com OAuth2 implementation will work with a non-SSL redirect URI for localhost ONLY. This is fine for development, but for production you will need to either configure an SSL reverse proxy in front of the web app, or deploy the code to a cloud platform such as Heroku with an SSL addon. The latter is much easier, but, if you want to configure this run locally, any one of a range of HTTP servers could be used; popular choices include Apache Web Server, lighttpd and nginx. I used nginx, following these instructions to install nginx with SSL support on the Mac and these instructions to configure nginx as an SSL reverse proxy.
You will also need to configure a Remote Access Application - the 'Setup' section of the 'Getting Started with the Force.com REST API' article contains detailed steps. Use https://localhost/oauth/callback as the Callback URL.
Note that, if you run the app locally, you must set three environment variables - CLIENT_ID, CLIENT_SECRET and LOGIN_SERVER. The first two are the consumer key and secret from your remote access app; the third will be https://login.salesforce.com for production and developer edition environments, and https://test.salesforce.com for sandboxes.
Full installation instructions are provided in the GitHub project.
Source
This is the complete content of demo.rb
require 'rubygems'
require 'sinatra'
require 'oauth2'
require 'json'
require 'cgi'
require 'dalli'
require 'rack/session/dalli' # For Rack sessions in Dalli
$stdout.sync = true
# Dalli is a Ruby client for memcache
def dalli_client
Dalli::Client.new(nil, :compression => true, :namespace => 'rack.session', :expires_in => 3600)
end
enable :sessions
# Use the Dalli Rack session implementation
use Rack::Session::Dalli, :cache => dalli_client
# Set up the OAuth2 client
def oauth2_client
OAuth2::Client.new(
ENV['CLIENT_ID'],
ENV['CLIENT_SECRET'],
:site => ENV['LOGIN_SERVER'],
:authorize_url =>'/services/oauth2/authorize',
:token_url => '/services/oauth2/token',
:raise_errors => false
)
end
# Subclass OAuth2::AccessToken so we can do auto-refresh
class ForceToken < OAuth2::AccessToken
def request(verb, path, opts={}, &block)
response = super(verb, path, opts, &block)
if response.status == 401 && refresh_token
puts "Refreshing access token"
@token = refresh!.token
response = super(verb, path, opts, &block)
end
response
end
end
# Filter for all paths except /oauth/callback
before do
pass if request.path_info == '/oauth/callback'
token = session['access_token']
refresh = session['refresh_token']
@instance_url = session['instance_url']
if token
@access_token = ForceToken.from_hash(oauth2_client, { :access_token => token, :refresh_token => refresh, :header_format => 'OAuth %s' } )
else
redirect oauth2_client.auth_code.authorize_url(:redirect_uri => "https://#{request.host}/oauth/callback")
end
end
after do
# Token may have refreshed!
if @access_token && session['access_token'] != @access_token.token
puts "Putting refreshed access token in session"
session['access_token'] = @access_token.token
end
end
get '/oauth/callback' do
begin
access_token = oauth2_client.auth_code.get_token(params[:code],
:redirect_uri => "https://#{request.host}/oauth/callback")
session['access_token'] = access_token.token
session['refresh_token'] = access_token.refresh_token
session['instance_url'] = access_token.params['instance_url']
redirect '/'
rescue => exception
output = '<html><body><tt>'
output += "Exception: #{exception.message}<br/>"+exception.backtrace.join('<br/>')
output += '<tt></body></html>'
end
end
get '/' do
# Field list isn't very volatile - stash it in the session
if !session['field_list']
session['field_list'] = @access_token.get("#{@instance_url}/services/data/v21.0/sobjects/Account/describe/").parsed
end
@field_list = session['field_list']
if params[:value]
query = "SELECT Name, Id FROM Account WHERE #{params[:field]} LIKE '#{params[:value]}%' ORDER BY Name LIMIT 20"
else
query = "SELECT Name, Id from Account ORDER BY Name LIMIT 20"
end
@accounts = @access_token.get("#{@instance_url}/services/data/v20.0/query/?q=#{CGI::escape(query)}").parsed
erb :index
end
get '/detail' do
@account = @access_token.get("#{@instance_url}/services/data/v20.0/sobjects/Account/#{params[:id]}").parsed
erb :detail
end
post '/action' do
if params[:new]
@action_name = 'create'
@action_value = 'Create'
@account = Hash.new
@account['Id'] = ''
@account['Name'] = ''
@account['Industry'] = ''
@account['TickerSymbol'] = ''
done = :edit
elsif params[:edit]
@account = @access_token.get("#{@instance_url}/services/data/v20.0/sobjects/Account/#{params[:id]}").parsed
@action_name = 'update'
@action_value = 'Update'
done = :edit
elsif params[:delete]
@access_token.delete("#{@instance_url}/services/data/v20.0/sobjects/Account/#{params[:id]}")
@action_value = 'Deleted'
@result = Hash.new
@result['id'] = params[:id]
done = :done
end
erb done
end
post '/account' do
if params[:create]
body = {"Name" => params[:Name],
"Industry" => params[:Industry],
"TickerSymbol" => params[:TickerSymbol]}.to_json
@result = @access_token.post("#{@instance_url}/services/data/v20.0/sobjects/Account/",
{:body => body,
:headers => {'Content-type' => 'application/json'}}).parsed
@action_value = 'Created'
elsif params[:update]
body = {"Name" => params[:Name],
"Industry" => params[:Industry],
"TickerSymbol" => params[:TickerSymbol]}.to_json
# No response for an update
@access_token.post("#{@instance_url}/services/data/v20.0/sobjects/Account/#{params[:id]}?_HttpMethod=PATCH",
{:body => body,
:headers => {'Content-type' => 'application/json'}})
@action_value = 'Updated'
@result = Hash.new
@result['id'] = params[:id]
end
erb :done
end
get '/logout' do
# First kill the access token
# (Strictly speaking, we could just do a plain GET on the revoke URL, but
# then we'd need to pull in Net::HTTP or somesuch)
@access_token.get(ENV['LOGIN_SERVER']+'/services/oauth2/revoke?token='+session['access_token'])
# Now save the logout_url
@logout_url = session['instance_url']+'/secur/logout.jsp'
# Clean up the session
session['access_token'] = nil
session['instance_url'] = nil
session['field_list'] = nil
# Now give the user some feedback, loading the logout page into an iframe...
erb :logout
end
get '/revoke' do
# For testing - revoke the token, but leave it in place, so we can test refresh
@access_token.get(ENV['LOGIN_SERVER']+'/services/oauth2/revoke?token='+session['access_token'])
puts "Revoked token #{@access_token.token}"
"Revoked token #{@access_token.token}"
end
Run the sample with
ruby demo.rb
or follow the instructions to deploy it on Heroku.
Restart your browser to clear out any session cookies, then browse to the app's URL (http://localhost/, or your Heroku app's endpoint - don't forget, it must be https!) and click the link. Login as usual, and you will be presented with a screen requesting authorization for the sample app to access your data. On approving access, you will see the app's output in the browser, similar to the following:
You can create a new Account record, or click an account name to go to a minimal Account detail page. From the detail page you can delete or edit the Account record.
Discussion
The before filter checks whether there is an OAuth access token in the session. If there is, then it creates an OAuth2::AccessToken object from the stored data, otherwise, it renders the auth.rb view, which checks if the browser connected via https. If the connection is secure, the browser is redirected to authenticate at salesforce.com, otherwise an error message is shown.
On authenticating the user and obtaining the user’s consent for the app to access the user’s data, Salesforce redirects the browser back to /oauth/callback, where the handler extracts the code query parameter and uses the OAuth2 library to obtain an access token. The access token and instance URL are saved to the session, and the browser is redirected to /.
Note, near the top of demo.rb:
# Dalli is a Ruby client for memcache
def dalli_client
Dalli::Client.new(nil, :compression => true, :namespace => 'rack.session', :expires_in => 3600)
end
# Use the Dalli Rack session implementation
use Rack::Session::Dalli, :cache => dalli_client
This code creates a Dalli client with which to interact with memcache, and sets it as the Rack session handler. In contrast to Rack’s default cookie-based sessions, sessions in memcache are independent of the Ruby server process, can be load-balanced across server instances, and survive restart of the server.
The / handler calls describe on the Account object, and caches the result in the session, since this is relatively static data. Next, it retrieves a list of accounts from Salesforce via the access token’s get method, and renders a page via erb. The index page shows a dropdown list of fields on which the user can search, and a list of 20 accounts that match the search parameters.
The handler for /detail simply retrieves a single Account record and renders a subset of its data via detail.erb. The /action and /account handlers render pages for creating and updating records, and apply those actions to the Account record respectively. The OAuth2::AccessToken methods make it very easy to manipulate records via the REST API.
Access tokens periodically expire, so the ForceToken class wraps OAuth2::AccessToken's request method to test for a 401 'unauthorized' response, refreshing the token and retrying the request. The /revoke endpoint allows you to easily simulate a token expiring, and test that the application does correctly refresh the token, providing uninterrupted service.
Finally, the /logout handler revokes the access token, cleans up the session, and logs the user out via the browser. Note the use of an invisible iframe in logout.erb to terminate the browser session.
Since the Ruby libraries do not yet support the PATCH HTTP method, in updateAccount we use the POST method, overriding it via the _HttpMethod query string parameter.
References
Recipe Activity - Please Log in to write a comment
Exception: No server available
Solution - start memcached
I have this installed on localhost, but get the same error as Andrew Taylor reported on 2012-05-15. Is there a known solution?
Exception: No server available
/.rvm/gems/ruby-1.9.3-p194/gems/dalli-1.1.4/lib/dalli/ring.rb:45:in `server_for_key'
...
/.rvm/gems/ruby-1.9.3-p194/gems/rack-1.4.1/lib/rack/session/abstract/id.rb:71:in `[]='
demo.rb:74:in `block in '
I installed this on heroku, but am getting the following error:
Exception: No server available
/app/.bundle/gems/ruby/1.9.1/gems/dalli-1.1.4/lib/dalli/ring.rb:45:in `server_for_key'
Anyone else encounter this?
Pat - Yes, got it working few months back. On Heroku it works.
Varun - it looks like your certificate store does not include the correct root certs. I would recommend deploying on Heroku - everything works just great there.
The Asf-REST-Adapter is now available. It is @ version 0.1.
You can download from RubyGems -> asf-rest-adapter
or You can see the project -> http://github.com/raygao/asf-rest-adapter
def showAccounts access_token
response = JSON.parse(access_token.get("#{INSTANCE_URL}/services/data/v20.0/query/?q=#{CGI::escape('SELECT Name, Id from Account LIMIT 100')}"))
var = ''
response['records'].each do |record|
var += "#{record['Id']}, #{record['Name']}<br/>"
end
var += '<br/>'
end
def callback
access_token = client.web_server.get_access_token(params[:code], :redirect_uri => "https://localhost/flight/oauth/callback", :grant_type => 'authorization_code')
@print = access_token.token
*************************************************
Till this point code works good. I'm able to publis @print value with .token. But the moment i invoke showAccounts. I start getting
Receieved HTTP 403 During request.
**************************************************
showAccounts access_token
end
when I go to https://localhost:4567, I get the following error in my console:
Good writing!
But, when I point my browser to https://localhost:4567, I am getting unable to connect. What might be causing that?
Thanks,
Hi Varun, Can you put some code in to dump the content of the response - usually that gives more of a clue as to the problem.
Able to resolve the issue using Faraday adapter monkey patch, by setting http.verify_mode = OpenSSL::SSL::VERIFY_NONE;
But i have new problem now. I'm getting following error
OAuth2::HTTPError in OauthController#callback
Received HTTP 400 during request.
Thanks
Hi Pat
Great write !! :) Thanks. I'm facing some issue while getting response back from SF.
Error message::
SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failedCan you point me to possible solution. I'm using R3, Apache 2.2 as reverse proxy, OpenSSL on window 7 machine.Thanks