access_token_wrapper 0.2.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 41caff490861ec2790a54b80f5fb40bcc7d8eacf
4
- data.tar.gz: 762c31fa85773bda6e1c6dcdb4148a90f5b1a18f
2
+ SHA256:
3
+ metadata.gz: 405caad8ee55bc656653df222a637247cbac4a6d7589383864dcb24d8d2934ed
4
+ data.tar.gz: 0d0462d4ac69b5d203fdef89383aa341459f8f8b0773e9498236dd39e7bfcce6
5
5
  SHA512:
6
- metadata.gz: fcf00843f23b13c5d6b4dd5ddf73cd4424b910c9e81e5e39d482efada0202981e4a9f1e9a1307243984f9a020b949d5987763718b2a5c68d7536096e212312b9
7
- data.tar.gz: 10a125e2293eabfab24f92fc161f5cd573ff3d84f0b723db837ef2d5e786efff89179d88fed861b796c1bd67c153a25e561d3538914b2820881f55513acb57dc
6
+ metadata.gz: fa7d945b0480387ae1c4354e3584443222e321149ec64b8e6bf533d3c0d3e9d6a45b1eef04c3efd240bbb37ed60efadf5932c330531cbcc77cc62dd989afa4f1
7
+ data.tar.gz: 11b3e0166640aef5c37e9470b15b18592c65806fd543fd8f0745fecb0966e1f65e30683e58bbdaf175f03b1fbc7c70f3a8a930a71345245d2b0cd7b2b2fcf223
@@ -1,3 +1,7 @@
1
+ ## 0.5.0 (2020-06-25)
2
+ - [FEATURE] Adds a basic configuration API with the ability to customize which exceptions are retried
3
+ - [FEATURE] Adds in `AccessTokenWrapper::FromRecord` as an alternative refresh wrapper that uses ActiveRecord locking to remove race-conditions
4
+
1
5
  ## 0.2.1 (2018-01-23)
2
6
  - [BUGFIX] Allow the request to fallback to the old style if no expires_at provided
3
7
 
@@ -1,34 +1,45 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- access_token_wrapper (0.2.0)
4
+ access_token_wrapper (0.2.1)
5
5
  oauth2
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- faraday (0.12.2)
10
+ addressable (2.7.0)
11
+ public_suffix (>= 2.0.2, < 5.0)
12
+ crack (0.4.3)
13
+ safe_yaml (~> 1.0.0)
14
+ faraday (1.0.1)
11
15
  multipart-post (>= 1.2, < 3)
12
- jwt (1.5.6)
13
- multi_json (1.12.2)
16
+ hashdiff (1.0.1)
17
+ jwt (2.2.1)
18
+ multi_json (1.14.1)
14
19
  multi_xml (0.6.0)
15
- multipart-post (2.0.0)
16
- oauth2 (1.4.0)
17
- faraday (>= 0.8, < 0.13)
18
- jwt (~> 1.0)
20
+ multipart-post (2.1.1)
21
+ oauth2 (1.4.4)
22
+ faraday (>= 0.8, < 2.0)
23
+ jwt (>= 1.0, < 3.0)
19
24
  multi_json (~> 1.3)
20
25
  multi_xml (~> 0.5)
21
26
  rack (>= 1.2, < 3)
22
- rack (2.0.3)
23
- rake (12.3.0)
27
+ public_suffix (4.0.5)
28
+ rack (2.2.2)
29
+ rake (13.0.1)
30
+ safe_yaml (1.0.5)
31
+ webmock (3.8.3)
32
+ addressable (>= 2.3.6)
33
+ crack (>= 0.3.2)
34
+ hashdiff (>= 0.4.0, < 2.0.0)
24
35
 
25
36
  PLATFORMS
26
37
  ruby
27
38
 
28
39
  DEPENDENCIES
29
40
  access_token_wrapper!
30
- bundler (~> 1.3)
31
41
  rake
42
+ webmock
32
43
 
33
44
  BUNDLED WITH
34
- 1.16.1
45
+ 2.1.4
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 TradeGecko Pte Ltd
1
+ Copyright (c) 2020 TradeGecko Pte Ltd
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -18,6 +18,7 @@ Or install it yourself as:
18
18
 
