Skip to content

Commit

Permalink
Merge pull request #387 from paracycle/bearer_token
Browse files Browse the repository at this point in the history
Adding Bearer Token support
  • Loading branch information
sferik committed Apr 18, 2013
2 parents b30e18c + 2c20ecc commit 6df3d85
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 11 deletions.
19 changes: 19 additions & 0 deletions lib/twitter/api/oauth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 39 additions & 5 deletions lib/twitter/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
require 'twitter/error/client_error'
require 'twitter/error/decode_error'
require 'simple_oauth'
require 'base64'
require 'uri'

module Twitter
Expand Down Expand Up @@ -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
Expand All @@ -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
8 changes: 6 additions & 2 deletions lib/twitter/configurable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -16,6 +16,7 @@ def keys
:consumer_secret,
:oauth_token,
:oauth_token_secret,
:bearer_token,
:endpoint,
:connection_options,
:identity_map,
Expand All @@ -37,7 +38,7 @@ def configure

# @return [Boolean]
def credentials?
credentials.values.all?
credentials.values.all? || @bearer_token
end

def reset!
Expand All @@ -49,6 +50,9 @@ def reset!
alias setup reset!

private
def application_only_auth?
!!@bearer_token
end

# @return [Hash]
def credentials
Expand Down
5 changes: 5 additions & 0 deletions lib/twitter/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
26 changes: 25 additions & 1 deletion spec/twitter/api/oauth_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 22 additions & 2 deletions spec/twitter/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
:middleware => Proc.new{},
:oauth_token => 'OT',
:oauth_token_secret => 'OS',
:bearer_token => 'BT',
:identity_map => ::Hash
}
end
Expand Down Expand Up @@ -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"
Expand All @@ -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
8 changes: 7 additions & 1 deletion spec/twitter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down

1 comment on commit 6df3d85

@paulwalker
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've played with this for about an hour and I believe a couple of tweaks are needed to make this production ready:

  • Requesting a bearer token does not cache the token in configuration. The user can work around this by setting it after requesting it, but not desirable
  • If a bearer_token has been configured #request_setup will force an application only request. I would instead favor a credentialed token request (if credentials are present), otherwise fallback to an app only bearer_token request (if a bearer_token is present), unless an option has been passed to favor a app only request (:app_only => true). Twitter recommends using app only request as a reserve when hitting rate limits etc. Also, the :app_only option should request a bearer_token if one is not present.

Please sign in to comment.