Merge pull request #523 from stripe/ob-oauth

Add OAuth methods
This commit is contained in:
Brandur 2017-05-18 12:56:19 -07:00 committed by GitHub
commit 429afa959f
7 changed files with 274 additions and 21 deletions

View File

@ -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

View File

@ -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

View File

@ -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

56
lib/stripe/oauth.rb Normal file
View 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

View File

@ -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)

85
test/stripe/oauth_test.rb Normal file
View 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

View File

@ -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