19
19
  ## Usage
20
20
 
21
+ `AccessTokenWrapper::Base`
21
22
  ```ruby
22
23
  def access_token
23
24
  @access_token ||= begin
@@ -43,8 +44,31 @@ def update_user_from_access_token(new_token)
43
44
  end
44
45
  ```
45
46
 
46
- ## Note
47
- The `AccessTokenWrapper#token` is replaced with `AccessTokenWrapper#raw_token`
47
+ or the more advanced `AccessTokenWrapper::FromRecord` that automatically locks the record on refresh to help ensure concurrency.
48
+
49
+ ```ruby
50
+ def access_token
51
+ @access_token ||= begin
52
+ token = OAuth2::AccessToken.new(oauth_client, @user.access_token,
53
+ refresh_token: @user.refresh_token,
54
+ expires_at: @user.expires_at
55
+ )
56
+ AccessTokenWrapper::FromRecord.new(client: oauth_client, record: @user) do |new_token, exception|
57
+ update_user_from_access_token(new_token)
58
+ end
59
+ end
60
+ end
61
+ ```
62
+
63
+ ## Configuration
64
+ ```ruby
65
+ AccessTokenWrapper.configure do |config|
66
+ config.skip_statuses << 520
67
+ config.skip_refresh do |response|
68
+ response.parsed['message'].start_with?('Duplicate Idempotency')
69
+ end
70
+ end
71
+ ```
48
72
 
49
73
  ## Contributing
50
74
 
@@ -18,8 +18,8 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_development_dependency "bundler", "~> 1.3"
22
21
  spec.add_development_dependency "rake"
22
+ spec.add_development_dependency "webmock"
23
23
 
24
24
  spec.add_dependency "oauth2"
25
25
  end
@@ -1 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "access_token_wrapper/base"
4
+ require "access_token_wrapper/from_record"
5
+ require "access_token_wrapper/configuration"
6
+
7
+ module AccessTokenWrapper
8
+ class << self
9
+ def configuration
10
+ @configuration ||= Configuration.new
11
+ end
12
+
13
+ def configure
14
+ yield(configuration)
15
+ end
16
+
17
+ def reset_configuration!
18
+ @configuration = Configuration.new
19
+ end
20
+ end
21
+ end
@@ -1,26 +1,56 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AccessTokenWrapper
2
4
  class Base
3
- NON_ERROR_CODES = [402, 404, 422, 414, 429, 500, 503]
4
5
  EXPIRY_GRACE_SEC = 30
6
+
7
+ def config
8
+ AccessTokenWrapper.configuration
9
+ end
10
+
5
11
  attr_reader :raw_token
6
12
 
13
+ # This is the core functionality
14
+ #
15
+ # @example
16
+ # AccessTokenWrapper::Base.new(token) do |new_token, exception|
17
+ # update_user_from_access_token(new_token)
18
+ # end
19
+ #
20
+ # @param [<OAuth2::AccessToken] raw_token An instance of an OAuth2::AccessToken object
21
+ # @param [&block] callback A callback that gets called when a token is refreshed,
22
+ # the callback is provided `new_token` and optional `exception` parameters
23
+ #
24
+ # @return <AccessTokenWrapper::Base>
25
+ #
26
+ # @api public
7
27
  def initialize(raw_token, &callback)
8
28
  @raw_token = raw_token
9
29
  @callback = callback
10
30
  end
11
31
 
32
+ private
33
+
12
34
  def method_missing(method_name, *args, &block)
13
35
  refresh_token! if token_expiring?
14
36
  @raw_token.send(method_name, *args, &block)
15
37
  rescue OAuth2::Error => exception
16
- if NON_ERROR_CODES.include?(exception.response.status)
17
- raise exception
38
+ if non_refreshable_exception?(exception)
39
+ raise
18
40
  else
19
41
  refresh_token!(exception)
20
42
  @raw_token.send(method_name, *args, &block)
21
43
  end
22
44
  end
23
45
 
46
+ def non_refreshable_exception?(exception)
47
+ if config.skip_statuses.include?(exception.response.status)
48
+ true
49
+ else
50
+ config.skip_refresh_block.call(exception.response)
51
+ end
52
+ end
53
+
24
54
  def refresh_token!(exception = nil)
