Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions lib/convertkit/oauth.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
38 changes: 29 additions & 9 deletions spec/lib/convertkit/oauth_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading