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
24 changes: 10 additions & 14 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ life cycle methods that operate against a persistent store.
As you can see, the methods are quite similar to Active Record's methods for dealing with database
records. But rather than dealing directly with a database record, you're dealing with HTTP resources (which may or may not be database records).

Connection settings (`site`, `headers`, `user`, `password`, `proxy`) and the connections themselves are store in
Connection settings (`site`, `headers`, `user`, `password`, `bearer_token`, `proxy`) and the connections themselves are store in
thread-local variables to make them thread-safe, so you can also set these dynamically, even in a multi-threaded environment, for instance:

ActiveResource::Base.site = api_site_for(request)
Expand All @@ -69,28 +69,24 @@ Active Resource supports the token based authentication provided by Rails throug
You can also set any specific HTTP header using the same way. As mentioned above, headers are thread-safe, so you can set headers dynamically, even in a multi-threaded environment:

ActiveResource::Base.headers['Authorization'] = current_session_api_token

Global Authentication to be used across all subclasses of ActiveResource::Base should be handled using the ActiveResource::Connection class.

ActiveResource::Base.connection.auth_type = :bearer
ActiveResource::Base.connection.bearer_token = @bearer_token

class Person < ActiveResource::Base
self.connection.auth_type = :bearer
self.connection.bearer_token = @bearer_token
end

ActiveResource supports 2 options for HTTP authentication today.

1. Basic

ActiveResource::Connection.new("http://my%40email.com:%31%32%33@localhost")
class Person < ActiveResource::Base
self.user = 'my@email.com'
self.password = '123'
end
# username: my@email.com password: 123

2. Bearer Token

ActiveResource::Base.connection.auth_type = :bearer
ActiveResource::Base.connection.bearer_token = @bearer_token
class Person < ActiveResource::Base
self.auth_type = :bearer
self.bearer_token = 'my-token123'
end
# Bearer my-token123

==== Protocol

Expand Down
40 changes: 37 additions & 3 deletions lib/active_resource/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,19 @@ module ActiveResource
# doesn't require SSL. However, this doesn't mean the connection is secure!
# Just the username and password.
#
# Another common way to authenticate requests is via bearer tokens, a scheme
# originally created as part of the OAuth 2.0 protocol (see RFC 6750).
#
# Bearer authentication sends a token, that can maybe only be a short string
# of hexadecimal characters or even a JWT Token. Similarly to the Basic
# authentication, this scheme should only be used with SSL.
#
# (You really, really want to use SSL. There's little reason not to.)
#
# === Picking an authentication scheme
#
# Basic authentication is the default. To switch to digest authentication,
# set +auth_type+ to +:digest+:
# Basic authentication is the default. To switch to digest or bearer token authentication,
# set +auth_type+ to +:digest+ or +:bearer+:
#
# class Person < ActiveResource::Base
# self.auth_type = :digest
Expand All @@ -157,6 +164,16 @@ module ActiveResource
# self.site = "https://ryan:password@api.people.com"
# end
#
# === Setting the bearer token
#
# Set +bearer_token+ on the class:
#
# class Person < ActiveResource::Base
# # Set bearer token directly:
# self.auth_type = :bearer
# self.bearer_token = "my-bearer-token"
# end
#
# === Certificate Authentication
#
# You can also authenticate using an X509 certificate. <tt>See ssl_options=</tt> for all options.
Expand Down Expand Up @@ -321,7 +338,7 @@ def self.logger=(logger)

class << self
include ThreadsafeAttributes
threadsafe_attribute :_headers, :_connection, :_user, :_password, :_site, :_proxy
threadsafe_attribute :_headers, :_connection, :_user, :_password, :_bearer_token, :_site, :_proxy

# Creates a schema for this resource - setting the attributes that are
# known prior to fetching an instance from the remote system.
Expand Down Expand Up @@ -527,6 +544,22 @@ def password=(password)
self._password = password
end

# Gets the \bearer_token for REST HTTP authentication.
def bearer_token
# Not using superclass_delegating_reader. See +site+ for explanation
if _bearer_token_defined?
_bearer_token
elsif superclass != Object && superclass.bearer_token
superclass.bearer_token.dup.freeze
end
end

# Sets the \bearer_token for REST HTTP authentication.
def bearer_token=(bearer_token)
self._connection = nil
self._bearer_token = bearer_token
end