25
55
  @raw_token = @raw_token.refresh!
26
56
  @callback.call(@raw_token, exception)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AccessTokenWrapper
4
+ # Global configuration object
5
+ #
6
+ # AccessTokenWrapper.configure do |config|
7
+ # config.skip_statuses << 520
8
+ # config.skip_refresh do |response|
9
+ # response.parsed['message'] == 'Duplicate Idempotency Key header detected'
10
+ # end
11
+ # end
12
+ class Configuration
13
+ attr_accessor :skip_statuses, :skip_refresh_block
14
+
15
+ def initialize
16
+ @skip_statuses = [402, 404, 414, 422, 429, 500, 503]
17
+ @skip_refresh_block = ->(_response) { false }
18
+ end
19
+
20
+ def skip_refresh(&block)
21
+ @skip_refresh_block = block
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AccessTokenWrapper
4
+ class FromRecord < Base
5
+ attr_reader :record
6
+
7
+ # This is the core functionality
8
+ #
9
+ # @example
10
+ # AccessTokenWrapper::FromRecord.new(client: client, record: user) do |new_token, exception|
11
+ # update_user_from_access_token(new_token)
12
+ # end
13
+ #
14
+ # @param [<OAuth2::Client>] client An instance of an OAuth2::Client object
15
+ # @param [<Object>] record An AR-like object that responds to `access_token`,
16
+ # `refresh_token`, `expires_at`, `with_lock` and `reload`.
17
+ # @param [&block] callback A callback that gets called when a token is refreshed,
18
+ # the callback is provided `new_token` and optional `exception` parameters
19
+ #
20
+ # @return <AccessTokenWrapper::FromRecord>
21
+ #
22
+ # @api public
23
+ def initialize(client:, record:, &callback)
24
+ @oauth_client = client
25
+ @record = record
26
+ super(build_token, &callback)
27
+ end
28
+
29
+ private
30
+
31
+ # Override the refresh_token! method from the Base class to extend with locking logic
32
+ def refresh_token!(exception = nil)
33
+ @record.with_lock do
34
+ fetch_fresh_record
35
+
36
+ if token_requires_refresh?
37
+ @raw_token = @raw_token.refresh!
38
+ @callback.call(@raw_token, exception)
39
+ end
40
+ end
41
+ end
42
+
43
+ def build_token
44
+ OAuth2::AccessToken.new(@oauth_client, record.access_token, {
45
+ refresh_token: record.refresh_token,
46
+ expires_at: record.expires_at
47
+ })
48
+ end
49
+
50
+ def fetch_fresh_record
51
+ @last_token = @raw_token
52
+ @record.reload
53
+ @raw_token = build_token
54
+ end
55
+
56
+ def token_requires_refresh?
57
+ @last_token.token == @raw_token.token
58
+ end
59
+ end
60
+ end
@@ -1,3 +1,3 @@
1
1
  module AccessTokenWrapper
2
- VERSION = "0.2.1"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -60,7 +60,6 @@ class AccessTokenWrapperTest < Minitest::Test
60
60
  assert token.refreshed
61
61
  end
62
62
 
63
-
64
63
  def test_runs_refresh_block_if_expiring
65
64
  @run = false
66
65
 
@@ -88,4 +87,28 @@ class AccessTokenWrapperTest < Minitest::Test
88
87
  assert !@run
89
88
  assert !token.refreshed
90
89
  end
90
+
91
+ def test_doesnt_run_refresh_block_with_skip_block
92
+ AccessTokenWrapper.configure do |config|
93
+ config.skip_refresh do |response|
94
+ true
95
+ end
96
+ end
97
+
98
+ @run = false
99
+
100
+ token = described_class.new(FakeToken.new) do |new_token, exception|
101
+ @run = true
102
+ end
103
+
104
+ begin
105
+ token.get_and_raise(401)
106
+ rescue OAuth2::Error
107
+ end
108
+
109
+ assert !@run
110
+ assert !token.refreshed
111
+ ensure
112
+ AccessTokenWrapper.reset_configuration!
113
+ end
91
114
  end
