oauth2_api_client 1.0.0 → 2.0.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
2
  SHA256:
3
- metadata.gz: 19c17dc8d34436a95460c68a49a070600cc70cccb68336bb58c47ea10ec42efb
4
- data.tar.gz: bfe18b660c1f56ed84ef4d26a9b277acb1bd3a50b135196b89988a4a676a0ff7
3
+ metadata.gz: cb758d909c7cbeae49beef096ef7b8b0eedb5e038c8a403fac15388d6fc71884
4
+ data.tar.gz: 86186e7192b22b2fddb16e6bd03c005efbe58d3f159585ef8ad2ec1cd9a79d4b
5
5
  SHA512:
6
- metadata.gz: 77f3e27e9d04b09ced28691dc76a48d3f00a47c76d8a6fbdd847bbb45097cc562dff75da161b25f4a2f39e84578aa361c41e24cb6ecb19a6a329b13f9685c899
7
- data.tar.gz: 5917eb46b4d65536d2246597774734d8c6b815245c9babfa8818f0b76f289c5d415ddb60066e9af4cbf96821d7d16c821c125936c2476daf1399d763cfe31175
6
+ metadata.gz: 5cbb79317198bd5c8bf2e3139ee5646be1d035ef7a95950ed05e0b35aad4b8bde1556b80294e73ee2c3823e6ce863fbc09ce26989fb6677b81fed0157c04aae6
7
+ data.tar.gz: ce5ef6661c8226f38770f2abc8cae7f33bc6b5efb6eaac11819fd8848ab084cc236374797d92c5d505a4d2142a6e677b0d17f1e2c0d0d910fb44b201bdf0537c
@@ -1,3 +1,8 @@
1
1
 
2
2
  # CHANGELOG
3
3
 
4
+ # v2.0.0
5
+
6
+ * `TokenProvider` added
7
+ * Added option to pass the pre-generated oauth token
8
+ * Simple concatenation of base url and path
data/README.md CHANGED
@@ -6,23 +6,33 @@
6
6
  Oauth2ApiClient is small, but powerful client around