def auth_type
if defined?(@auth_type)
@auth_type
Expand Down Expand Up @@ -651,6 +684,7 @@ def connection(refresh = false)
_connection.proxy = proxy if proxy
_connection.user = user if user
_connection.password = password if password
_connection.bearer_token = bearer_token if bearer_token
_connection.auth_type = auth_type if auth_type
_connection.timeout = timeout if timeout
_connection.open_timeout = open_timeout if open_timeout
Expand Down
76 changes: 76 additions & 0 deletions test/cases/base_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,20 @@ def test_should_accept_setting_password
assert_equal("test123", Forum.connection.password)
end

def test_should_accept_setting_bearer_token
Forum.bearer_token = "token123"
assert_equal("token123", Forum.bearer_token)
assert_equal("token123", Forum.connection.bearer_token)
end

def test_should_accept_setting_auth_type
Forum.auth_type = :digest
assert_equal(:digest, Forum.auth_type)
assert_equal(:digest, Forum.connection.auth_type)

Forum.auth_type = :bearer
assert_equal(:bearer, Forum.auth_type)
assert_equal(:bearer, Forum.connection.auth_type)
end

def test_should_accept_setting_timeout
Expand Down Expand Up @@ -141,6 +151,16 @@ def test_password_variable_can_be_reset
assert_nil actor.connection.password
end

def test_bearer_token_variable_can_be_reset
actor = Class.new(ActiveResource::Base)
actor.site = "http://cinema"
assert_nil actor.bearer_token
actor.bearer_token = "token"
actor.bearer_token = nil
assert_nil actor.bearer_token
assert_nil actor.connection.bearer_token
end

def test_timeout_variable_can_be_reset
actor = Class.new(ActiveResource::Base)
actor.site = "http://cinema"
Expand Down Expand Up @@ -358,6 +378,46 @@ def test_password_reader_uses_superclass_password_until_written
assert_equal fruit.password, apple.password, "subclass did not adopt changes from parent class"
end

def test_bearer_token_reader_uses_superclass_bearer_token_until_written
# Superclass is Object so returns nil.
assert_nil ActiveResource::Base.bearer_token
assert_nil Class.new(ActiveResource::Base).bearer_token
Person.bearer_token = "my-token".dup

# Subclass uses superclass bearer_token.
actor = Class.new(Person)
assert_equal Person.bearer_token, actor.bearer_token

# Subclass returns frozen superclass copy.
assert_not Person.bearer_token.frozen?
assert actor.bearer_token.frozen?

# Changing subclass bearer_token doesn't change superclass bearer_token.
actor.bearer_token = "token123"
assert_not_equal Person.bearer_token, actor.bearer_token

# Changing superclass bearer_token doesn't overwrite subclass bearer_token.
Person.bearer_token = "super-secret-token"
assert_not_equal Person.bearer_token, actor.bearer_token

# Changing superclass bearer_token after subclassing changes subclass bearer_token.
jester = Class.new(actor)
actor.bearer_token = "super-secret-token123"
assert_equal actor.bearer_token, jester.bearer_token

# Subclasses are always equal to superclass bearer_token when not overridden
fruit = Class.new(ActiveResource::Base)
apple = Class.new(fruit)

fruit.bearer_token = "mega-secret-token"
assert_equal fruit.bearer_token, apple.bearer_token, "subclass did not adopt changes from parent class"

fruit.bearer_token = "ok-token"
assert_equal fruit.bearer_token, apple.bearer_token, "subclass did not adopt changes from parent class"

Person.bearer_token = nil
end

def test_timeout_reader_uses_superclass_timeout_until_written
# Superclass is Object so returns nil.
assert_nil ActiveResource::Base.timeout
Expand Down Expand Up @@ -558,6 +618,22 @@ def test_updating_baseclass_password_wipes_descendent_cached_connection_objects
assert_not_equal(first_connection, second_connection, "Connection should be re-created")
end

def test_updating_baseclass_bearer_token_wipes_descendent_cached_connection_objects
# Subclasses are always equal to superclass bearer_token when not overridden
fruit = Class.new(ActiveResource::Base)
apple = Class.new(fruit)
fruit.site = "http://market"

fruit.bearer_token = "my-token"
assert_equal fruit.connection.bearer_token, apple.connection.bearer_token
first_connection = apple.connection.object_id

fruit.bearer_token = "another-token"
assert_equal fruit.connection.bearer_token, apple.connection.bearer_token
second_connection = apple.connection.object_id
assert_not_equal(first_connection, second_connection, "Connection should be re-created")
end

def test_updating_baseclass_timeout_wipes_descendent_cached_connection_objects
# Subclasses are always equal to superclass timeout when not overridden
fruit = Class.new(ActiveResource::Base)
Expand Down