keycloak-api-rails 1.1.1 → 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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +51 -21
  5. data/keycloak-api-rails.gemspec +16 -6
  6. data/lib/keycloak-api-rails/authentication.rb +3 -11
  7. data/lib/keycloak-api-rails/configuration.rb +84 -0
  8. data/lib/keycloak-api-rails/helper.rb +31 -4
  9. data/lib/keycloak-api-rails/http_client.rb +54 -19
  10. data/lib/keycloak-api-rails/middleware.rb +17 -20
  11. data/lib/keycloak-api-rails/public_key_cached_resolver.rb +26 -7
  12. data/lib/keycloak-api-rails/railtie.rb +16 -1
  13. data/lib/keycloak-api-rails/service.rb +77 -28
  14. data/lib/keycloak-api-rails/testing.rb +5 -3
  15. data/lib/keycloak-api-rails/token_error.rb +41 -23
  16. data/lib/keycloak-api-rails/version.rb +1 -1
  17. data/lib/keycloak-api-rails.rb +21 -6
  18. metadata +39 -23
  19. data/.github/workflows/ci.yml +0 -35
  20. data/.github/workflows/release.yml +0 -52
  21. data/.gitignore +0 -9
  22. data/.rspec +0 -2
  23. data/Dockerfile +0 -13
  24. data/Gemfile +0 -3
  25. data/Rakefile +0 -6
  26. data/spec/keycloak-api-rails/activesupport_free_spec.rb +0 -18
  27. data/spec/keycloak-api-rails/authentication_spec.rb +0 -70
  28. data/spec/keycloak-api-rails/configuration_spec.rb +0 -101
  29. data/spec/keycloak-api-rails/helper_spec.rb +0 -94
  30. data/spec/keycloak-api-rails/public_key_cached_resolver_spec.rb +0 -80
  31. data/spec/keycloak-api-rails/service_spec.rb +0 -281
  32. data/spec/keycloak-api-rails/testing_spec.rb +0 -102
  33. data/spec/spec_helper.rb +0 -14
  34. data/spec/support/activesupport_free_probe.rb +0 -131
  35. data/spec/support/public_key_cached_resolver_stub.rb +0 -11
  36. data/spec/support/public_key_resolver_stub.rb +0 -7
  37. data/spec/support/rails_helper.rb +0 -4
