Permalink
Browse files

Implement automatic token refresh

  • Loading branch information...
1 parent 24f0eb8 commit 9897a1ef7b5a554dc7b8fc94a02b886f24708a4f @dburkes dburkes committed Sep 21, 2011
@@ -11,6 +11,8 @@ class Client
attr_accessor :client_secret
# The OAuth access token in use by the client
attr_accessor :oauth_token
+ # The OAuth refresh token in use by the client
+ attr_accessor :refresh_token
# The base URL to the authenticated user's SalesForce instance
attr_accessor :instance_url
# If true, print API debugging information to stdout. Defaults to false.
@@ -79,7 +81,7 @@ def initialize(options = {})
#
# * If _options_ contains the keys <tt>:username</tt> and <tt>:password</tt>, those credentials are used to authenticate. In this case, the value of <tt>:password</tt> may need to include a concatenated security token, if required by your Salesforce org
# * If _options_ contains the key <tt>:provider</tt>, it is assumed to be the hash returned by Omniauth from a successful web-based OAuth2 authentication
- # * If _options_ contains the keys <tt>:token</tt> and <tt>:instance_url</tt>, those are assumed to be a valid OAuth2 token and instance URL for a Salesforce account, obtained from an external source
+ # * If _options_ contains the keys <tt>:token</tt> and <tt>:instance_url</tt>, those are assumed to be a valid OAuth2 token and instance URL for a Salesforce account, obtained from an external source. _options_ may also optionally contain the key <tt>:refresh_token</tt>
#
# Raises SalesForceError if an error occurs
def authenticate(options = nil)
@@ -102,10 +104,12 @@ def authenticate(options = nil)
@user_id = options["extra"]["user_hash"]["user_id"] rescue nil
self.instance_url = options["credentials"]["instance_url"]
self.oauth_token = options["credentials"]["token"]
+ self.refresh_token = options["credentials"]["refresh_token"]
else
raise ArgumentError unless options.has_key?(:token) && options.has_key?(:instance_url)
self.instance_url = options[:instance_url]
self.oauth_token = options[:token]
+ self.refresh_token = options[:refresh_token]
end
end
@@ -324,12 +328,25 @@ def with_logging(encoded_path, optional_data = nil)
def ensure_expected_response(expected_result_class)
yield.tap do |response|
+ unless response.is_a?(expected_result_class || Net::HTTPSuccess)
+ if response.is_a?(Net::HTTPUnauthorized) && self.refresh_token
+ with_encoded_path_and_checked_response("/services/oauth2/token", { :grant_type => "refresh_token", :refresh_token => self.refresh_token, :client_id => self.client_id, :client_secret => self.client_secret}) do |encoded_path|
+ response = https_request(self.host).post(encoded_path, nil)
+ response
+ end
+
+ if response.is_a?(Net::HTTPSuccess)
+ response = yield
+ end
+ end
+ end
+
raise SalesForceError.new(response) unless response.is_a?(expected_result_class || Net::HTTPSuccess)
end
end
- def https_request
- Net::HTTP.new(URI.parse(self.instance_url).host, 443).tap{|n| n.use_ssl = true }
+ def https_request(host=nil)
+ Net::HTTP.new(host || URI.parse(self.instance_url).host, 443).tap{|n| n.use_ssl = true }
end
def encode_path_with_params(path, parameters={})
@@ -3,6 +3,7 @@
"uid":"https://login.salesforce.com/id/foo/bar",
"credentials":{
"token":"access_token",
- "instance_url":"https://na1.salesforce.com"
+ "instance_url":"https://na1.salesforce.com",
+ "refresh_token":"refresh_token"
}
}
@@ -0,0 +1 @@
+{"error":"invalid_grant","error_description":"expired access/refresh token"}
@@ -0,0 +1,7 @@
+{
+ "id": "https://login.salesforce.com/id/foo/bar",
+ "issued_at": "1309974610026",
+ "instance_url": "https://na1.salesforce.com",
+ "signature": "sig=",
+ "access_token": "access_token"
+}
View
@@ -127,7 +127,7 @@
@client.debugging = false
end
- it "defaults to 22.0" do
+ it "defaults to version 22.0" do
@client.version = nil
response_body = File.read(File.join(File.dirname(__FILE__), '..', "fixtures/auth_success_response.json"))
@@ -200,6 +200,11 @@
@client.authenticate(@response)
@client.instance_url.should == "https://na1.salesforce.com"
end
+
+ it "remembers the refresh token" do
+ @client.authenticate(@response)
+ @client.refresh_token.should == "refresh_token"
+ end
it "returns the token" do
@client.authenticate(@response).should == "access_token"
@@ -217,6 +222,11 @@
@client.authenticate(:token => "obtained_access_token", :instance_url => "https://na1.salesforce.com")
@client.instance_url.should == "https://na1.salesforce.com"
end
+
+ it "sets the refresh token" do
+ @client.authenticate(:token => "obtained_access_token", :instance_url => "https://na1.salesforce.com", :refresh_token => "refresh_token")
+ @client.refresh_token.should == "refresh_token"
+ end
it "returns the token" do
@client.authenticate(:token => "foo", :instance_url => "https://na1.salesforce.com").should == "foo"
@@ -938,6 +948,8 @@ class Something;
@client.http_get("/my/path", nil, {"Something" => "Header"})
}.should raise_error(Databasedotcom::SalesForceError)
end
+
+ it_should_behave_like "a request that can refresh the oauth token", :get, "get", "https://na1.salesforce.com/my/path", 200
end
describe "#http_delete" do
@@ -965,6 +977,8 @@ class Something;
@client.http_delete("/my/path")
}.should raise_error(Databasedotcom::SalesForceError)
end
+
+ it_should_behave_like "a request that can refresh the oauth token", :delete, "delete", "https://na1.salesforce.com/my/path", 204
end
describe "#http_post" do
@@ -992,6 +1006,8 @@ class Something;
@client.http_post("/my/path", "data", nil, {"Something" => "Header"})
}.should raise_error(Databasedotcom::SalesForceError)
end
+
+ it_should_behave_like "a request that can refresh the oauth token", :post, "post", "https://na1.salesforce.com/my/path", 201
end
describe "#http_multipart_post" do
@@ -1019,6 +1035,8 @@ class Something;
@client.http_multipart_post("/my/path", {}, {}, {"Something" => "Header"})
}.should raise_error(Databasedotcom::SalesForceError)
end
+
+ it_should_behave_like "a request that can refresh the oauth token", :post, "multipart_post", "https://na1.salesforce.com/my/path", 201
end
describe "#http_patch" do
@@ -0,0 +1,46 @@
+shared_examples_for("a request that can refresh the oauth token") do |request_method, request_method_name, request_url, success_status_code|
+ describe "when receiving a 401 response" do
+ before do
+ stub_request(request_method, request_url).to_return(:body => "", :status => 401).then.to_return(:body => "", :status => success_status_code)
+ end
+
+ context "with a refresh token" do
+ before do
+ @client.refresh_token = "refresh"
+ end
+
+ context "when the refresh token flow succeeds" do
+ before do
+ response_body = File.read(File.join(File.dirname(__FILE__), "../../fixtures/refresh_success_response.json"))
+ stub_request(:post, "https://bro.baz/services/oauth2/token?client_id=client_id&client_secret=client_secret&grant_type=refresh_token&refresh_token=refresh").to_return(:body => response_body, :status => 200)
+ end
+
+ it "retries the request" do
+ @client.send("http_#{request_method_name}", URI.parse(request_url).path, {})
+ WebMock.should have_requested(request_method, request_url).twice
+ end
+ end
+
+ context "when the refresh token flow fails" do
+ before do
+ response_body = File.read(File.join(File.dirname(__FILE__), "../../fixtures/refresh_error_response.json"))
+ stub_request(:post, "https://bro.baz/services/oauth2/token?client_id=client_id&client_secret=client_secret&grant_type=refresh_token&refresh_token=refresh").to_return(:body => response_body, :status => 400)
+ end
+
+ it "raises SalesForceError" do
+ lambda {
+ @client.send("http_#{request_method_name}", URI.parse(request_url).path, {})
+ }.should raise_error(Databasedotcom::SalesForceError)
+ end
+ end
+ end
+
+ context "without a refresh token" do
+ it "raises SalesForceError" do
+ lambda {
+ @client.send("http_#{request_method_name}", URI.parse(request_url).path, {})
+ }.should raise_error(Databasedotcom::SalesForceError)
+ end
+ end
+ end
+end

0 comments on commit 9897a1e

Please sign in to comment.