access_token_wrapper 0.2.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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