]> git.openstreetmap.org Git - rails.git/commitdiff
Add tests for OAuth2
authorTom Hughes <tom@compton.nu>
Thu, 15 Oct 2020 21:07:34 +0000 (22:07 +0100)
committerTom Hughes <tom@compton.nu>
Tue, 18 May 2021 11:05:33 +0000 (12:05 +0100)
test/controllers/oauth2_applications_controller_test.rb [new file with mode: 0644]
test/controllers/oauth2_authorizations_controller_test.rb [new file with mode: 0644]
test/controllers/oauth2_authorized_applications_controller_test.rb [new file with mode: 0644]
test/factories/oauth_access_grant.rb [new file with mode: 0644]
test/factories/oauth_access_token.rb [new file with mode: 0644]
test/factories/oauth_applications.rb [new file with mode: 0644]
test/integration/oauth2_test.rb [new file with mode: 0644]
test/test_helper.rb

diff --git a/test/controllers/oauth2_applications_controller_test.rb b/test/controllers/oauth2_applications_controller_test.rb
new file mode 100644 (file)
index 0000000..eec5e02
--- /dev/null
@@ -0,0 +1,221 @@
+require "test_helper"
+
+class Oauth2ApplicationsControllerTest < ActionDispatch::IntegrationTest
+  ##
+  # test all routes which lead to this controller
+  def test_routes
+    assert_routing(
+      { :path => "/oauth2/applications", :method => :get },
+      { :controller => "oauth2_applications", :action => "index" }
+    )
+    assert_routing(
+      { :path => "/oauth2/applications", :method => :post },
+      { :controller => "oauth2_applications", :action => "create" }
+    )
+    assert_routing(
+      { :path => "/oauth2/applications/new", :method => :get },
+      { :controller => "oauth2_applications", :action => "new" }
+    )
+    assert_routing(
+      { :path => "/oauth2/applications/1/edit", :method => :get },
+      { :controller => "oauth2_applications", :action => "edit", :id => "1" }
+    )
+    assert_routing(
+      { :path => "/oauth2/applications/1", :method => :get },
+      { :controller => "oauth2_applications", :action => "show", :id => "1" }
+    )
+    assert_routing(
+      { :path => "/oauth2/applications/1", :method => :patch },
+      { :controller => "oauth2_applications", :action => "update", :id => "1" }
+    )
+    assert_routing(
+      { :path => "/oauth2/applications/1", :method => :put },
+      { :controller => "oauth2_applications", :action => "update", :id => "1" }
+    )
+    assert_routing(
+      { :path => "/oauth2/applications/1", :method => :delete },
+      { :controller => "oauth2_applications", :action => "destroy", :id => "1" }
+    )
+  end
+
+  def test_index
+    user = create(:user)
+    create_list(:oauth_application, 2, :owner => user)
+
+    get oauth_applications_path
+    assert_response :redirect
+    assert_redirected_to login_path(:referer => oauth_applications_path)
+
+    session_for(user)
+
+    get oauth_applications_path
+    assert_response :success
+    assert_template "oauth2_applications/index"
+    assert_select "tr", 2
+  end
+
+  def test_new
+    user = create(:user)
+
+    get new_oauth_application_path
+    assert_response :redirect
+    assert_redirected_to login_path(:referer => new_oauth_application_path)
+
+    session_for(user)
+
+    get new_oauth_application_path
+    assert_response :success
+    assert_template "oauth2_applications/new"
+    assert_select "form", 1 do
+      assert_select "input#doorkeeper_application_name", 1
+      assert_select "textarea#doorkeeper_application_redirect_uri", 1
+      assert_select "input#doorkeeper_application_confidential", 1
+      Oauth.scopes.each do |scope|
+        assert_select "input#doorkeeper_application_scopes_#{scope.name}", 1
+      end
+    end
+  end
+
+  def test_create
+    user = create(:user)
+
+    assert_difference "Doorkeeper::Application.count", 0 do
+      post oauth_applications_path
+    end
+    assert_response :forbidden
+
+    session_for(user)
+
+    assert_difference "Doorkeeper::Application.count", 0 do
+      post oauth_applications_path(:doorkeeper_application => {
+                                     :name => "Test Application"
+                                   })
+    end
+    assert_response :success
+    assert_template "oauth2_applications/new"
+
+    assert_difference "Doorkeeper::Application.count", 0 do
+      post oauth_applications_path(:doorkeeper_application => {
+                                     :name => "Test Application",
+                                     :redirect_uri => "https://test.example.com/",
+                                     :scopes => ["bad_scope"]
+                                   })
+    end
+    assert_response :success
+    assert_template "oauth2_applications/new"
+
+    assert_difference "Doorkeeper::Application.count", 1 do
+      post oauth_applications_path(:doorkeeper_application => {
+                                     :name => "Test Application",
+                                     :redirect_uri => "https://test.example.com/",
+                                     :scopes => ["read_prefs"]
+                                   })
+    end
+    assert_response :redirect
+    assert_redirected_to oauth_application_path(:id => Doorkeeper::Application.find_by(:name => "Test Application").id)
+  end
+
+  def test_show
+    user = create(:user)
+    client = create(:oauth_application, :owner => user)
+    other_client = create(:oauth_application)
+
+    get oauth_application_path(:id => client)
+    assert_response :redirect
+    assert_redirected_to login_path(:referer => oauth_application_path(:id => client.id))
+
+    session_for(user)
+
+    get oauth_application_path(:id => other_client)
+    assert_response :not_found
+    assert_template "oauth2_applications/not_found"
+
+    get oauth_application_path(:id => client)
+    assert_response :success
+    assert_template "oauth2_applications/show"
+  end
+
+  def test_edit
+    user = create(:user)
+    client = create(:oauth_application, :owner => user)
+    other_client = create(:oauth_application)
+
+    get edit_oauth_application_path(:id => client)
+    assert_response :redirect
+    assert_redirected_to login_path(:referer => edit_oauth_application_path(:id => client.id))
+
+    session_for(user)
+
+    get edit_oauth_application_path(:id => other_client)
+    assert_response :not_found
+    assert_template "oauth2_applications/not_found"
+
+    get edit_oauth_application_path(:id => client)
+    assert_response :success
+    assert_template "oauth2_applications/edit"
+    assert_select "form", 1 do
+      assert_select "input#doorkeeper_application_name", 1
+      assert_select "textarea#doorkeeper_application_redirect_uri", 1
+      assert_select "input#doorkeeper_application_confidential", 1
+      Oauth.scopes.each do |scope|
+        assert_select "input#doorkeeper_application_scopes_#{scope.name}", 1
+      end
+    end
+  end
+
+  def test_update
+    user = create(:user)
+    client = create(:oauth_application, :owner => user)
+    other_client = create(:oauth_application)
+
+    put oauth_application_path(:id => client)
+    assert_response :forbidden
+
+    session_for(user)
+
+    put oauth_application_path(:id => other_client)
+    assert_response :not_found
+    assert_template "oauth2_applications/not_found"
+
+    put oauth_application_path(:id => client,
+                               :doorkeeper_application => {
+                                 :name => "New Name",
+                                 :redirect_uri => nil
+                               })
+    assert_response :success
+    assert_template "oauth2_applications/edit"
+
+    put oauth_application_path(:id => client,
+                               :doorkeeper_application => {
+                                 :name => "New Name",
+                                 :redirect_uri => "https://new.example.com/url"
+                               })
+    assert_response :redirect
+    assert_redirected_to oauth_application_path(:id => client.id)
+  end
+
+  def test_destroy
+    user = create(:user)
+    client = create(:oauth_application, :owner => user)
+    other_client = create(:oauth_application)
+
+    assert_difference "Doorkeeper::Application.count", 0 do
+      delete oauth_application_path(:id => client)
+    end
+    assert_response :forbidden
+
+    session_for(user)
+
+    assert_difference "Doorkeeper::Application.count", 0 do
+      delete oauth_application_path(:id => other_client)
+    end
+    assert_response :not_found
+    assert_template "oauth2_applications/not_found"
+
+    assert_difference "Doorkeeper::Application.count", -1 do
+      delete oauth_application_path(:id => client)
+    end
+    assert_response :redirect
+    assert_redirected_to oauth_applications_path
+  end
+end
diff --git a/test/controllers/oauth2_authorizations_controller_test.rb b/test/controllers/oauth2_authorizations_controller_test.rb
new file mode 100644 (file)
index 0000000..19bc798
--- /dev/null
@@ -0,0 +1,184 @@
+require "test_helper"
+
+class Oauth2AuthorizationsControllerTest < ActionDispatch::IntegrationTest
+  ##
+  # test all routes which lead to this controller
+  def test_routes
+    assert_routing(
+      { :path => "/oauth2/authorize", :method => :get },
+      { :controller => "oauth2_authorizations", :action => "new" }
+    )
+    assert_routing(
+      { :path => "/oauth2/authorize", :method => :post },
+      { :controller => "oauth2_authorizations", :action => "create" }
+    )
+    assert_routing(
+      { :path => "/oauth2/authorize", :method => :delete },
+      { :controller => "oauth2_authorizations", :action => "destroy" }
+    )
+    assert_routing(
+      { :path => "/oauth2/authorize/native", :method => :get },
+      { :controller => "oauth2_authorizations", :action => "show" }
+    )
+  end
+
+  def test_new
+    application = create(:oauth_application, :scopes => "write_api")
+
+    get oauth_authorization_path(:client_id => application.uid,
+                                 :redirect_uri => application.redirect_uri,
+                                 :response_type => "code",
+                                 :scope => "write_api")
+    assert_response :redirect
+    assert_redirected_to login_path(:referer => oauth_authorization_path(:client_id => application.uid,
+                                                                         :redirect_uri => application.redirect_uri,
+                                                                         :response_type => "code",
+                                                                         :scope => "write_api"))
+
+    session_for(create(:user))
+
+    get oauth_authorization_path(:client_id => application.uid,
+                                 :redirect_uri => application.redirect_uri,
+                                 :response_type => "code",
+                                 :scope => "write_api")
+    assert_response :success
+    assert_template "oauth2_authorizations/new"
+  end
+
+  def test_new_native
+    application = create(:oauth_application, :scopes => "write_api", :redirect_uri => "urn:ietf:wg:oauth:2.0:oob")
+
+    get oauth_authorization_path(:client_id => application.uid,
+                                 :redirect_uri => application.redirect_uri,
+                                 :response_type => "code",
+                                 :scope => "write_api")
+    assert_response :redirect
+    assert_redirected_to login_path(:referer => oauth_authorization_path(:client_id => application.uid,
+                                                                         :redirect_uri => application.redirect_uri,
+                                                                         :response_type => "code",
+                                                                         :scope => "write_api"))
+
+    session_for(create(:user))
+
+    get oauth_authorization_path(:client_id => application.uid,
+                                 :redirect_uri => application.redirect_uri,
+                                 :response_type => "code",
+                                 :scope => "write_api")
+    assert_response :success
+    assert_template "oauth2_authorizations/new"
+  end
+
+  def test_new_bad_uri
+    application = create(:oauth_application, :scopes => "write_api")
+
+    session_for(create(:user))
+
+    get oauth_authorization_path(:client_id => application.uid,
+                                 :redirect_uri => "https://bad.example.com/",
+                                 :response_type => "code",
+                                 :scope => "write_api")
+    assert_response :success
+    assert_template "oauth2_authorizations/error"
+    assert_select "p", "The requested redirect uri is malformed or doesn't match client redirect URI."
+  end
+
+  def test_new_bad_scope
+    application = create(:oauth_application, :scopes => "write_api")
+
+    session_for(create(:user))
+
+    get oauth_authorization_path(:client_id => application.uid,
+                                 :redirect_uri => application.redirect_uri,
+                                 :response_type => "code",
+                                 :scope => "bad_scope")
+    assert_response :success
+    assert_template "oauth2_authorizations/error"
+    assert_select "p", "The requested scope is invalid, unknown, or malformed."
+
+    get oauth_authorization_path(:client_id => application.uid,
+                                 :redirect_uri => application.redirect_uri,
+                                 :response_type => "code",
+                                 :scope => "write_prefs")
+    assert_response :success
+    assert_template "oauth2_authorizations/error"
+    assert_select "p", "The requested scope is invalid, unknown, or malformed."
+  end
+
+  def test_create
+    application = create(:oauth_application, :scopes => "write_api")
+
+    post oauth_authorization_path(:client_id => application.uid,
+                                  :redirect_uri => application.redirect_uri,
+                                  :response_type => "code",
+                                  :scope => "write_api")
+    assert_response :forbidden
+
+    session_for(create(:user))
+
+    post oauth_authorization_path(:client_id => application.uid,
+                                  :redirect_uri => application.redirect_uri,
+                                  :response_type => "code",
+                                  :scope => "write_api")
+    assert_response :redirect
+    assert_redirected_to(/^#{Regexp.escape(application.redirect_uri)}\?code=/)
+  end
+
+  def test_create_native
+    application = create(:oauth_application, :scopes => "write_api", :redirect_uri => "urn:ietf:wg:oauth:2.0:oob")
+
+    post oauth_authorization_path(:client_id => application.uid,
+                                  :redirect_uri => application.redirect_uri,
+                                  :response_type => "code",
+                                  :scope => "write_api")
+    assert_response :forbidden
+
+    session_for(create(:user))
+
+    post oauth_authorization_path(:client_id => application.uid,
+                                  :redirect_uri => application.redirect_uri,
+                                  :response_type => "code",
+                                  :scope => "write_api")
+    assert_response :redirect
+    assert_equal native_oauth_authorization_path, URI.parse(response.location).path
+    follow_redirect!
+    assert_response :success
+    assert_template "oauth2_authorizations/show"
+  end
+
+  def test_destroy
+    application = create(:oauth_application)
+
+    delete oauth_authorization_path(:client_id => application.uid,
+                                    :redirect_uri => application.redirect_uri,
+                                    :response_type => "code",
+                                    :scope => "write_api")
+    assert_response :forbidden
+
+    session_for(create(:user))
+
+    delete oauth_authorization_path(:client_id => application.uid,
+                                    :redirect_uri => application.redirect_uri,
+                                    :response_type => "code",
+                                    :scope => "write_api")
+    assert_response :redirect
+    assert_redirected_to(/^#{Regexp.escape(application.redirect_uri)}\?error=access_denied/)
+  end
+
+  def test_destroy_native
+    application = create(:oauth_application, :redirect_uri => "urn:ietf:wg:oauth:2.0:oob")
+
+    delete oauth_authorization_path(:client_id => application.uid,
+                                    :redirect_uri => application.redirect_uri,
+                                    :response_type => "code",
+                                    :scope => "write_api")
+    assert_response :forbidden
+
+    session_for(create(:user))
+
+    delete oauth_authorization_path(:client_id => application.uid,
+                                    :redirect_uri => application.redirect_uri,
+                                    :response_type => "code",
+                                    :scope => "write_api")
+    assert_response :bad_request
+  end
+end
diff --git a/test/controllers/oauth2_authorized_applications_controller_test.rb b/test/controllers/oauth2_authorized_applications_controller_test.rb
new file mode 100644 (file)
index 0000000..45a60ef
--- /dev/null
@@ -0,0 +1,63 @@
+require "test_helper"
+
+class Oauth2AuthorizedApplicationsControllerTest < ActionDispatch::IntegrationTest
+  ##
+  # test all routes which lead to this controller
+  def test_routes
+    assert_routing(
+      { :path => "/oauth2/authorized_applications", :method => :get },
+      { :controller => "oauth2_authorized_applications", :action => "index" }
+    )
+    assert_routing(
+      { :path => "/oauth2/authorized_applications/1", :method => :delete },
+      { :controller => "oauth2_authorized_applications", :action => "destroy", :id => "1" }
+    )
+  end
+
+  def test_index
+    user = create(:user)
+    application1 = create(:oauth_application)
+    create(:oauth_access_grant, :resource_owner_id => user.id, :application => application1)
+    create(:oauth_access_token, :resource_owner_id => user.id, :application => application1)
+    application2 = create(:oauth_application)
+    create(:oauth_access_grant, :resource_owner_id => user.id, :application => application2)
+    create(:oauth_access_token, :resource_owner_id => user.id, :application => application2)
+    create(:oauth_application)
+
+    get oauth_authorized_applications_path
+    assert_response :redirect
+    assert_redirected_to login_path(:referer => oauth_authorized_applications_path)
+
+    session_for(user)
+
+    get oauth_authorized_applications_path
+    assert_response :success
+    assert_template "oauth2_authorized_applications/index"
+    assert_select "tr", 2
+  end
+
+  def test_destroy
+    user = create(:user)
+    application1 = create(:oauth_application)
+    create(:oauth_access_grant, :resource_owner_id => user.id, :application => application1)
+    create(:oauth_access_token, :resource_owner_id => user.id, :application => application1)
+    application2 = create(:oauth_application)
+    create(:oauth_access_grant, :resource_owner_id => user.id, :application => application2)
+    create(:oauth_access_token, :resource_owner_id => user.id, :application => application2)
+    create(:oauth_application)
+
+    delete oauth_authorized_application_path(:id => application1.id)
+    assert_response :forbidden
+
+    session_for(user)
+
+    delete oauth_authorized_application_path(:id => application1.id)
+    assert_response :redirect
+    assert_redirected_to oauth_authorized_applications_path
+
+    get oauth_authorized_applications_path
+    assert_response :success
+    assert_template "oauth2_authorized_applications/index"
+    assert_select "tr", 1
+  end
+end
diff --git a/test/factories/oauth_access_grant.rb b/test/factories/oauth_access_grant.rb
new file mode 100644 (file)
index 0000000..caddea8
--- /dev/null
@@ -0,0 +1,9 @@
+FactoryBot.define do
+  factory :oauth_access_grant, :class => "Doorkeeper::AccessGrant" do
+    association :resource_owner_id, :factory => :user
+    association :application, :factory => :oauth_application
+
+    expires_in { 86400 }
+    redirect_uri { application.redirect_uri }
+  end
+end
diff --git a/test/factories/oauth_access_token.rb b/test/factories/oauth_access_token.rb
new file mode 100644 (file)
index 0000000..c0f6245
--- /dev/null
@@ -0,0 +1,6 @@
+FactoryBot.define do
+  factory :oauth_access_token, :class => "Doorkeeper::AccessToken" do
+    association :resource_owner_id, :factory => :user
+    association :application, :factory => :oauth_application
+  end
+end
diff --git a/test/factories/oauth_applications.rb b/test/factories/oauth_applications.rb
new file mode 100644 (file)
index 0000000..a9b3b87
--- /dev/null
@@ -0,0 +1,8 @@
+FactoryBot.define do
+  factory :oauth_application, :class => "Doorkeeper::Application" do
+    sequence(:name) { |n| "OAuth application #{n}" }
+    sequence(:redirect_uri) { |n| "https://example.com/app/#{n}" }
+
+    association :owner, :factory => :user
+  end
+end
diff --git a/test/integration/oauth2_test.rb b/test/integration/oauth2_test.rb
new file mode 100644 (file)
index 0000000..8de381c
--- /dev/null
@@ -0,0 +1,170 @@
+require "test_helper"
+
+class OAuth2Test < ActionDispatch::IntegrationTest
+  def test_oauth2
+    client = create(:oauth_application, :redirect_uri => "https://some.web.app.example.org/callback", :scopes => "read_prefs write_api read_gpx")
+    state = SecureRandom.urlsafe_base64(16)
+
+    authorize_client(client, :state => state)
+    assert_response :redirect
+    code = validate_redirect(client, state)
+
+    token = request_token(client, code)
+
+    test_token(token, client)
+  end
+
+  def test_oauth2_oob
+    client = create(:oauth_application, :redirect_uri => "urn:ietf:wg:oauth:2.0:oob", :scopes => "read_prefs write_api read_gpx")
+
+    authorize_client(client)
+    assert_response :redirect
+    follow_redirect!
+    assert_response :success
+    assert_template "oauth2_authorizations/show"
+    m = response.body.match(%r{<code id="authorization_code">([A-Za-z0-9_-]+)</code>})
+    assert_not_nil m
+    code = m[1]
+
+    token = request_token(client, code)
+
+    test_token(token, client)
+  end
+
+  def test_oauth2_pkce_plain
+    client = create(:oauth_application, :redirect_uri => "https://some.web.app.example.org/callback", :scopes => "read_prefs write_api read_gpx")
+    state = SecureRandom.urlsafe_base64(16)
+    verifier = SecureRandom.urlsafe_base64(48)
+    challenge = verifier
+
+    authorize_client(client, :state => state, :code_challenge => challenge, :code_challenge_method => "plain")
+    assert_response :redirect
+    code = validate_redirect(client, state)
+
+    token = request_token(client, code, verifier)
+
+    test_token(token, client)
+  end
+
+  def test_oauth2_pkce_s256
+    client = create(:oauth_application, :redirect_uri => "https://some.web.app.example.org/callback", :scopes => "read_prefs write_api read_gpx")
+    state = SecureRandom.urlsafe_base64(16)
+    verifier = SecureRandom.urlsafe_base64(48)
+    challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), :padding => false)
+
+    authorize_client(client, :state => state, :code_challenge => challenge, :code_challenge_method => "S256")
+    assert_response :redirect
+    code = validate_redirect(client, state)
+
+    token = request_token(client, code, verifier)
+
+    test_token(token, client)
+  end
+
+  private
+
+  def authorize_client(client, options = {})
+    options = options.merge(:client_id => client.uid,
+                            :redirect_uri => client.redirect_uri,
+                            :response_type => "code",
+                            :scope => "read_prefs")
+
+    get oauth_authorization_path(options)
+    assert_response :redirect
+    assert_redirected_to login_path(:referer => request.fullpath)
+
+    user = create(:user)
+
+    post login_path(:username => user.email, :password => "test")
+    follow_redirect!
+    assert_response :success
+
+    get oauth_authorization_path(options)
+    assert_response :success
+    assert_template "oauth2_authorizations/new"
+
+    delete oauth_authorization_path(options)
+
+    validate_deny(client, options)
+
+    post oauth_authorization_path(options)
+  end
+
+  def validate_deny(client, options)
+    if client.redirect_uri == "urn:ietf:wg:oauth:2.0:oob"
+      assert_response :bad_request
+    else
+      assert_response :redirect
+      location = URI.parse(response.location)
+      assert_match(/^#{Regexp.escape(client.redirect_uri)}/, location.to_s)
+      query = Rack::Utils.parse_query(location.query)
+      assert_equal "access_denied", query["error"]
+      assert_equal "The resource owner or authorization server denied the request.", query["error_description"]
+      assert_equal options[:state], query["state"]
+    end
+  end
+
+  def validate_redirect(client, state)
+    location = URI.parse(response.location)
+    assert_match(/^#{Regexp.escape(client.redirect_uri)}/, location.to_s)
+    query = Rack::Utils.parse_query(location.query)
+    assert_equal state, query["state"]
+
+    query["code"]
+  end
+
+  def request_token(client, code, verifier = nil)
+    options = {
+      :client_id => client.uid,
+      :client_secret => client.plaintext_secret,
+      :code => code,
+      :grant_type => "authorization_code",
+      :redirect_uri => client.redirect_uri
+    }
+
+    if verifier
+      post oauth_token_path(options)
+      assert_response :bad_request
+
+      options = options.merge(:code_verifier => verifier)
+    end
+
+    post oauth_token_path(options)
+    assert_response :success
+    token = JSON.parse(response.body)
+    assert_equal "Bearer", token["token_type"]
+    assert_equal "read_prefs", token["scope"]
+
+    token["access_token"]
+  end
+
+  def test_token(token, client)
+    get user_preferences_path
+    assert_response :unauthorized
+
+    auth_header = bearer_authorization_header(token)
+
+    get user_preferences_path, :headers => auth_header
+    assert_response :success
+
+    get user_preferences_path(:access_token => token)
+    assert_response :unauthorized
+
+    get user_preferences_path(:bearer_token => token)
+    assert_response :unauthorized
+
+    get api_trace_path(:id => 2), :headers => auth_header
+    assert_response :forbidden
+
+    post oauth_revoke_path(:token => token)
+    assert_response :forbidden
+
+    post oauth_revoke_path(:token => token,
+                           :client_id => client.uid,
+                           :client_secret => client.plaintext_secret)
+    assert_response :success
+
+    get user_preferences_path, :headers => auth_header
+    assert_response :unauthorized
+  end
+end
index a6147ef29054e817dbe27d7cd9b3c5a146dcf6a6..505fa256876b2a9000965d59fda9923b67f6580d 100644 (file)
@@ -138,6 +138,12 @@ module ActiveSupport
       { "Authorization" => format("Basic %<auth>s", :auth => Base64.encode64("#{user}:#{pass}")) }
     end
 
+    ##
+    # return request header for HTTP Bearer Authorization
+    def bearer_authorization_header(token)
+      { "Authorization" => "Bearer #{token}" }
+    end
+
     ##
     # make an OAuth signed request
     def signed_request(method, uri, options = {})