mirror of
https://github.com/stripe/stripe-ruby.git
synced 2025-06-03 00:01:47 -04:00
commit
429afa959f
@ -69,6 +69,9 @@ require 'stripe/three_d_secure'
|
|||||||
require 'stripe/token'
|
require 'stripe/token'
|
||||||
require 'stripe/transfer'
|
require 'stripe/transfer'
|
||||||
|
|
||||||
|
# OAuth
|
||||||
|
require 'stripe/oauth'
|
||||||
|
|
||||||
module Stripe
|
module Stripe
|
||||||
DEFAULT_CA_BUNDLE_PATH = File.dirname(__FILE__) + '/data/ca-certificates.crt'
|
DEFAULT_CA_BUNDLE_PATH = File.dirname(__FILE__) + '/data/ca-certificates.crt'
|
||||||
|
|
||||||
@ -90,7 +93,7 @@ module Stripe
|
|||||||
@read_timeout = 80
|
@read_timeout = 80
|
||||||
|
|
||||||
class << self
|
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
|
:open_timeout, :read_timeout
|
||||||
|
|
||||||
attr_reader :max_network_retry_delay, :initial_network_retry_delay
|
attr_reader :max_network_retry_delay, :initial_network_retry_delay
|
||||||
|
@ -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\'')
|
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
|
end
|
||||||
|
|
||||||
def deauthorize(client_id, opts={})
|
def deauthorize(client_id=nil, opts={})
|
||||||
opts = {:api_base => Stripe.connect_base}.merge(Util.normalize_opts(opts))
|
params = {
|
||||||
resp, opts = request(:post, '/oauth/deauthorize', { 'client_id' => client_id, 'stripe_user_id' => self.id }, opts)
|
client_id: client_id,
|
||||||
opts.delete(:api_base) # the api_base here is a one-off, don't persist it
|
stripe_user_id: self.id,
|
||||||
Util.convert_to_stripe_object(resp.data, opts)
|
}
|
||||||
|
OAuth.deauthorize(params, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
ARGUMENT_NOT_PROVIDED = Object.new
|
ARGUMENT_NOT_PROVIDED = Object.new
|
||||||
|
@ -100,4 +100,44 @@ module Stripe
|
|||||||
@sig_header = sig_header
|
@sig_header = sig_header
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
56
lib/stripe/oauth.rb
Normal file
56
lib/stripe/oauth.rb
Normal file
@ -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 = <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
|
@ -197,7 +197,7 @@ module Stripe
|
|||||||
case e
|
case e
|
||||||
when Faraday::ClientError
|
when Faraday::ClientError
|
||||||
if e.response
|
if e.response
|
||||||
handle_api_error(e.response)
|
handle_error_response(e.response)
|
||||||
else
|
else
|
||||||
handle_network_error(e, retry_count, api_base)
|
handle_network_error(e, retry_count, api_base)
|
||||||
end
|
end
|
||||||
@ -229,12 +229,12 @@ module Stripe
|
|||||||
str
|
str
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_api_error(http_resp)
|
def handle_error_response(http_resp)
|
||||||
begin
|
begin
|
||||||
resp = StripeResponse.from_faraday_hash(http_resp)
|
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")
|
raise StripeError.new("Indeterminate error")
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -242,47 +242,79 @@ module Stripe
|
|||||||
raise general_api_error(http_resp[:status], http_resp[:body])
|
raise general_api_error(http_resp[:status], http_resp[:body])
|
||||||
end
|
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
|
case resp.http_status
|
||||||
when 400, 404
|
when 400, 404
|
||||||
error = InvalidRequestError.new(
|
error = InvalidRequestError.new(
|
||||||
error[:message], error[:param],
|
error_data[:message], error_data[:param],
|
||||||
http_status: resp.http_status, http_body: resp.http_body,
|
http_status: resp.http_status, http_body: resp.http_body,
|
||||||
json_body: resp.data, http_headers: resp.http_headers
|
json_body: resp.data, http_headers: resp.http_headers
|
||||||
)
|
)
|
||||||
when 401
|
when 401
|
||||||
error = AuthenticationError.new(
|
error = AuthenticationError.new(
|
||||||
error[:message],
|
error_data[:message],
|
||||||
http_status: resp.http_status, http_body: resp.http_body,
|
http_status: resp.http_status, http_body: resp.http_body,
|
||||||
json_body: resp.data, http_headers: resp.http_headers
|
json_body: resp.data, http_headers: resp.http_headers
|
||||||
)
|
)
|
||||||
when 402
|
when 402
|
||||||
error = CardError.new(
|
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,
|
http_status: resp.http_status, http_body: resp.http_body,
|
||||||
json_body: resp.data, http_headers: resp.http_headers
|
json_body: resp.data, http_headers: resp.http_headers
|
||||||
)
|
)
|
||||||
when 403
|
when 403
|
||||||
error = PermissionError.new(
|
error = PermissionError.new(
|
||||||
error[:message],
|
error_data[:message],
|
||||||
http_status: resp.http_status, http_body: resp.http_body,
|
http_status: resp.http_status, http_body: resp.http_body,
|
||||||
json_body: resp.data, http_headers: resp.http_headers
|
json_body: resp.data, http_headers: resp.http_headers
|
||||||
)
|
)
|
||||||
when 429
|
when 429
|
||||||
error = RateLimitError.new(
|
error = RateLimitError.new(
|
||||||
error[:message],
|
error_data[:message],
|
||||||
http_status: resp.http_status, http_body: resp.http_body,
|
http_status: resp.http_status, http_body: resp.http_body,
|
||||||
json_body: resp.data, http_headers: resp.http_headers
|
json_body: resp.data, http_headers: resp.http_headers
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
error = APIError.new(
|
error = APIError.new(
|
||||||
error[:message],
|
error_data[:message],
|
||||||
http_status: resp.http_status, http_body: resp.http_body,
|
http_status: resp.http_status, http_body: resp.http_body,
|
||||||
json_body: resp.data, http_headers: resp.http_headers
|
json_body: resp.data, http_headers: resp.http_headers
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
error.response = resp
|
error
|
||||||
raise(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
|
end
|
||||||
|
|
||||||
def handle_network_error(e, retry_count, api_base=nil)
|
def handle_network_error(e, retry_count, api_base=nil)
|
||||||
|
85
test/stripe/oauth_test.rb
Normal file
85
test/stripe/oauth_test.rb
Normal file
@ -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
|
@ -243,16 +243,16 @@ module Stripe
|
|||||||
assert_equal 'Invalid response object from API: "" (HTTP response code was 200)', e.message
|
assert_equal 'Invalid response object from API: "" (HTTP response code was 200)', e.message
|
||||||
end
|
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").
|
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
|
client = StripeClient.new
|
||||||
e = assert_raises Stripe::APIError do
|
e = assert_raises Stripe::APIError do
|
||||||
client.execute_request(:post, '/v1/charges')
|
client.execute_request(:post, '/v1/charges')
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
should "raise InvalidRequestError on 400" do
|
should "raise InvalidRequestError on 400" do
|
||||||
@ -332,6 +332,42 @@ module Stripe
|
|||||||
assert_equal(true, e.json_body.kind_of?(Hash))
|
assert_equal(true, e.json_body.kind_of?(Hash))
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
context "idempotency keys" do
|
context "idempotency keys" do
|
||||||
|
Loading…
x
Reference in New Issue
Block a user