Skip to content

Commit ac2f7d2

Browse files
committed
Add support to set bearer token connection through Base class
1 parent dedecf9 commit ac2f7d2

File tree

3 files changed

+123
-17
lines changed

3 files changed

+123
-17
lines changed

README.rdoc

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ life cycle methods that operate against a persistent store.
5353
As you can see, the methods are quite similar to Active Record's methods for dealing with database
5454
records. But rather than dealing directly with a database record, you're dealing with HTTP resources (which may or may not be database records).
5555

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

5959
ActiveResource::Base.site = api_site_for(request)
@@ -69,28 +69,24 @@ Active Resource supports the token based authentication provided by Rails throug
6969
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:
7070

7171
ActiveResource::Base.headers['Authorization'] = current_session_api_token
72-
73-
Global Authentication to be used across all subclasses of ActiveResource::Base should be handled using the ActiveResource::Connection class.
74-
75-
ActiveResource::Base.connection.auth_type = :bearer
76-
ActiveResource::Base.connection.bearer_token = @bearer_token
77-
78-
class Person < ActiveResource::Base
79-
self.connection.auth_type = :bearer
80-
self.connection.bearer_token = @bearer_token
81-
end
8272

8373
ActiveResource supports 2 options for HTTP authentication today.
8474

8575
1. Basic
8676

87-
ActiveResource::Connection.new("http://my%40email.com:%31%32%33@localhost")
77+
class Person < ActiveResource::Base
78+
self.user = 'my@email.com'
79+
self.password = '123'
80+
end
8881
# username: my@email.com password: 123
8982

9083
2. Bearer Token
9184

92-
ActiveResource::Base.connection.auth_type = :bearer
93-
ActiveResource::Base.connection.bearer_token = @bearer_token
85+
class Person < ActiveResource::Base
86+
self.auth_type = :bearer
87+
self.bearer_token = 'my-token123'
88+
end
89+
# Bearer my-token123
9490

9591
==== Protocol
9692

lib/active_resource/base.rb

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,19 @@ module ActiveResource
133133
# doesn't require SSL. However, this doesn't mean the connection is secure!
134134
# Just the username and password.
135135
#
136+
# Another common way to authenticate requests is via bearer tokens, a scheme
137+
# originally created as part of the OAuth 2.0 protocol (see RFC 6750).
138+
#
139+
# Bearer authentication sends a token, that can maybe only be a short string
140+
# of hexadecimal characters or even a JWT Token. Similarly to the Basic
141+
# authentication, this scheme should only be used with SSL.
142+
#
136143
# (You really, really want to use SSL. There's little reason not to.)
137144
#
138145
# === Picking an authentication scheme
139146
#
140-
# Basic authentication is the default. To switch to digest authentication,
141-
# set +auth_type+ to +:digest+:
147+
# Basic authentication is the default. To switch to digest or bearer token authentication,
148+
# set +auth_type+ to +:digest+ or +:bearer+:
142149
#
143150
# class Person < ActiveResource::Base
144151
# self.auth_type = :digest
@@ -157,6 +164,16 @@ module ActiveResource
157164
# self.site = "https://ryan:password@api.people.com"
158165
# end
159166
#
167+
# === Setting the bearer token
168+
#
169+
# Set +bearer_token+ on the class:
170+
#
171+
# class Person < ActiveResource::Base
172+
# # Set bearer token directly:
173+
# self.auth_type = :bearer
174+
# self.bearer_token = "my-bearer-token"
175+
# end
176+
#
160177
# === Certificate Authentication
161178
#
162179
# You can also authenticate using an X509 certificate. <tt>See ssl_options=</tt> for all options.
@@ -321,7 +338,7 @@ def self.logger=(logger)
321338

322339
class << self
323340
include ThreadsafeAttributes
324-
threadsafe_attribute :_headers, :_connection, :_user, :_password, :_site, :_proxy
341+
threadsafe_attribute :_headers, :_connection, :_user, :_password, :_bearer_token, :_site, :_proxy
325342

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

547+
# Gets the \bearer_token for REST HTTP authentication.
548+
def bearer_token
549+
# Not using superclass_delegating_reader. See +site+ for explanation
550+
if _bearer_token_defined?
551+
_bearer_token
552+
elsif superclass != Object && superclass.bearer_token
553+
superclass.bearer_token.dup.freeze
554+
end
555+
end
556+
557+
# Sets the \bearer_token for REST HTTP authentication.
558+
def bearer_token=(bearer_token)
559+
self._connection = nil
560+
self._bearer_token = bearer_token
561+
end
562+
530563
def auth_type
531564
if defined?(@auth_type)
532565
@auth_type
@@ -651,6 +684,7 @@ def connection(refresh = false)
651684
_connection.proxy = proxy if proxy
652685
_connection.user = user if user
653686
_connection.password = password if password
687+
_connection.bearer_token = bearer_token if bearer_token
654688
_connection.auth_type = auth_type if auth_type
655689
_connection.timeout = timeout if timeout
656690
_connection.open_timeout = open_timeout if open_timeout