@@ -1,70 +0,0 @@
1
- require "spec_helper"
2
-
3
- describe KeycloakApiRails::Authentication do
4
- class ExampleController < ActionController::Base
5
- include KeycloakApiRails::Authentication
6
- # Mark protected methods public so they may be called in tests
7
- public(*KeycloakApiRails::Authentication.protected_instance_methods)
8
- end
9
-
10
- let(:controller) { ExampleController.new }
11
- let(:token) { 'keycloak_valid_token'}
12
- let(:headers) do
13
- {
14
- 'REQUEST_METHOD' => :get,
15
- 'REQUEST_URI' => 'http://www.an-url.io',
16
- 'HTTP_AUTHORIZATION' => "Bearer #{token}"
17
- }
18
- end
19
-
20
- describe "when included" do
21
- it "declares keycloak_authenticate as a helper method when the base class supports helpers" do
22
- declared_helper_methods = []
23
- base = Class.new do
24
- define_singleton_method(:helper_method) { |*names| declared_helper_methods.concat(names) }
25
- end
26
-
27
- base.include(KeycloakApiRails::Authentication)
28
-
29
- expect(declared_helper_methods).to eq [:keycloak_authenticate]
30
- end
31
-
32
- it "declares the helper method on a real controller" do
33
- expect(ExampleController._helper_methods).to include :keycloak_authenticate
34
- end
35
-
36
- it "does not fail when the base class does not support helpers" do
37
- expect {
38
- Class.new.include(KeycloakApiRails::Authentication)
39
- }.to_not raise_error
40
- end
41
-
42
- it "exposes its methods as protected instance methods" do
43
- expect(KeycloakApiRails::Authentication.protected_instance_methods).to match_array %i[
44
- keycloak_authenticate
45
- authentication_failed
46
- authentication_succeeded
47
- ]
48
- end
49
-
50
- it "does not add any public method to the including class" do
51
- base = Class.new.include(KeycloakApiRails::Authentication)
52
-
53
- expect(base.public_instance_methods).to_not include :keycloak_authenticate
54
- expect(base.protected_instance_methods).to include :keycloak_authenticate
55
- end
56
- end
57
-
58
- describe "#keycloak_authenticate" do
59
- before do
60
- # Mock request object because we aren't using real request spec
61
- allow(controller).to receive(:request).and_return(double("request", env: headers ))
62
- end
63
-
64
- it "it authenticates with request header" do
65
- expect_any_instance_of(KeycloakApiRails::Service).to receive(:decode_and_verify).with(token).and_return("A User")
66
- expect(controller).to receive(:authentication_succeeded)
67
- controller.keycloak_authenticate
68
- end
69
- end
70
- end
@@ -1,101 +0,0 @@
1
- RSpec.describe KeycloakApiRails::Configuration do
2
-
3
- # Every attribute that used to be declared through ActiveSupport's `config_accessor`.
4
- let(:attribute_names) do
5
- %i[
6
- server_url
7
- realm_id
8
- skip_paths
9
- opt_in
10
- token_expiration_tolerance_in_seconds
11
- public_key_cache_ttl
12
- custom_attributes
13
- logger
14
- ca_certificate_file
15
- ]
16
- end
17
-
18
- describe "attributes" do
19
- let(:configuration) { KeycloakApiRails::Configuration.new }
20
-
21
- it "exposes a reader and a writer for each attribute" do
22
- attribute_names.each do |name|
23
- expect(configuration).to respond_to name
24
- expect(configuration).to respond_to "#{name}="
25
- end
26
- end
27
-
28
- it "returns nil for each attribute that has not been assigned" do
29
- attribute_names.each do |name|
30
- expect(configuration.public_send(name)).to be_nil
31
- end
32
- end
33
-
34
- it "returns the value that has been assigned" do
35
- attribute_names.each do |name|
36
- configuration.public_send("#{name}=", "a value for #{name}")
37
- expect(configuration.public_send(name)).to eq "a value for #{name}"
38
- end
39
- end
40
-
41
- it "does not respond to an unknown attribute" do
42
- expect(configuration).to_not respond_to :not_a_configuration_attribute
43
- expect {
44
- configuration.not_a_configuration_attribute
45
- }.to raise_error NoMethodError
46
- end
47
- end
48
-
49
- describe "#configure" do
50
- after(:each) do
51
- KeycloakApiRails.load_configuration
52
- end
53
-
54
- it "yields the configuration" do
55
- expect { |block| KeycloakApiRails.configure(&block) }.to yield_with_args KeycloakApiRails::Configuration
56
- end
57
-
58
- it "keeps the values assigned within the block" do
59
- KeycloakApiRails.configure do |config|
60
- config.server_url = "http://keycloak:8080"
61
- config.realm_id = "a-realm"
62
- config.ca_certificate_file = "/etc/ssl/a-certificate.pem"
63
- end
64
-
65
- expect(KeycloakApiRails.config.server_url).to eq "http://keycloak:8080"
66
- expect(KeycloakApiRails.config.realm_id).to eq "a-realm"
67
- expect(KeycloakApiRails.config.ca_certificate_file).to eq "/etc/ssl/a-certificate.pem"
68
- end
69
-
70
- it "always returns the same configuration instance" do
71
- expect(KeycloakApiRails.config).to be KeycloakApiRails.config
72
- end
73
- end
74
-
75
- describe "#load_configuration" do
76
- before(:each) do
77
- KeycloakApiRails.load_configuration
78
- end
79
-
80
- after(:each) do
81
- KeycloakApiRails.load_configuration
82
- end
83
-
84
- it "applies the documented default values" do
85
- config = KeycloakApiRails.config
86
-
87
- expect(config.server_url).to be_nil
88
- expect(config.realm_id).to be_nil
89
- expect(config.logger).to be_a ::Logger
90
- expect(config.skip_paths).to eq({})
91
- expect(config.opt_in).to be false
92
- expect(config.token_expiration_tolerance_in_seconds).to eq 10
93
- expect(config.public_key_cache_ttl).to eq 86400
94
- expect(config.custom_attributes).to eq []
95
- end
96
-
97
- it "exposes the logger through KeycloakApiRails.logger" do
98
- expect(KeycloakApiRails.logger).to be KeycloakApiRails.config.logger
99
- end
100
- end
101
- end
@@ -1,94 +0,0 @@
1
- RSpec.describe KeycloakApiRails::Helper do
2
- describe "#read_token_from_query_string" do
3
-
4
- subject { KeycloakApiRails::Helper.read_token_from_query_string(uri) }
5
-
6
- context "when the uri is nil" do
7
- let(:uri) { nil }
8
- it "returns an empty token" do
9
- expect(subject).to eq ""
10
- end
11
- end
12
-
13
- context "when the uri is empty" do
14
- let(:uri) { "" }
15
- it "returns an empty token" do
16
- expect(subject).to eq ""
17
- end
18
- end
19
-
20
- context "when the uri only contains whitespaces" do
21
- let(:uri) { " " }
22
- it "returns an empty token" do
23
- expect(subject).to eq ""
24
- end
25
- end
26
-
27
- context "when the uri has no query string" do
28
- let(:uri) { "http://www.an-url.io/health" }
29
- it "returns no token" do
30
- expect(subject).to be_nil
31
- end
32
- end
33
-
34
- context "when the uri has a query string but no token" do
35
- let(:uri) { "http://www.an-url.io/health?firstName=ouioui" }
36
- it "returns no token" do
37
- expect(subject).to be_nil
38
- end
39
- end
40
-
41
- context "when the uri has a token in the query string" do
42
- let(:uri) { "http://www.an-url.io/health?authorizationToken=aToken" }
43
- it "returns the token" do
44
- expect(subject).to eq "aToken"
45
- end
46
- end
47
-
48
- context "when the uri has a token among other query string parameters" do
49
- let(:uri) { "http://www.an-url.io/health?firstName=ouioui&authorizationToken=aToken&lastName=nonnon" }
50
- it "returns the token" do
51
- expect(subject).to eq "aToken"
52
- end
53
- end
54
-
55
- context "when the uri has an empty token in the query string" do
56
- let(:uri) { "http://www.an-url.io/health?authorizationToken=" }
57
- it "returns an empty token" do
58
- expect(subject).to eq ""
59
- end
60
- end
61
- end
62
-
63
- describe "#create_url_with_token" do
64
-
65
- let(:uri) { "http://www.an-url.io" }
66
- let(:token) { "aToken" }
67
-
68
- before(:each) do
69
- @url_with_token = KeycloakApiRails::Helper.create_url_with_token(uri, token)
70
- end
71
-
72
- context "when the uri has no query string yet" do
73
- it "returns an url with the provided token" do
74
- expect(@url_with_token).to eq "#{uri}?authorizationToken=#{token}"
75
- end
76
- end
77
-
78
- context "when the uri already has no query strings" do
79
- context "but no token yet" do
80
- let(:uri) { "http://www.an-url.io?firstName=ouioui&lastName=nonnon" }
81
- it "returns an url with all the query string and the token" do
82
- expect(@url_with_token).to eq "#{uri}&authorizationToken=#{token}"
83
- end
84
- end
85
-
86
- context "including a token" do
87
- let(:uri) { "http://www.an-url.io?authorizationToken=ouioui&lastName=nonnon" }
88
- it "returns an url with all the query string and the new token" do
89
- expect(@url_with_token).to eq "http://www.an-url.io?lastName=nonnon&authorizationToken=#{token}"
90
- end
91
- end
92
- end
93
- end
94
- end
@@ -1,80 +0,0 @@
1
- RSpec.describe KeycloakApiRails::Service do
2
-
3
- let(:public_key_cache_ttl) { 86400 }
4
- let(:server_url) { "whatever:8080" }
5
- let(:realm_id) { "pouet" }
6
- let!(:resolver) { KeycloakApiRails::PublicKeyCachedResolver.new(server_url, realm_id, public_key_cache_ttl) }
7
-
8
- before(:each) do
9
- resolver.instance_variable_set(:@resolver, KeycloakApiRails::PublicKeyResolverStub.new)
10
- now = Time.local(2018, 1, 9, 12, 0, 0)
11
- Timecop.freeze(now)
12
- end
13
-
14
- after(:each) do
15
- Timecop.return
16
- end
17
-
18
- describe "#find_public_key" do
19
- context "when there is no public key in cache yet" do
20
- before(:each) do
21
- @public_key = resolver.find_public_keys
22
- end
23
-
24
- it "returns a valid public key" do
25
- expect(@public_key).to_not be_nil
26
- end
27
-
28
- it "sets the current time to the resolver" do
29
- expect(resolver.cached_public_key_retrieved_at).to eq Time.now
30
- end
31
- end
32
-
33
- context "when there is already a public key in cache" do
34
- before(:each) do
35
- @first_public_key = resolver.find_public_keys
36
- @first_cached_public_key_retrieved_at = resolver.cached_public_key_retrieved_at
37
- end
38
-
39
- context "and no need to refresh it" do
40
- before(:each) do
41
- Timecop.freeze(Time.now + public_key_cache_ttl.seconds - 10.seconds)
42
- @second_public_key = resolver.find_public_keys
43
- @second_cached_public_key_retrieved_at = resolver.cached_public_key_retrieved_at
44
- end
45
-
46
- it "returns a valid public key" do
47
- expect(@second_public_key).to_not be_nil
48
- end
49
-
50
- it "does not refresh the public key" do
51
- expect(@second_public_key).to eq @first_public_key
52
- end
53
-
54
- it "does not refresh the public key retrieval time" do
55
- expect(@first_cached_public_key_retrieved_at).to eq @second_cached_public_key_retrieved_at
56
- end
57
- end
58
-
59
- context "and its TTL has expired" do
60
- before(:each) do
61
- Timecop.freeze(Time.now + public_key_cache_ttl.seconds + 10.seconds)
62
- @second_public_key = resolver.find_public_keys
63
- @second_cached_public_key_retrieved_at = resolver.cached_public_key_retrieved_at
64
- end
65
-
66
- it "returns a valid public key" do
67
- expect(@second_public_key).to_not be_nil
68
- end
69
-
70
- it "refreshes the public key" do
71
- expect(@second_public_key).to_not eq @first_public_key
72
- end
73
-
74
- it "refreshes the public key retrieval time" do
75
- expect(@first_cached_public_key_retrieved_at).to_not eq @second_cached_public_key_retrieved_at
76
- end
77
- end
78
- end
79
- end
80
- end
@@ -1,281 +0,0 @@
1
- RSpec.describe KeycloakApiRails::Service do
2
-
3
- let!(:private_key) { OpenSSL::PKey::RSA.generate(2048) }
4
- let!(:public_key) { private_key.public_key }
5
- let!(:key_resolver) { KeycloakApiRails::PublicKeyCachedResolverStub.new(public_key) }
6
- let!(:service) { KeycloakApiRails::Service.new(key_resolver) }
7
-
8
- before(:each) do
9
- now = Time.local(2018, 1, 9, 12, 0, 0)
10
- Timecop.freeze(now)
11
- end
12
-
13
- after(:each) do
14
- Timecop.return
15
- end
16
-
17
- describe "#decode_and_verify" do
18
- def create_token(private_key, expiration_date, algorithm)
19
- claim = {
20
- iss: "KeycloakApiRails",
21
- exp: expiration_date,
22
- nbf: Time.local(2018, 1, 1, 0, 0, 0)
23
- }
24
- jws = JSON::JWT.new(claim).sign(private_key, algorithm)
25
- jws.to_s
26
- end
27
-
28
- context "when token is nil" do
29
- let(:token) { nil }
30
- it "should raise an error :no_token" do
31
- expect {
32
- service.decode_and_verify(token)
33
- }.to raise_error(TokenError, "No JWT token provided")
34
- end
35
- end
36
-
37
- context "when token is an empty string" do
38
- let(:token) { "" }
39
- it "should raise an error :no_token" do
40
- expect {
41
- service.decode_and_verify(token)
42
- }.to raise_error(TokenError, "No JWT token provided")
43
- end
44
- end
45
-
46
- context "when token is in an invalid format" do
47
- let(:token) { "coucou" }
48
- it "should raise an error :invalid_format" do
49
- expect {
50
- service.decode_and_verify(token)
51
- }.to raise_error(TokenError, "Wrong JWT Format")
52
- end
53
- end
54
-
55
- context "when token is in a valid format" do
56
- let(:algorithm) { :RS256 }
57
- let(:expiration_date) { 1.week.from_now }
58
-
59
- context "and token is generated by another private key" do
60
- let(:another_private_key) { OpenSSL::PKey::RSA.generate(1024) }
61
- let(:token) { create_token(another_private_key, expiration_date, algorithm) }
62
-
63
- it "should raise an error :verification_failed" do
64
- expect {
65
- service.decode_and_verify(token)
66
- }.to raise_error(TokenError, "Failed to verify JWT token")
67
- end
68
- end
69
-
70
- context "and token is generated by the right private key" do
71
- let(:token) { create_token(private_key, expiration_date, algorithm) }
72
-
73
- context "and token is expired" do
74
- let(:expiration_date) { Time.now - 2.days }
75
-
76
- it "should raise an error :expiration_date" do
77
- expect {
78
- service.decode_and_verify(token)
79
- }.to raise_error(TokenError, "JWT token is expired")
80
- end
81
- end
82
-
83
- # The service reads `token_expiration_tolerance_in_seconds` when it is instantiated:
84
- # a token expiring within that tolerance is already considered as expired.
85
- context "and token expires within the expiration tolerance" do
86
- let(:expiration_date) { Time.now + 5 }
87
-
88
- it "should raise an error :expiration_date" do
89
- expect(KeycloakApiRails.config.token_expiration_tolerance_in_seconds).to eq 10
90
- expect {
91
- service.decode_and_verify(token)
92
- }.to raise_error(TokenError, "JWT token is expired")
93
- end
94
- end
95
-
96
- context "and token expires just after the expiration tolerance" do
97
- let(:expiration_date) { Time.now + 30 }
98
-
99
- it "should return a not-nil decoded token" do
100
- expect(KeycloakApiRails.config.token_expiration_tolerance_in_seconds).to eq 10
101
- expect(service.decode_and_verify(token)).to_not be_nil
102
- end
103
- end
104
-
105
- context "and token is not expired" do
106
- let(:expiration_date) { Time.now + 2.days }
107
-
108
- context "and token is encrypted using RS256" do
109
- let(:algorithm) { :RS256 }
110
-
111
- it "should return a not-nil decoded token" do
112
- expect(service.decode_and_verify(token)).to_not be_nil
113
- end
114
- end
115
-
116
- context "and token is encrypted using RS512" do
117
- let(:algorithm) { :RS512 }
118
-
119
- it "should return a not-nil decoded token" do
120
- expect(service.decode_and_verify(token)).to_not be_nil
121
- end
122
- end
123
- end
124
- end
125
- end
126
- end
127
-
128
- describe "#need_middleware_authentication?" do
129
-
130
- let(:method) { nil }
131
- let(:path) { nil }
132
- let(:headers) { {} }
133
-
134
-
135
- before(:each) do
136
- KeycloakApiRails.config.skip_paths = {
137
- post: [/^\/skip/],
138
- get: [/^\/skip/]
139
- }
140
- @result = service.need_middleware_authentication?(method, path, headers)
141
- end
142
-
143
- context "when method is nil" do
144
- let(:method) { nil }
145
- let(:path) { "/do-not-skip" }
146
- it "should return true" do
147
- expect(@result).to be true
148
- end
149
- end
150
-
151
- context "when path is nil" do
152
- let(:method) { :get }
153
- let(:path) { nil }
154
- it "should return true" do
155
- expect(@result).to be true
156
- end
157
- end
158
-
159
- context "when method does not match the configuration" do
160
- let(:method) { :put }
161
- let(:path) { "/skip" }
162
- it "should return true" do
163
- expect(@result).to be true
164
- end
165
- end
166
-
167
- context "when path does not match the configuration" do
168
- let(:method) { :get }
169
- let(:path) { "/do-not-skip" }
170
- it "should return true" do
171
- expect(@result).to be true
172
- end
173
- end
174
-
175
- context "when method [get] and path do match the configuration" do
176
- let(:method) { :get }
177
- let(:path) { "/skip" }
178
- it "should return false" do
179
- expect(@result).to be false
180
- end
181
- end
182
-
183
-
184
- context "when method [post] and path do match the configuration" do
185
- let(:method) { :get }
186
- let(:path) { "/skip" }
187
- it "should return false" do
188
- expect(@result).to be false
189
- end
190
- end
191
-
192
- context "when the request is preflight" do
193
- let(:method) { :options }
194
- let(:headers) { { "HTTP_ACCESS_CONTROL_REQUEST_METHOD" => ["Authorization"] } }
195
- let(:path) { "/do-not-skip" }
196
- it "should return false" do
197
- expect(@result).to be false
198
- end
199
- end
200
-
201
- context "when configured as opt_in" do
202
- before do
203
- KeycloakApiRails.config.opt_in = true
204
- service2 = KeycloakApiRails::Service.new(key_resolver)
205
- @result = service2.need_middleware_authentication?(method, path, headers)
206
- end
207
-
208
- # The configuration is global: leaving 'opt_in' enabled would disable the authentication
209
- # of every example running afterwards.
210
- after do
211
- KeycloakApiRails.config.opt_in = false
212
- end
213
-
214
- it "should return false" do
215
- expect(@result).to be false
216
- end
217
- end
218
- end
219
-
220
- describe "#read_token" do
221
- let(:query_string) { "" }
222
- let(:url) { "http://api.service.com/api/health?aParameter=true#{query_string}" }
223
- let(:headers) { {} }
224
- let(:header_token) { "header_token" }
225
- let(:query_string_token) { "query_string_token" }
226
-
227
- before(:each) do
228
- @token = service.read_token(url, headers)
229
- end
230
-
231
- context "when the token is provided in the Authorization headers" do
232
- let(:headers) do
233
- {
234
- "HTTP_AUTHORIZATION" => "Bearer #{header_token}"
235
- }
236
- end
237
- context "and not in the query string" do
238
- let(:query_string) { "" }
239
- it "returns the header token" do
240
- expect(@token).to eq header_token
241
- end
242
- end
243
-
244
- context "and also in the query string" do
245
- let(:query_string) { "&authorizationToken=#{query_string_token}" }
246
- it "returns the query string token" do
247
- expect(@token).to eq query_string_token
248
- end
249
- end
250
- end
251
-
252
- context "when the token is not provided in the Authorization headers" do
253
- let(:headers) do
254
- {
255
- "ANOTHER_HEADER" => header_token
256
- }
257
- end
258
-
259
- context "and not in the query string" do
260
- let(:query_string) { "" }
261
- it "returns an empty token" do
262
- expect(@token).to eq ""
263
- end
264
- end
265
-
266
- context "and query string is nil" do
267
- let(:query_string) { nil }
268
- it "returns an empty token" do
269
- expect(@token).to eq ""
270
- end
271
- end
272
-
273
- context "but in the query string" do
274
- let(:query_string) { "&authorizationToken=#{query_string_token}" }
275
- it "returns the query string token" do
276
- expect(@token).to eq query_string_token
277
- end
278
- end
279
- end
280
- end
281
- end