@@ -0,0 +1,187 @@
1
+ require 'test_helper'
2
+
3
+ class FromRecordTest < Minitest::Test
4
+ def described_class
5
+ AccessTokenWrapper::FromRecord
6
+ end
7
+
8
+ class FakeRecord
9
+ class << self
10
+ attr_accessor :locked
11
+ end
12
+
13
+ attr_reader :id, :reloaded
14
+ attr_accessor :access_token, :refresh_token, :expires_at
15
+ attr_writer :fresh_token
16
+
17
+ def initialize(access_token: 'ABC', refresh_token: 'DEF', expires_at: Time.now.to_i + 3600)
18
+ update_from_access_token(OpenStruct.new(token: access_token, refresh_token: refresh_token, expires_at: expires_at))
19
+ @id = 42
20
+ @fresh_token = nil
21
+ end
22
+
23
+ def update_from_access_token(token)
24
+ @access_token = token.token
25
+ @refresh_token = token.refresh_token
26
+ @expires_at = token.expires_at
27
+ end
28
+
29
+ def reload
30
+ @reloaded = true
31
+ if @fresh_token
32
+ update_from_access_token(@fresh_token)
33
+ @fresh_token = nil
34
+ end
35
+ end
36
+
37
+ def with_lock
38
+ true while self.class.locked
39
+
40
+ self.class.locked = true
41
+ sleep 0.1
42
+ yield
43
+ self.class.locked = false
44
+ end
45
+ end
46
+
47
+ class ExpiringFakeRecord < FakeRecord
48
+ def reload
49
+ @access_token = access_token + "*"
50
+ @refresh_token = refresh_token + "*"
51
+ @expires_at = Time.now.to_i + 3600
52
+ super
53
+ end
54
+ end
55
+
56
+ def client
57
+ @client ||= OAuth2::Client.new('AAA', 'BBB', site: 'http://localhost:3000')
58
+ end
59
+
60
+ def setup
61
+ stub_request(:get, "http://localhost:3000/200").to_return(status: 200, body: "")
62
+ stub_request(:get, "http://localhost:3000/401").to_return({ status: 401, body: "" }, { status: 200, body: "" })
63
+ stub_request(:get, "http://localhost:3000/429").to_return(status: 429, body: "")
64
+ end
65
+
66
+ def stub_token_refresh
67
+ stub_request(:post, "http://localhost:3000/oauth/token").with(body: {"client_id"=>"AAA", "client_secret"=>"BBB", "grant_type"=>"refresh_token", "refresh_token"=>"DEF"}).to_return(status: 200, headers: { 'Content-Type' => 'application/json' }, body: token_response.to_json).times(1)
68
+ end
69
+
70
+ def stub_token_refresh_with_wait
71
+ stub_request(:post, "http://localhost:3000/oauth/token").to_return(status: 200, headers: { 'Content-Type' => 'application/json' }, body: lambda { |request| sleep 0.1; token_response.to_json }).times(1)
72
+ end
73
+
74
+ def token_response
75
+ {
76
+ "access_token": "57ed301af04bf35b40f255feb5ef469ab2f046aff14",
77
+ "expires_in": 7200,
78
+ "refresh_token": "026b343de07818b3ffebfb3001eff9a00aea43da0 ",
79
+ "scope": "public",
80
+ "token_type": "bearer"
81
+ }
82
+ end
83
+
84
+ def test_doesnt_run_block_if_no_exception
85
+ @run = false
86
+
87
+ token = described_class.new(client: client, record: FakeRecord.new) do |new_token, exception|
88
+ @run = true
89
+ end
90
+
91
+ token.get('/200')
92
+ assert !@run
93
+ assert !token.record.reloaded
94
+ end
95
+
96
+ def test_runs_refresh_block_if_exception
97
+ @run = false
98
+ stub_refresh = stub_token_refresh
99
+
100
+ token = described_class.new(client: client, record: FakeRecord.new) do |new_token, exception|
101
+ @run = true
102
+ end
103
+
104
+ token.get('/401')
105
+ assert @run
106
+ assert token.record.reloaded
107
+ assert_requested(stub_refresh)
108
+ end
109
+
110
+ def test_runs_refresh_block_if_expiring
111
+ @run = false
112
+ stub_refresh = stub_token_refresh
113
+ token = described_class.new(client: client, record: FakeRecord.new(expires_at: Time.now.to_i - 1)) do |new_token, exception|
114
+ @run = true
115
+ end
116
+
117
+ token.get('/200')
118
+ assert @run
119
+ assert token.record.reloaded
120
+ assert_requested(stub_refresh)
121
+ end
122
+
123
+ def test_doesnt_run_block_if_non_auth_exception
124
+ @run = false
125
+
126
+ token = described_class.new(client: client, record: FakeRecord.new) do |new_token, exception|
127
+ @run = true
128
+ end
129
+
130
+ begin
131
+ token.get('/429')
132
+ rescue OAuth2::Error
133
+ end
134
+
135
+ assert !@run
136
+ assert !token.record.reloaded
137
+ end
138
+
139
+ def test_refreshes_record_and_halts_api_request_if_not_needed
140
+ @run = false
141
+ stub_refresh = stub_token_refresh
142
+
143
+ token = described_class.new(client: client, record: ExpiringFakeRecord.new) do |new_token, exception|
144
+ @run = true
145
+ end
146
+
147
+ token.get('/401')
148
+
149
+ assert !@run
150
+ assert_not_requested(stub_refresh)
151
+ assert token.record.reloaded
152
+ end
153
+
154
+ def test_that_the_lock_locks_multiple_requests
155
+ @results = []
156
+ record = FakeRecord.new(expires_at: Time.now.to_i-1)
157
+ record_copy = FakeRecord.new(expires_at: Time.now.to_i-1)
158
+
159
+ token = described_class.new(client: client, record: record) do |new_token, exception|
160
+ record.update_from_access_token(new_token)
161
+ record_copy.fresh_token = new_token
162
+ @run = true
163
+ end
164
+
165
+ token2 = described_class.new(client: client, record: record_copy) do |new_token, exception|
166
+ @run = true
167
+ end
168
+
169
+ stub_refresh = stub_token_refresh_with_wait
170
+
171
+ new_thread = Thread.new do
172
+ @results << :before_primary
173
+ token.get('/200')
174
+ @results << :after_primary
175
+ end
176
+
177
+ sleep 0.01
178
+
179
+ @results << :before_secondary
180
+ token2.get('/200')
181
+ @results << :after_secondary
182
+
183
+ new_thread.join
184
+ assert_equal [:before_primary, :before_secondary, :after_primary, :after_secondary], @results
185
+ assert_requested(stub_refresh)
186
+ end
187
+ end
@@ -1,3 +1,4 @@
1
1
  require 'minitest/autorun'