test/cases/base_test.rb

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,20 @@ def test_should_accept_setting_password
9090
assert_equal("test123", Forum.connection.password)
9191
end
9292

93+
def test_should_accept_setting_bearer_token
94+
Forum.bearer_token = "token123"
95+
assert_equal("token123", Forum.bearer_token)
96+
assert_equal("token123", Forum.connection.bearer_token)
97+
end
98+
9399
def test_should_accept_setting_auth_type
94100
Forum.auth_type = :digest
95101
assert_equal(:digest, Forum.auth_type)
96102
assert_equal(:digest, Forum.connection.auth_type)
103+
104+
Forum.auth_type = :bearer
105+
assert_equal(:bearer, Forum.auth_type)
106+
assert_equal(:bearer, Forum.connection.auth_type)
97107
end
98108

99109
def test_should_accept_setting_timeout
@@ -141,6 +151,16 @@ def test_password_variable_can_be_reset
141151
assert_nil actor.connection.password
142152
end
143153

154+
def test_bearer_token_variable_can_be_reset
155+
actor = Class.new(ActiveResource::Base)
156+
actor.site = "http://cinema"
157+
assert_nil actor.bearer_token
158+
actor.bearer_token = "token"
159+
actor.bearer_token = nil
160+
assert_nil actor.bearer_token
161+
assert_nil actor.connection.bearer_token
162+
end
163+
144164
def test_timeout_variable_can_be_reset
145165
actor = Class.new(ActiveResource::Base)
146166
actor.site = "http://cinema"
@@ -358,6 +378,46 @@ def test_password_reader_uses_superclass_password_until_written
358378
assert_equal fruit.password, apple.password, "subclass did not adopt changes from parent class"
359379
end
360380

381+
def test_bearer_token_reader_uses_superclass_bearer_token_until_written
382+
# Superclass is Object so returns nil.
383+
assert_nil ActiveResource::Base.bearer_token
384+
assert_nil Class.new(ActiveResource::Base).bearer_token
385+
Person.bearer_token = "my-token".dup
386+
387+
# Subclass uses superclass bearer_token.
388+
actor = Class.new(Person)
389+
assert_equal Person.bearer_token, actor.bearer_token
390+
391+
# Subclass returns frozen superclass copy.
392+
assert_not Person.bearer_token.frozen?
393+
assert actor.bearer_token.frozen?
394+
395+
# Changing subclass bearer_token doesn't change superclass bearer_token.
396+
actor.bearer_token = "token123"
397+
assert_not_equal Person.bearer_token, actor.bearer_token
398+
399+
# Changing superclass bearer_token doesn't overwrite subclass bearer_token.
400+
Person.bearer_token = "super-secret-token"
401+
assert_not_equal Person.bearer_token, actor.bearer_token
402+
403+
# Changing superclass bearer_token after subclassing changes subclass bearer_token.
404+
jester = Class.new(actor)
405+
actor.bearer_token = "super-secret-token123"
406+
assert_equal actor.bearer_token, jester.bearer_token
407+
408+
# Subclasses are always equal to superclass bearer_token when not overridden
409+
fruit = Class.new(ActiveResource::Base)
410+
apple = Class.new(fruit)
411+
412+
fruit.bearer_token = "mega-secret-token"
413+
assert_equal fruit.bearer_token, apple.bearer_token, "subclass did not adopt changes from parent class"
414+
415+
fruit.bearer_token = "ok-token"
416+
assert_equal fruit.bearer_token, apple.bearer_token, "subclass did not adopt changes from parent class"
417+
418+
Person.bearer_token = nil
419+
end
420+
361421
def test_timeout_reader_uses_superclass_timeout_until_written
362422
# Superclass is Object so returns nil.
363423
assert_nil ActiveResource::Base.timeout
@@ -558,6 +618,22 @@ def test_updating_baseclass_password_wipes_descendent_cached_connection_objects
558618
assert_not_equal(first_connection, second_connection, "Connection should be re-created")
559619
end
560620

621+
def test_updating_baseclass_bearer_token_wipes_descendent_cached_connection_objects
622+
# Subclasses are always equal to superclass bearer_token when not overridden
623+
fruit = Class.new(ActiveResource::Base)
624+
apple = Class.new(fruit)
625+
fruit.site = "http://market"
626+
627+
fruit.bearer_token = "my-token"
628+
assert_equal fruit.connection.bearer_token, apple.connection.bearer_token
629+
first_connection = apple.connection.object_id
630+
631+
fruit.bearer_token = "another-token"
632+
assert_equal fruit.connection.bearer_token, apple.connection.bearer_token
633+
second_connection = apple.connection.object_id
634+
assert_not_equal(first_connection, second_connection, "Connection should be re-created")
635+
end
636+
561637
def test_updating_baseclass_timeout_wipes_descendent_cached_connection_objects
562638
# Subclasses are always equal to superclass timeout when not overridden
563639
fruit = Class.new(ActiveResource::Base)

0 commit comments

Comments
 (0)