diff --git a/lib/twitter/api/oauth.rb b/lib/twitter/api/oauth.rb index 29b6d1eb0..ad9c4928c 100644 --- a/lib/twitter/api/oauth.rb +++ b/lib/twitter/api/oauth.rb @@ -6,6 +6,25 @@ module API module OAuth include Twitter::API::Utils + # Allows a registered application to obtain an OAuth 2 Bearer Token, which can be used to make API requests + # on an application's own behalf, without a user context. + # + # Only one bearer token may exist outstanding for an application, and repeated requests to this method + # will yield the same already-existent token until it has been invalidated. + # + # @see https://dev.twitter.com/docs/api/1.1/post/oauth2/token + # @rate_limited No + # @authentication Required + # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid. + # @return [Twitter::Token] The Bearer Token. token_type should be 'bearer'. + # @example Generate a Bearer Token + # client = Twitter::Client.new :consumer_key => "abc", :consumer_secret => 'def' + # bearer_token = client.token + def token + object_from_response(Twitter::Token, :post, "/oauth2/token", :grant_type => "client_credentials", :bearer_token_request => true) + end + alias bearer_token token + # Allows a registered application to revoke an issued OAuth 2 Bearer Token by presenting its client credentials. # # @see https://dev.twitter.com/docs/api/1.1/post/oauth2/invalidate_token diff --git a/lib/twitter/client.rb b/lib/twitter/client.rb index ca3392032..28f70bdd6 100644 --- a/lib/twitter/client.rb +++ b/lib/twitter/client.rb @@ -20,6 +20,7 @@ require 'twitter/error/client_error' require 'twitter/error/decode_error' require 'simple_oauth' +require 'base64' require 'uri' module Twitter @@ -78,11 +79,33 @@ def put(path, params={}) end private + # Returns a proc that can be used to setup the Faraday::Request headers + # + # @param method [Symbol] + # @param path [String] + # @param params [Hash] + # @return [Proc] + def request_setup(method, path, params) + if params.delete :bearer_token_request + Proc.new do |request| + request.headers[:authorization] = bearer_token_credentials_auth_header + request.headers[:content_type] = 'application/x-www-form-urlencoded; charset=UTF-8' + request.headers[:accept] = '*/*' # It is important we set this, otherwise we get an error. + end + elsif application_only_auth? + Proc.new do |request| + request.headers[:authorization] = bearer_auth_header + end + else + Proc.new do |request| + request.headers[:authorization] = oauth_auth_header(method, path, params).to_s + end + end + end def request(method, path, params={}, signature_params=params) - connection.send(method.to_sym, path, params) do |request| - request.headers[:authorization] = auth_header(method.to_sym, path, signature_params).to_s - end.env + request_setup = request_setup(method, path, params) + connection.send(method.to_sym, path, params, &request_setup).env rescue Faraday::Error::ClientError raise Twitter::Error::ClientError rescue MultiJson::DecodeError @@ -100,10 +123,21 @@ def connection end end - def auth_header(method, path, params={}) + # Generates authentication header for a bearer token request + # + # @return [String] + def bearer_token_credentials_auth_header + basic_auth_token = Base64.strict_encode64("#{@consumer_key}:#{@consumer_secret}") + "Basic #{basic_auth_token}" + end + + def bearer_auth_header + "Bearer #{@bearer_token}" + end + + def oauth_auth_header(method, path, params={}) uri = URI(@endpoint + path) SimpleOAuth::Header.new(method, uri, params, credentials) end - end end diff --git a/lib/twitter/configurable.rb b/lib/twitter/configurable.rb index 0dc3bac80..a0ed00f54 100644 --- a/lib/twitter/configurable.rb +++ b/lib/twitter/configurable.rb @@ -4,7 +4,7 @@ module Twitter module Configurable extend Forwardable - attr_writer :consumer_key, :consumer_secret, :oauth_token, :oauth_token_secret + attr_writer :consumer_key, :consumer_secret, :oauth_token, :oauth_token_secret, :bearer_token attr_accessor :endpoint, :connection_options, :identity_map, :middleware def_delegator :options, :hash @@ -16,6 +16,7 @@ def keys :consumer_secret, :oauth_token, :oauth_token_secret, + :bearer_token, :endpoint, :connection_options, :identity_map, @@ -37,7 +38,7 @@ def configure # @return [Boolean] def credentials? - credentials.values.all? + credentials.values.all? || @bearer_token end def reset! @@ -49,6 +50,9 @@ def reset! alias setup reset! private + def application_only_auth? + !!@bearer_token + end # @return [Hash] def credentials diff --git a/lib/twitter/default.rb b/lib/twitter/default.rb index 92205961f..3f05bce1d 100644 --- a/lib/twitter/default.rb +++ b/lib/twitter/default.rb @@ -69,6 +69,11 @@ def oauth_token_secret ENV['TWITTER_OAUTH_TOKEN_SECRET'] end + # @return [String] + def bearer_token + ENV['TWITTER_BEARER_TOKEN'] + end + # @note This is configurable in case you want to use a Twitter-compatible endpoint. # @see http://status.net/wiki/Twitter-compatible_API # @see http://en.blog.wordpress.com/2009/12/12/twitter-api/ diff --git a/spec/twitter/api/oauth_spec.rb b/spec/twitter/api/oauth_spec.rb index 4036ef2dd..28e256fc1 100644 --- a/spec/twitter/api/oauth_spec.rb +++ b/spec/twitter/api/oauth_spec.rb @@ -3,7 +3,31 @@ describe Twitter::API::OAuth do before do - @client = Twitter::Client.new + @client = Twitter::Client.new :consumer_key => 'CK', :consumer_secret => 'CS' + end + + describe "#token" do + before do + # WebMock treats Basic Auth differently so we have to chack against the full url with credentials. + @oauth2_token_url = "https://CK:CS@api.twitter.com/oauth2/token" + stub_request(:post, @oauth2_token_url).with(:body => "grant_type=client_credentials").to_return(:body => '{"token_type" : "bearer", "access_token" : "AAAA%2FAAA%3DAAAAAAAA"}', :headers => {:content_type => "application/json; charset=utf-8"}) + end + it "requests the correct resource" do + @client.token + expect(a_request(:post, @oauth2_token_url).with(:body => {:grant_type => "client_credentials"})).to have_been_made + end + it "requests with the correct headers" do + @client.token + expect(a_request(:post, @oauth2_token_url).with(:headers => { + :content_type => "application/x-www-form-urlencoded; charset=UTF-8", + :accept => "*/*" + })).to have_been_made + end + it "returns the bearer token" do + token = @client.token + expect(token.access_token).to eq "AAAA%2FAAA%3DAAAAAAAA" + expect(token.token_type).to eq "bearer" + end end describe "#invalidate_token" do diff --git a/spec/twitter/client_spec.rb b/spec/twitter/client_spec.rb index 2bc282c71..2c28a7e6b 100644 --- a/spec/twitter/client_spec.rb +++ b/spec/twitter/client_spec.rb @@ -38,6 +38,7 @@ :middleware => Proc.new{}, :oauth_token => 'OT', :oauth_token_secret => 'OS', + :bearer_token => 'BT', :identity_map => ::Hash } end @@ -139,10 +140,10 @@ end end - describe "#auth_header" do + describe "#oauth_auth_header" do it "creates the correct auth headers" do uri = "/1.1/direct_messages.json" - authorization = subject.send(:auth_header, :get, uri) + authorization = subject.send(:oauth_auth_header, :get, uri) expect(authorization.options[:signature_method]).to eq "HMAC-SHA1" expect(authorization.options[:version]).to eq "1.0" expect(authorization.options[:consumer_key]).to eq "CK" @@ -152,4 +153,23 @@ end end + describe "#bearer_auth_header" do + subject do + Twitter::Client.new(:bearer_token => "BT") + end + + it "creates the correct auth headers with supplied bearer_token" do + uri = "/1.1/direct_messages.json" + authorization = subject.send(:bearer_auth_header) + expect(authorization).to eq "Bearer BT" + end + end + + describe "#bearer_token_credentials_auth_header" do + it "creates the correct auth header with supplied consumer_key and consumer_secret" do + uri = "/1.1/direct_messages.json" + authorization = subject.send(:bearer_token_credentials_auth_header) + expect(authorization).to eq "Basic #{Base64.strict_encode64("CK:CS")}" + end + end end diff --git a/spec/twitter_spec.rb b/spec/twitter_spec.rb index abb4ffe16..1cc5ace95 100644 --- a/spec/twitter_spec.rb +++ b/spec/twitter_spec.rb @@ -91,7 +91,13 @@ end describe ".credentials?" do - it "returns true if all credentials are present" do + it "returns true if only bearer_token is supplied" do + Twitter.configure do |config| + config.bearer_token = 'BT' + end + expect(Twitter.credentials?).to be_true + end + it "returns true if all oauth credentials are present" do Twitter.configure do |config| config.consumer_key = 'CK' config.consumer_secret = 'CS'