diff --git a/lib/convertkit/oauth.rb b/lib/convertkit/oauth.rb index 3317ebe..2c00d1a 100644 --- a/lib/convertkit/oauth.rb +++ b/lib/convertkit/oauth.rb @@ -1,3 +1,5 @@ +require 'net/http' + module ConvertKit # Use this class to get an access token or refresh token using the OAuth2 flow. The client id and client secret # should be obtained from ConvertKit and is required for this flow. The code should be obtained when initially connect @@ -41,15 +43,24 @@ def refresh_token(option = {}) AccessTokenResponse.new(response) end + # POSTs a form-encoded RFC 7009 revoke request via Net::HTTP. The gem's Connection class + # sends application/json, which Kit's /oauth/revoke cannot parse; per RFC 7009 the endpoint + # returns 200 even for unparseable requests, so any Content-Type other than form-encoded + # would silently no-op without actually revoking the token. def revoke_token(token) - params = { + response = Net::HTTP.post_form( + URI.join(URL, REVOKE_PATH), + token: token, client_id: @id, client_secret: @secret, - token: token - } + token_type_hint: 'access_token' + ) + + unless response.code.to_i.between?(200, 299) + raise ConvertKit::OauthError, "Kit /oauth/revoke returned HTTP #{response.code}: #{response.body}" + end - response = handle_response(@connection.post(REVOKE_PATH, params), true) - response.success? + true end private diff --git a/spec/lib/convertkit/oauth_spec.rb b/spec/lib/convertkit/oauth_spec.rb index fd00ce9..6a02a4d 100644 --- a/spec/lib/convertkit/oauth_spec.rb +++ b/spec/lib/convertkit/oauth_spec.rb @@ -130,22 +130,42 @@ describe '#revoke_token' do let(:oauth) { ConvertKit::OAuth.new(client_id, client_secret, redirect_uri: redirect_uri, code: code, refresh_token: refresh_token) } - let(:revoke_path) { 'oauth/revoke' } + let(:revoke_uri) { URI('https://app.convertkit.com/oauth/revoke') } let(:token) { 'random_token' } + let(:net_response) { double('net_response') } before do allow(ConvertKit::Connection).to receive(:new).with(url).and_return(connection) - allow(response).to receive(:success?).and_return(true) end - it 'returns success response' do - expect(connection).to receive(:post).with(revoke_path, { - client_id: client_id, - client_secret: client_secret, - token: token, - }).and_return(response) + context 'when Kit returns 2xx' do + before do + allow(net_response).to receive(:code).and_return('200') + end + + it 'POSTs form-encoded params to app.convertkit.com/oauth/revoke and returns true' do + expect(Net::HTTP).to receive(:post_form).with( + revoke_uri, + token: token, + client_id: client_id, + client_secret: client_secret, + token_type_hint: 'access_token' + ).and_return(net_response) + + expect(oauth.revoke_token(token)).to eq(true) + end + end - expect(oauth.revoke_token(token)).to eq(true) + context 'when Kit returns a non-2xx response' do + before do + allow(net_response).to receive(:code).and_return('401') + allow(net_response).to receive(:body).and_return('{"error":"unauthorized"}') + allow(Net::HTTP).to receive(:post_form).and_return(net_response) + end + + it 'raises ConvertKit::OauthError including the HTTP status and body' do + expect { oauth.revoke_token(token) }.to raise_error(ConvertKit::OauthError, /401.*unauthorized/) + end end end