diff --git a/README.rdoc b/README.rdoc index a4b0914626..9fc5777a04 100644 --- a/README.rdoc +++ b/README.rdoc @@ -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) @@ -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 diff --git a/lib/active_resource/base.rb b/lib/active_resource/base.rb index 11c266bbb5..ac9a17b1ae 100644 --- a/lib/active_resource/base.rb +++ b/lib/active_resource/base.rb @@ -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 @@ -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. See ssl_options= for all options. @@ -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. @@ -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 @@ -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 diff --git a/test/cases/base_test.rb b/test/cases/base_test.rb index f95ae093a7..a5292dfcf2 100644 --- a/test/cases/base_test.rb +++ b/test/cases/base_test.rb @@ -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 @@ -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" @@ -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 @@ -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)