7
7
  [oauth2](https://github.com/oauth-xx/oauth2) and
8
8
  [http-rb](https://github.com/httprb/http) to interact with APIs which use
9
- oauth2 for authentication with automatic token caching and renewal.
9
+ oauth2 for authentication.
10
10
 
11
11
  ```ruby
12
- client = Oauth2ApiClient.new(
13
- base_url: "https://api.example.com/",
14
- client_id: "the client id",
15
- client_secret: "the client secret",
16
- oauth_token_url: "https://auth.example.com/oauth2/token",
17
- max_token_ttl: 3600, # optional
18
- cache: Rails.cache # optional
19
- )
12
+ client = Oauth2ApiClient.new(base_url: "https://api.example.com", token "oauth2 token")
20
13
 
21
14
  client.post("/orders", json: { address: "..." }).status.success?
22
15
  client.headers("User-Agent" => "API Client").timeout(read: 5, write: 5).get("/orders").parse
23
16
  # ...
24
17
  ```
25
18
 
19
+ Oauth2ApiClient is capable of generating oauth2 tokens, when a client id,
20
+ client secret and oauth token url is given with automatic token caching and
21
+ renewal on expiry, including retry of the current request.
22
+
23
+ ```ruby
24
+ client = Oauth2ApiClient.new(
25
+ base_url: "https://api.example.com",
26
+ token: Oauth2ApiClient::TokenProvider.new(
27
+ client_id: "client id",
28
+ client_secret: "client secret",
29
+ token_url: "https.//auth.example.com/oauth2/token",
30
+ cache: Rails.cache, # optional,
31
+ max_token_ttl: 1800 # optional
32
+ )
33
+ )
34
+ ```
35
+
26
36
  ## Install
27
37
 
28
38
  Add this line to your application's Gemfile:
@@ -6,6 +6,7 @@ require "active_support"
6
6
 
7
7
  require "oauth2_api_client/version"
8
8
  require "oauth2_api_client/http_error"
9
+ require "oauth2_api_client/token_provider"
9
10
 
10
11
  # The Oauth2ApiClient class is a client wrapped around the oauth2 and http-rb
11
12
  # gem to interact with APIs using oauth2 for authentication with automatic
@@ -15,46 +16,35 @@ class Oauth2ApiClient
15
16
  # Creates a new Oauth2ApiClient
16
17
  #
17
18
  # @param base_url [String] The base url of the API to interact with
18
- # @param client_id [String] The client id to use for oauth2 authentication
19
- # @param client_secret [String] The client secret to use for oauth2 authentication
20
- # @param oauth_token_url [String] The url to obtain tokens from
21
- # @param cache The cache instance to cache the tokens, e.g. `Rails.cache`.
22
- # Defaults to `ActiveSupport::Cache::MemoryStore.new`
23
- # @param max_token_ttl [#to_i] The max lifetime of the token in the cache
24
- # @param base_request You can pass some http-rb rqeuest as the base. Useful,
25
- # if some information needs to be passed with every request. Defaults to
26
- # `HTTP`
19
+ # @param token [String, Oauth2ApiClient::TokenProvider] Allows to pass an
20
+ # existing token received via external sources or an instance of
21
+ # `Oauth2ApiClient::TokenProvider` which is capable of generating
22
+ # tokens when client id, client secret, etc. is given
27
23
  #
28
24
  # @example
29
25
  # client = Oauth2ApiClient.new(
30
- # base_url: "https://api.example.com/",
31
- # client_id: "the client id",
32
- # client_secret: "the client secret",
33
- # oauth_token_url: "https://auth.example.com/oauth2/token",
34
- # cache: Rails.cache,
35
- # base_request: HTTP.headers("User-Agent" => "API client")
26
+ # base_url: "https://api.example.com",
27
+ # token: "the api token"
36
28
  # )
37
29
  #
38
30
  # client.post("/orders", json: { address: "..." }).status.success?
39
31
  # client.headers("User-Agent" => "API Client").timeout(read: 5, write: 5).get("/orders").parse
32
+ #
33
+ # @example
34
+ # client = Oauth2ApiClient.new(
35
+ # base_url: "https://api.example.com",
36
+ # token: Oauth2ApiClient::TokenProvider.new(
37
+ # client_id: "the client id",
38
+ # client_secret: "the client secret",
39
+ # oauth_token_url: "https://auth.example.com/oauth2/token",
40
+ # cache: Rails.cache
41
+ # )
42
+ # )
40
43
 
41
- def initialize(base_url:, client_id:, client_secret:, oauth_token_url:, cache: ActiveSupport::Cache::MemoryStore.new, max_token_ttl: 3600, base_request: HTTP)
44
+ def initialize(base_url:, base_request: HTTP, token:)
42
45
  @base_url = base_url
43
- @client_id = client_id
44
- @client_secret = client_secret
45
- @oauth_token_url = oauth_token_url
46
- @max_token_ttl = max_token_ttl
47
- @cache = cache
46
+ @token = token
48
47
  @request = base_request
49
-
50
- oauth_uri = URI.parse(oauth_token_url)
51
-
52
- @oauth_client = OAuth2::Client.new(
53
- @client_id,
54
- @client_secret,
55
- site: URI.parse("#{oauth_uri.scheme}://#{oauth_uri.host}:#{oauth_uri.port}/").to_s,
56
- token_url: oauth_uri.path
57
- )
58
48
  end
59
49
 
60
50
  # Returns a oauth2 token to use for authentication
@@ -62,9 +52,7 @@ class Oauth2ApiClient
62
52
  # @return [String] The token
63
53
 
64
54
  def token
65
- @cache.fetch(cache_key, expires_in: @max_token_ttl.to_i) do
66
- @oauth_client.client_credentials.get_token.token
67
- end
55
+ @token.respond_to?(:to_str) ? @token.to_str : @token.token
68
56
  end
69
57
 
70
58
  [:timeout, :headers, :cookies, :via, :encoding, :accept, :auth, :basic_auth].each do |method|
@@ -85,13 +73,9 @@ class Oauth2ApiClient
85
73
 
86
74
  private
87
75
 
88
- def cache_key
89
- @cache_key ||= ["oauth_api_client", @base_url, @oauth_token_url, @client_id].join("|")
90
- end
91
-
92
76
  def execute(verb, path, options = {})
93
77
  with_retry do
94
- response = @request.auth("Bearer #{token}").send(verb, URI.join(@base_url, path), options)
78
+ response = @request.auth("Bearer #{token}").send(verb, "#{@base_url}#{path}", options)
95
79
 
96
80
  return response if response.status.success?
97
81
 
@@ -105,13 +89,15 @@ class Oauth2ApiClient
105
89
  begin
106
90
  yield
107
91
  rescue HttpError => e
108
- @cache.delete(cache_key) if e.response.status.unauthorized?
92
+ if !retried && e.response.status.unauthorized? && @token.respond_to?(:invalidate_token)
93
+ @token.invalidate_token
109
94
 
110
- raise(e) if retried || !e.response.status.unauthorized?
95
+ retried = true
111
96
 
112
- retried = true
97
+ retry
98
+ end
113
99
 
114
- retry
100
+ raise
115
101
  end
116
102
  end
117
103
  end
@@ -0,0 +1,74 @@
1
+ require "uri"
2
+
3
+ class Oauth2ApiClient
4
+ # The TokenProvider class is responsible for obtaining and caching an oauth2
5
+ # token, when client id, client secret and token url is given.
6
+ #
7
+ # @example
8
+ # Oauth2ApiClient::TokenProvider.new(
9
+ # client_id: "client id",
10
+ # client_secret: "client secret",
11
+ # token_url: "https://auth.example.com/oauth2/token",
12
+ # cache: Rails.cache, # optional
13
+ # max_token_ttl: 1800 # optional
14
+ # )
15
+
16
+ class TokenProvider
17
+ # Creates a new TokenProvider instance.
18
+ #
19
+ # @param client_id [String] The client id
20
+ # @param client_secret [String] The client secret
21
+ # @param token_url [String] The oauth2 endpoint for generating tokens
22
+ # @param cache An ActiveSupport compatible cache implementation. Defaults
23
+ # to `ActiveSupport::Cache::MemoryStore.new`
24
+ # @param max_token_ttl [#to_i] A maximum token lifetime. Defaults to 3600
25
+ #
26
+ # @example
27
+ # Oauth2ApiClient::TokenProvider.new(
28
+ # client_id: "client id",
29
+ # client_secret: "client secret",
30
+ # token_url: "https://auth.example.com/oauth2/token",
31
+ # )
32
+
33
+ def initialize(client_id:, client_secret:, token_url:, cache: ActiveSupport::Cache::MemoryStore.new, max_token_ttl: 3600)
34
+ @client_id = client_id
35
+ @client_secret = client_secret
36
+ @token_url = token_url
37
+ @max_token_ttl = max_token_ttl
38
+ @cache = cache
39
+
40
+ oauth_uri = URI.parse(token_url)
41
+
42
+ @oauth_client = OAuth2::Client.new(
43
+ @client_id,
44
+ @client_secret,
45
+ site: URI.parse("#{oauth_uri.scheme}://#{oauth_uri.host}:#{oauth_uri.port}/").to_s,
46
+ token_url: oauth_uri.path
47
+ )
48
+ end
49
+
50
+ # Returns the oauth2 token, either from the cache, or newly generated
51
+ #
52
+ # @return [String] the token
53
+
54
+ def token
55
+ @cache.fetch(cache_key, expires_in: @max_token_ttl.to_i) do
56
+ @oauth_client.client_credentials.get_token.token
57
+ end
58
+ end
59
+
60
+ # Invalidates the cached token, i.e. removes it from the cache
61
+ #
62
+ # @return [String] the token
63
+
64
+ def invalidate_token
65
+ @cache.delete(cache_key)
66
+ end
67
+
68
+ private
69
+
70
+ def cache_key
71
+ @cache_key ||= ["oauth_api_client", @token_url, @client_id].join("|")
72
+ end
73
+ end
74
+ end
@@ -1,3 +1,3 @@
1
1
  class Oauth2ApiClient
2
- VERSION = "1.0.0"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -0,0 +1,82 @@
1
+ require File.expand_path("../spec_helper", __dir__)
2
+
3
+ RSpec.describe Oauth2ApiClient::TokenProvider do
4
+ before do
5
+ token_response = {
6
+ access_token: "access_token",
7
+ token_type: "bearer",
8
+ expires_in: 3600,
9
+ refresh_token: "refresh_token",
10
+ scope: "create"
11
+ }
12
+
13
+ stub_request(:post, "http://localhost/oauth2/token")
14
+ .to_return(status: 200, body: JSON.generate(token_response), headers: { "Content-Type" => "application/json" })
15
+ end
16
+
17
+ describe "#token" do
18
+ it "returns a oauth2 token" do
19
+ token_provider = described_class.new(
20
+ client_id: "client_id",
21
+ client_secret: "client_secret",
22
+ token_url: "http://localhost/oauth2/token"
23
+ )
24
+
25
+ expect(token_provider.token).to eq("access_token")
26
+ end
27
+
28
+ it "returns the cached token if existing" do
29
+ cache = ActiveSupport::Cache::MemoryStore.new
30
+
31
+ allow(cache).to receive(:fetch).and_return("cached_token")
32
+
33
+ token_provider = described_class.new(
34
+ client_id: "client_id",
35
+ client_secret: "client_secret",
36
+ token_url: "http://localhost/oauth2/token",
37
+ cache: cache
38
+ )
39
+
40
+ expect(token_provider.token).to eq("cached_token")
41
+ end
42
+
43
+ it "caches the token" do
44
+ cache = ActiveSupport::Cache::MemoryStore.new
45
+
46
+ allow(cache).to receive(:fetch).and_yield
47
+
48
+ token_provider = described_class.new(
49
+ client_id: "client_id",
50
+ client_secret: "client_secret",
51
+ token_url: "http://localhost/oauth2/token",
52
+ max_token_ttl: 60,
53
+ cache: cache
54
+ )
55
+
56
+ token_provider.token
57
+
58
+ expect(cache).to have_received(:fetch).with("oauth_api_client|http://localhost/oauth2/token|client_id", expires_in: 60)
59
+ end
60
+ end
61
+
62
+ describe "#invalidate_token" do
63
+ it "deletes the token from the cache" do
64
+ cache = ActiveSupport::Cache::MemoryStore.new
65
+
66
+ token_provider = described_class.new(
67
+ client_id: "client_id",
68
+ client_secret: "client_secret",
69
+ token_url: "http://localhost/oauth2/token",
70
+ cache: cache
71
+ )
72
+
73
+ token_provider.token
74
+
75
+ expect(cache.read("oauth_api_client|http://localhost/oauth2/token|client_id")).not_to be_nil
76
+
77
+ token_provider.invalidate_token
78
+
79
+ expect(cache.read("oauth_api_client|http://localhost/oauth2/token|client_id")).to be_nil
80
+ end
81
+ end
82
+ end
@@ -31,63 +31,30 @@ RSpec.describe Oauth2ApiClient do
31
31
  end
32
32
 
33
33
  describe "#token" do
34
- it "returns a oauth2 token" do
35
- client = described_class.new(
36
- base_url: "http://localhost/",
37
- client_id: "client_id",
38
- client_secret: "client_secret",
39
- oauth_token_url: "http://localhost/oauth2/token"
40
- )
34
+ it "returns the supplier token" do
35
+ client = described_class.new(base_url: "http://localhost/", token: "access_token")
41
36
 
42
37
  expect(client.token).to eq("access_token")
43
38
  end
44
39
 
45
- it "returns the cached token if existing" do
46
- cache = ActiveSupport::Cache::MemoryStore.new
47
-
48
- allow(cache).to receive(:fetch).and_return("cached_token")
49
-
50
- client = described_class.new(
51
- base_url: "http://localhost/",
52
- client_id: "client_id",
53
- client_secret: "client_secret",
54
- oauth_token_url: "http://localhost/oauth2/token",
55
- cache: cache
56
- )
57
-
58
- expect(client.token).to eq("cached_token")
59
- end
60
-
61
- it "caches the token" do
62
- cache = ActiveSupport::Cache::MemoryStore.new
63
-
64
- allow(cache).to receive(:fetch).and_yield
65
-
40
+ it "returns a oauth2 token" do
66
41
  client = described_class.new(
67
42
  base_url: "http://localhost/",
68
- client_id: "client_id",
69
- client_secret: "client_secret",
70
- oauth_token_url: "http://localhost/oauth2/token",
71
- max_token_ttl: 60,
72
- cache: cache
43
+ token: described_class::TokenProvider.new(
44
+ client_id: "client_id",
45
+ client_secret: "client_secret",
46
+ token_url: "http://localhost/oauth2/token"
47
+ )
73
48
  )
74
49
 
75
- client.token
76
-
77
- expect(cache).to have_received(:fetch).with("oauth_api_client|http://localhost/|http://localhost/oauth2/token|client_id", expires_in: 60)
50
+ expect(client.token).to eq("access_token")
78
51
  end
79
52
  end
80
53
 
81
54
  [:timeout, :headers, :cookies, :via, :encoding, :accept, :auth, :basic_auth].each do |method|
82
55
  describe "##{method}" do
83
56
  it "creates a dupped instance" do
84
- client = described_class.new(
85
- base_url: "http://localhost/",
86
- client_id: "client_id",
87
- client_secret: "client_secret",
88
- oauth_token_url: "http://localhost/oauth2/token",
89
- base_request: HttpTestRequest.new
90
- )
57
+ client = described_class.new(base_url: "http://localhost/", token: "token", base_request: HttpTestRequest.new)
91
58
 
92
59
  client1 = client.send(method, "key1")
93
60
  client2 = client1.send(method, "key1")
@@ -96,13 +63,7 @@ RSpec.describe Oauth2ApiClient do
96
63
  end
97
64
 
98
65
  it "extends the request" do
99
- client = described_class.new(
100
- base_url: "http://localhost/",
101
- client_id: "client_id",
102
- client_secret: "client_secret",
103
- oauth_token_url: "http://localhost/oauth2/token",
104
- base_request: HttpTestRequest.new
105
- )
66
+ client = described_class.new(base_url: "http://localhost/", token: "token", base_request: HttpTestRequest.new)
106
67
 
107
68
  client1 = client.send(method, "key1")
108
69
  client2 = client1.send(method, "key2")
@@ -115,76 +76,51 @@ RSpec.describe Oauth2ApiClient do
115
76
 
116
77
  describe "request" do
117
78
  it "prepends the base url" do
118
- stub_request(:get, "http://localhost/path?key=value")
79
+ stub_request(:get, "http://localhost/api/path?key=value")
119
80
  .to_return(status: 200, body: "ok")
120
81
 
121
- client = described_class.new(
122
- base_url: "http://localhost/",
123
- client_id: "client_id",
124
- client_secret: "client_secret",
125
- oauth_token_url: "http://localhost/oauth2/token"
126
- )
82
+ client = described_class.new(base_url: "http://localhost/api", token: "token")
127
83
 
128
84
  expect(client.get("/path", params: { key: "value" }).to_s).to eq("ok")
129
85
  end
130
86
 
131
87
  it "passes the token in the authentication header" do
132
- stub_request(:get, "http://localhost/path")
88
+ stub_request(:get, "http://localhost/api/path")
133
89
  .with(headers: { "Authorization" => "Bearer access_token" })
134
90
  .to_return(status: 200, body: "ok", headers: {})
135
91
 
136
- client = described_class.new(
137
- base_url: "http://localhost/",
138
- client_id: "client_id",
139
- client_secret: "client_secret",
140
- oauth_token_url: "http://localhost/oauth2/token"
141
- )
92
+ client = described_class.new(base_url: "http://localhost/api", token: "access_token")
142
93
 
143
94
  expect(client.get("/path").to_s).to eq("ok")
144
95
  end
145
96
 
146
- it "invalidates the cached token when an http unauthorized status is returned" do
147
- stub_request(:get, "http://localhost/path")
148
- .to_return(status: 401, body: "unauthorized")
149
-
150
- cache = ActiveSupport::Cache::MemoryStore.new
151
-
152
- client = described_class.new(
153
- base_url: "http://localhost/",
154
- client_id: "client_id",
155
- client_secret: "client_secret",
156
- oauth_token_url: "http://localhost/oauth2/token",
157
- cache: cache
158
- )
159
-
160
- expect { client.get("/path") }.to raise_error(described_class::HttpError)
161
-
162
- expect(cache.read("oauth_api_client|http://localhost/|http://localhost/oauth2/token|client_id")).to be_nil
163
- end
164
-
165
97
  it "retries the request when an http unauthorized status is returned" do
166
- stub_request(:get, "http://localhost/path")
98
+ stub_request(:get, "http://localhost/api/path")
167
99
  .to_return({ status: 401, body: "unauthorized" }, { status: 200, body: "ok" })
168
100
 
169
101
  client = described_class.new(
170
- base_url: "http://localhost/",
171
- client_id: "client_id",
172
- client_secret: "client_secret",
173
- oauth_token_url: "http://localhost/oauth2/token"
102
+ base_url: "http://localhost/api",
103
+ token: described_class::TokenProvider.new(
104
+ client_id: "client_id",
105
+ client_secret: "client_secret",
106
+ token_url: "http://localhost/oauth2/token"
107
+ )
174
108
  )
175
109
 
176
110
  expect(client.get("/path").to_s).to eq("ok")
177
111
  end
178
112
 
179
113
  it "raises if the retried request also fails" do
180
- stub_request(:get, "http://localhost/path")
114
+ stub_request(:get, "http://localhost/api/path")
181
115
  .to_return(status: 401, body: "unauthorized")
182
116
 
183
117
  client = described_class.new(
184
- base_url: "http://localhost/",
185
- client_id: "client_id",
186
- client_secret: "client_secret",
187
- oauth_token_url: "http://localhost/oauth2/token"
118
+ base_url: "http://localhost/api",
119
+ token: described_class::TokenProvider.new(
120
+ client_id: "client_id",
121
+ client_secret: "client_secret",
122
+ token_url: "http://localhost/oauth2/token"
123
+ )
188
124
  )
189
125
 
190
126
  expect { client.get("/path") }.to raise_error(described_class::HttpError)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oauth2_api_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Vetter
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-19 00:00:00.000000000 Z
11
+ date: 2020-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -154,8 +154,10 @@ files:
154
154
  - Rakefile
155
155
  - lib/oauth2_api_client.rb
156
156
  - lib/oauth2_api_client/http_error.rb
157
+ - lib/oauth2_api_client/token_provider.rb
157
158
  - lib/oauth2_api_client/version.rb
158
159
  - oauth2_api_client.gemspec
160
+ - spec/oauth2_api_client/token_provider_spec.rb
159
161
  - spec/oauth2_api_client_spec.rb
160
162
  - spec/spec_helper.rb
161
163
  homepage: https://github.com/mrkamel/oauth2_api_client
@@ -182,5 +184,6 @@ signing_key:
182
184
  specification_version: 4
183
185
  summary: Small but powerful client around oauth2 and http-rb to interact with APIs
184
186
  test_files:
187
+ - spec/oauth2_api_client/token_provider_spec.rb
185
188
  - spec/oauth2_api_client_spec.rb
186
189
  - spec/spec_helper.rb