2
2
  require 'access_token_wrapper'
3
3
  require 'oauth2'
4
+ require 'webmock/minitest'
metadata CHANGED
@@ -1,31 +1,31 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: access_token_wrapper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bradley Priest
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-01-23 00:00:00.000000000 Z
11
+ date: 2020-06-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
14
+ name: rake
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.3'
19
+ version: '0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.3'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: rake
28
+ name: webmock
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -70,8 +70,11 @@ files:
70
70
  - access_token_wrapper.gemspec
71
71
  - lib/access_token_wrapper.rb
72
72
  - lib/access_token_wrapper/base.rb
73
+ - lib/access_token_wrapper/configuration.rb
74
+ - lib/access_token_wrapper/from_record.rb
73
75
  - lib/access_token_wrapper/version.rb
74
76
  - test/base_test.rb
77
+ - test/from_record_test.rb
75
78
  - test/test_helper.rb
76
79
  homepage: https://github.com/tradegecko/access_token_wrapper
77
80
  licenses:
@@ -92,12 +95,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
92
95
  - !ruby/object:Gem::Version
93
96
  version: '0'
94
97
  requirements: []
95
- rubyforge_project:
96
- rubygems_version: 2.6.13
98
+ rubygems_version: 3.1.2
97
99
  signing_key:
98
100
  specification_version: 4
99
101
  summary: Wrapper for OAuth2::Token to automatically refresh the expiry token when
100
102
  expired.
101
103
  test_files:
102
104
  - test/base_test.rb
105
+ - test/from_record_test.rb
103
106
  - test/test_helper.rb