From a645a78cd028554165e44760d531f946db0ea1cb Mon Sep 17 00:00:00 2001 From: Olivier Bellone Date: Sun, 2 Apr 2017 01:04:09 -0700 Subject: [PATCH] Add OAuth methods --- lib/stripe.rb | 5 +- lib/stripe/account.rb | 11 ++-- lib/stripe/errors.rb | 40 +++++++++++++++ lib/stripe/oauth.rb | 56 ++++++++++++++++++++ lib/stripe/stripe_client.rb | 56 +++++++++++++++----- test/stripe/oauth_test.rb | 85 +++++++++++++++++++++++++++++++ test/stripe/stripe_client_test.rb | 42 +++++++++++++-- 7 files changed, 274 insertions(+), 21 deletions(-) create mode 100644 lib/stripe/oauth.rb create mode 100644 test/stripe/oauth_test.rb diff --git a/lib/stripe.rb b/lib/stripe.rb index 41c7793a..7145fef6 100644 --- a/lib/stripe.rb +++ b/lib/stripe.rb @@ -69,6 +69,9 @@ require 'stripe/three_d_secure' require 'stripe/token' require 'stripe/transfer' +# OAuth +require 'stripe/oauth' + module Stripe DEFAULT_CA_BUNDLE_PATH = File.dirname(__FILE__) + '/data/ca-certificates.crt' @@ -90,7 +93,7 @@ module Stripe @read_timeout = 80 class << self - attr_accessor :stripe_account, :api_key, :api_base, :verify_ssl_certs, :api_version, :connect_base, :uploads_base, + attr_accessor :stripe_account, :api_key, :api_base, :verify_ssl_certs, :api_version, :client_id, :connect_base, :uploads_base, :open_timeout, :read_timeout attr_reader :max_network_retry_delay, :initial_network_retry_delay diff --git a/lib/stripe/account.rb b/lib/stripe/account.rb index 08fad9ba..52473bc7 100644 --- a/lib/stripe/account.rb +++ b/lib/stripe/account.rb @@ -93,11 +93,12 @@ module Stripe raise NoMethodError.new('Overridding legal_entity can cause serious issues. Instead, set the individual fields of legal_entity like blah.legal_entity.first_name = \'Blah\'') end - def deauthorize(client_id, opts={}) - opts = {:api_base => Stripe.connect_base}.merge(Util.normalize_opts(opts)) - resp, opts = request(:post, '/oauth/deauthorize', { 'client_id' => client_id, 'stripe_user_id' => self.id }, opts) - opts.delete(:api_base) # the api_base here is a one-off, don't persist it - Util.convert_to_stripe_object(resp.data, opts) + def deauthorize(client_id=nil, opts={}) + params = { + client_id: client_id, + stripe_user_id: self.id, + } + OAuth.deauthorize(params, opts) end ARGUMENT_NOT_PROVIDED = Object.new diff --git a/lib/stripe/errors.rb b/lib/stripe/errors.rb index 06ff3150..c5a5e756 100644 --- a/lib/stripe/errors.rb +++ b/lib/stripe/errors.rb @@ -100,4 +100,44 @@ module Stripe @sig_header = sig_header end end + + module OAuth + # OAuthError is raised when the OAuth API returns an error. + class OAuthError < StripeError + attr_accessor :code + + def initialize(code, description, http_status: nil, http_body: nil, json_body: nil, + http_headers: nil) + super(description, http_status: http_status, http_body: http_body, + json_body: json_body, http_headers: http_headers) + @code = code + end + end + + # InvalidGrantError is raised when a specified code doesn't exist, is + # expired, has been used, or doesn't belong to you; a refresh token doesn't + # exist, or doesn't belong to you; or if an API key's mode (live or test) + # doesn't match the mode of a code or refresh token. + class InvalidGrantError < OAuthError + end + + # InvalidRequestError is raised when a code, refresh token, or grant type + # parameter is not provided, but was required. + class InvalidRequestError < OAuthError + end + + # InvalidScopeError is raised when an invalid scope parameter is provided. + class InvalidScopeError < OAuthError + end + + # UnsupportedGrantTypeError is raised when an unuspported grant type + # parameter is specified. + class UnsupportedGrantTypeError < OAuthError + end + + # UnsupportedResponseTypeError is raised when an unsupported response type + # parameter is specified. + class UnsupportedResponseTypeError < OAuthError + end + end end diff --git a/lib/stripe/oauth.rb b/lib/stripe/oauth.rb new file mode 100644 index 00000000..b0c504a5 --- /dev/null +++ b/lib/stripe/oauth.rb @@ -0,0 +1,56 @@ +module Stripe + module OAuth + module OAuthOperations + extend APIOperations::Request::ClassMethods + + def self.request(method, url, params, opts) + opts = Util.normalize_opts(opts) + opts[:client] ||= StripeClient.active_client + opts[:api_base] ||= Stripe.connect_base + + super(method, url, params, opts) + end + end + + def self.get_client_id(params={}) + client_id = params[:client_id] || Stripe.client_id + unless client_id + raise AuthenticationError.new('No client_id provided. ' \ + 'Set your client_id using "Stripe.client_id = ". ' \ + 'You can find your client_ids in your Stripe dashboard at ' \ + 'https://dashboard.stripe.com/account/applications/settings, ' \ + 'after registering your account as a platform. See ' \ + 'https://stripe.com/docs/connect/standalone-accounts for details, ' \ + 'or email support@stripe.com if you have any questions.') + end + client_id + end + + def self.authorize_url(params={}, opts={}) + base = opts[:connect_base] || Stripe.connect_base + + params[:client_id] = get_client_id(params) + params[:response_type] ||= 'code' + query = Util.encode_parameters(params) + + "#{base}/oauth/authorize?#{query}" + end + + def self.token(params={}, opts={}) + opts = Util.normalize_opts(opts) + resp, opts = OAuthOperations.request( + :post, '/oauth/token', params, opts) + # This is just going to return a generic StripeObject, but that's okay + Util.convert_to_stripe_object(resp.data, opts) + end + + def self.deauthorize(params={}, opts={}) + opts = Util.normalize_opts(opts) + params[:client_id] = get_client_id(params) + resp, opts = OAuthOperations.request( + :post, '/oauth/deauthorize', params, opts) + # This is just going to return a generic StripeObject, but that's okay + Util.convert_to_stripe_object(resp.data, opts) + end + end +end diff --git a/lib/stripe/stripe_client.rb b/lib/stripe/stripe_client.rb index 6a8011bf..2b20349a 100644 --- a/lib/stripe/stripe_client.rb +++ b/lib/stripe/stripe_client.rb @@ -197,7 +197,7 @@ module Stripe case e when Faraday::ClientError if e.response - handle_api_error(e.response) + handle_error_response(e.response) else handle_network_error(e, retry_count, api_base) end @@ -229,12 +229,12 @@ module Stripe str end - def handle_api_error(http_resp) + def handle_error_response(http_resp) begin resp = StripeResponse.from_faraday_hash(http_resp) - error = resp.data[:error] + error_data = resp.data[:error] - unless error && error.is_a?(Hash) + unless error_data raise StripeError.new("Indeterminate error") end @@ -242,47 +242,79 @@ module Stripe raise general_api_error(http_resp[:status], http_resp[:body]) end + if error_data.is_a?(String) + error = specific_oauth_error(resp, error_data) + end + if error.nil? + error = specific_api_error(resp, error_data) + end + + error.response = resp + raise(error) + end + + def specific_api_error(resp, error_data) case resp.http_status when 400, 404 error = InvalidRequestError.new( - error[:message], error[:param], + error_data[:message], error_data[:param], http_status: resp.http_status, http_body: resp.http_body, json_body: resp.data, http_headers: resp.http_headers ) when 401 error = AuthenticationError.new( - error[:message], + error_data[:message], http_status: resp.http_status, http_body: resp.http_body, json_body: resp.data, http_headers: resp.http_headers ) when 402 error = CardError.new( - error[:message], error[:param], error[:code], + error_data[:message], error_data[:param], error_data[:code], http_status: resp.http_status, http_body: resp.http_body, json_body: resp.data, http_headers: resp.http_headers ) when 403 error = PermissionError.new( - error[:message], + error_data[:message], http_status: resp.http_status, http_body: resp.http_body, json_body: resp.data, http_headers: resp.http_headers ) when 429 error = RateLimitError.new( - error[:message], + error_data[:message], http_status: resp.http_status, http_body: resp.http_body, json_body: resp.data, http_headers: resp.http_headers ) else error = APIError.new( - error[:message], + error_data[:message], http_status: resp.http_status, http_body: resp.http_body, json_body: resp.data, http_headers: resp.http_headers ) end - error.response = resp - raise(error) + error + end + + # Attempts to look at a response's error code and return an OAuth error if + # one matches. Will return `nil` if the code isn't recognized. + def specific_oauth_error(resp, error_code) + description = resp.data[:error_description] || error_code + + args = [error_code, description, { + http_status: resp.http_status, http_body: resp.http_body, + json_body: resp.data, http_headers: resp.http_headers + }] + + case error_code + when 'invalid_grant' then OAuth::InvalidGrantError.new(*args) + when 'invalid_request' then OAuth::InvalidRequestError.new(*args) + when 'invalid_scope' then OAuth::InvalidScopeError.new(*args) + when 'unsupported_grant_type' then OAuth::UnsupportedGrantTypeError.new(*args) + when 'unsupported_response_type' then OAuth::UnsupportedResponseTypeError.new(*args) + else + nil + end end def handle_network_error(e, retry_count, api_base=nil) diff --git a/test/stripe/oauth_test.rb b/test/stripe/oauth_test.rb new file mode 100644 index 00000000..b540a257 --- /dev/null +++ b/test/stripe/oauth_test.rb @@ -0,0 +1,85 @@ +require File.expand_path('../../test_helper', __FILE__) + +module Stripe + class OAuthTest < Test::Unit::TestCase + setup do + Stripe.client_id = 'ca_test' + end + + teardown do + Stripe.client_id = nil + end + + context ".authorize_url" do + should "return the authorize URL" do + uri_str = OAuth.authorize_url({ + scope: 'read_write', + state: 'csrf_token', + stripe_user: { + email: 'test@example.com', + url: 'https://example.com/profile/test', + country: 'US', + }, + }) + + uri = URI::parse(uri_str) + params = CGI::parse(uri.query) + + assert_equal('https', uri.scheme) + assert_equal('connect.stripe.com', uri.host) + assert_equal('/oauth/authorize', uri.path) + + assert_equal(['ca_test'], params['client_id']) + assert_equal(['read_write'], params['scope']) + assert_equal(['test@example.com'], params['stripe_user[email]']) + assert_equal(['https://example.com/profile/test'], params['stripe_user[url]']) + assert_equal(['US'], params['stripe_user[country]']) + end + end + + context ".token" do + should "exchange a code for an access token" do + # The OpenAPI fixtures don't cover the OAuth endpoints, so we just + # stub the request manually. + stub_request(:post, "#{Stripe.connect_base}/oauth/token"). + with(body: { + 'grant_type' => 'authorization_code', + 'code' => 'this_is_an_authorization_code', + }). + to_return(body: JSON.generate({ + access_token: 'sk_access_token', + scope: 'read_only', + livemode: false, + token_type: 'bearer', + refresh_token: 'sk_refresh_token', + stripe_user_id: 'acct_test', + stripe_publishable_key: 'pk_test', + })) + + resp = OAuth.token({ + grant_type: 'authorization_code', + code: 'this_is_an_authorization_code', + }) + assert_equal('sk_access_token', resp.access_token) + end + end + + context ".deauthorize" do + should "deauthorize an account" do + # The OpenAPI fixtures don't cover the OAuth endpoints, so we just + # stub the request manually. + stub_request(:post, "#{Stripe.connect_base}/oauth/deauthorize"). + with(body: { + 'client_id' => 'ca_test', + 'stripe_user_id' => 'acct_test_deauth', + }). + to_return(body: JSON.generate({ + stripe_user_id: 'acct_test_deauth', + })) + + resp = OAuth.deauthorize({stripe_user_id: 'acct_test_deauth'}) + assert_equal('acct_test_deauth', resp.stripe_user_id) + end + end + end +end diff --git a/test/stripe/stripe_client_test.rb b/test/stripe/stripe_client_test.rb index 5b8ae157..5b30be15 100644 --- a/test/stripe/stripe_client_test.rb +++ b/test/stripe/stripe_client_test.rb @@ -243,16 +243,16 @@ module Stripe assert_equal 'Invalid response object from API: "" (HTTP response code was 200)', e.message end - should "handle error response with non-object error value" do + should "handle error response with unknown value" do stub_request(:post, "#{Stripe.api_base}/v1/charges"). - to_return(body: JSON.generate({ error: "foo" }), status: 500) + to_return(body: JSON.generate({ bar: "foo" }), status: 500) client = StripeClient.new e = assert_raises Stripe::APIError do client.execute_request(:post, '/v1/charges') end - assert_equal 'Invalid response object from API: "{\"error\":\"foo\"}" (HTTP response code was 500)', e.message + assert_equal 'Invalid response object from API: "{\"bar\":\"foo\"}" (HTTP response code was 500)', e.message end should "raise InvalidRequestError on 400" do @@ -332,6 +332,42 @@ module Stripe assert_equal(true, e.json_body.kind_of?(Hash)) end end + + should "raise OAuth::InvalidRequestError when error is a string with value 'invalid_request'" do + stub_request(:post, "#{Stripe.connect_base}/oauth/token"). + to_return(body: JSON.generate({ + error: "invalid_request", + error_description: "No grant type specified", + }), status: 400) + + client = StripeClient.new + opts = {api_base: Stripe.connect_base} + e = assert_raises Stripe::OAuth::InvalidRequestError do + client.execute_request(:post, '/oauth/token', opts) + end + + assert_equal(400, e.http_status) + assert_equal(true, !!e.http_body) + assert_equal('No grant type specified', e.message) + end + + should "raise OAuth::InvalidGrantError when error is a string with value 'invalid_grant'" do + stub_request(:post, "#{Stripe.connect_base}/oauth/token"). + to_return(body: JSON.generate({ + error: "invalid_grant", + error_description: "This authorization code has already been used. All tokens issued with this code have been revoked.", + }), status: 400) + + client = StripeClient.new + opts = {api_base: Stripe.connect_base} + e = assert_raises Stripe::OAuth::InvalidGrantError do + client.execute_request(:post, '/oauth/token', opts) + end + + assert_equal(400, e.http_status) + assert_equal('invalid_grant', e.code) + assert_equal('This authorization code has already been used. All tokens issued with this code have been revoked.', e.message) + end end context "idempotency keys" do