oauth2_api_client 1.0.0 → 2.0.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
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