keycloak-api-rails 1.0.0 → 1.0.1

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: c8848fb5696ab7e2962d8491b2df8e17c1bc88a3f08bb4b328b96e872b5858de
4
- data.tar.gz: 9b2f8541d0a2e26f59cec67fcd6573e34c898db12a769335faa701a9c15b66be
3
+ metadata.gz: 72a9cf5907ae0256e7acfbf9bd9e453dcbcaeae5fd4c5ce146780e3ef5ec2644
4
+ data.tar.gz: ac22d1edfbf8d1701ac82a213f3052ca32976dd766841111066600910497d58d
5
5
  SHA512:
6
- metadata.gz: e7f9ca820ba2eb32fca2bcbf72c9a19e70ed2bc62e95f6f059fcd848f7aafbb40847017a012a93ff7b5f55fe029b4e7cd353f2376eb90c7a5c0b11f4fac4e310
7
- data.tar.gz: 57a3a77dec4b4daeb697ee3b3d8f4ed4ad349c2b68e60ca7001e46be2809a358c1e24e6dd1a3e2d31270be9e0e8daaac0176511e7009ce9cca4ea591e8040ba7
6
+ metadata.gz: e73d57fcaf904c534048ff6d6e611913298b173be1e47b01b31c7c6cb351f2e8d74b00a57027ce03063b63391f2200c9c8d866a18a7cb632856acb77bdb5d494
7
+ data.tar.gz: be4136364d974a0ebcba62a31f526341cdef89f0f87f991dde409cb40a36303f0ae4a781599929caf830cf89a58ce01b20e2c7209ef7613c9e0d465b831bbb17
data/CHANGELOG.md CHANGED
@@ -5,7 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [1.0.0] - 2025-06-20
8
+ ## [1.0.1] - 2026-07-30
9
+
10
+ * Dependencies: Upgrade test dependency `timecop` to `0.9.11`
11
+ * Upgrade Docker image to Ruby 3.4
12
+ * Remove all usages of `ActiveSupport` from the library, replaced by plain Ruby
13
+
14
+ ## [1.0.0] - 2025-12-10
9
15
 
10
16
  * *[BREAKING CHANGE!]* Rename top-level module from `Keycloak` to `KeycloakApiRails` to avoid conflicts with other gems. (thanks to @eilers)
11
17
  * *Migration: Please do a global find and replace of `Keycloak.` to `KeycloakApiRails.` in your application.*
data/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM ruby:3.2.2-slim-bullseye
1
+ FROM ruby:3.4.10-slim-bookworm
2
2
 
3
3
  RUN apt-get update -qq && apt-get install -y build-essential git ruby-dev && apt-get clean && \
4
4
  mkdir -p /usr/src/app/lib/keycloak-api-rails
data/Gemfile.lock CHANGED
@@ -8,31 +8,31 @@ PATH
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- action_text-trix (2.1.15)
11
+ action_text-trix (2.1.19)
12
12
  railties
13
- actioncable (8.1.1)
14
- actionpack (= 8.1.1)
15
- activesupport (= 8.1.1)
13
+ actioncable (8.1.3.1)
14
+ actionpack (= 8.1.3.1)
15
+ activesupport (= 8.1.3.1)
16
16
  nio4r (~> 2.0)
17
17
  websocket-driver (>= 0.6.1)
18
18
  zeitwerk (~> 2.6)
19
- actionmailbox (8.1.1)
20
- actionpack (= 8.1.1)
21
- activejob (= 8.1.1)
22
- activerecord (= 8.1.1)
23
- activestorage (= 8.1.1)
24
- activesupport (= 8.1.1)
19
+ actionmailbox (8.1.3.1)
20
+ actionpack (= 8.1.3.1)
21
+ activejob (= 8.1.3.1)
22
+ activerecord (= 8.1.3.1)
23
+ activestorage (= 8.1.3.1)
24
+ activesupport (= 8.1.3.1)
25
25
  mail (>= 2.8.0)
26
- actionmailer (8.1.1)
27
- actionpack (= 8.1.1)
28
- actionview (= 8.1.1)
29
- activejob (= 8.1.1)
30
- activesupport (= 8.1.1)
26
+ actionmailer (8.1.3.1)
27
+ actionpack (= 8.1.3.1)
28
+ actionview (= 8.1.3.1)
29
+ activejob (= 8.1.3.1)
30
+ activesupport (= 8.1.3.1)
31
31
  mail (>= 2.8.0)
32
32
  rails-dom-testing (~> 2.2)
33
- actionpack (8.1.1)
34
- actionview (= 8.1.1)
35
- activesupport (= 8.1.1)
33
+ actionpack (8.1.3.1)
34
+ actionview (= 8.1.3.1)
35
+ activesupport (= 8.1.3.1)
36
36
  nokogiri (>= 1.8.5)
37
37
  rack (>= 2.2.4)
38
38
  rack-session (>= 1.0.1)
@@ -40,36 +40,36 @@ GEM
40
40
  rails-dom-testing (~> 2.2)
41
41
  rails-html-sanitizer (~> 1.6)
42
42
  useragent (~> 0.16)
43
- actiontext (8.1.1)
43
+ actiontext (8.1.3.1)
44
44
  action_text-trix (~> 2.1.15)
45
- actionpack (= 8.1.1)
46
- activerecord (= 8.1.1)
47
- activestorage (= 8.1.1)
48
- activesupport (= 8.1.1)
45
+ actionpack (= 8.1.3.1)
46
+ activerecord (= 8.1.3.1)
47
+ activestorage (= 8.1.3.1)
48
+ activesupport (= 8.1.3.1)
49
49
  globalid (>= 0.6.0)
50
50
  nokogiri (>= 1.8.5)
51
- actionview (8.1.1)
52
- activesupport (= 8.1.1)
51
+ actionview (8.1.3.1)
52
+ activesupport (= 8.1.3.1)
53
53
  builder (~> 3.1)
54
54
  erubi (~> 1.11)
55
55
  rails-dom-testing (~> 2.2)
56
56
  rails-html-sanitizer (~> 1.6)
57
- activejob (8.1.1)
58
- activesupport (= 8.1.1)
57
+ activejob (8.1.3.1)
58
+ activesupport (= 8.1.3.1)
59
59
  globalid (>= 0.3.6)
60
- activemodel (8.1.1)
61
- activesupport (= 8.1.1)
62
- activerecord (8.1.1)
63
- activemodel (= 8.1.1)
64
- activesupport (= 8.1.1)
60
+ activemodel (8.1.3.1)
61
+ activesupport (= 8.1.3.1)
62
+ activerecord (8.1.3.1)
63
+ activemodel (= 8.1.3.1)
64
+ activesupport (= 8.1.3.1)
65
65
  timeout (>= 0.4.0)
66
- activestorage (8.1.1)
67
- actionpack (= 8.1.1)
68
- activejob (= 8.1.1)
69
- activerecord (= 8.1.1)
70
- activesupport (= 8.1.1)
66
+ activestorage (8.1.3.1)
67
+ actionpack (= 8.1.3.1)
68
+ activejob (= 8.1.3.1)
69
+ activerecord (= 8.1.3.1)
70
+ activesupport (= 8.1.3.1)
71
71
  marcel (~> 1.0)
72
- activesupport (8.1.1)
72
+ activesupport (8.1.3.1)
73
73
  base64
74
74
  bigdecimal
75
75
  concurrent-ruby (~> 1.0, >= 1.3.1)
@@ -84,37 +84,38 @@ GEM
84
84
  uri (>= 0.13.1)
85
85
  aes_key_wrap (1.1.0)
86
86
  base64 (0.3.0)
87
- bigdecimal (3.3.1)
88
- bindata (2.5.1)
87
+ bigdecimal (4.1.2)
88
+ bindata (3.0.0)
89
89
  builder (3.3.0)
90
90
  byebug (12.0.0)
91
- concurrent-ruby (1.3.5)
91
+ concurrent-ruby (1.3.8)
92
92
  connection_pool (3.0.2)
93
- crass (1.0.6)
93
+ crass (1.0.7)
94
94
  date (3.5.1)
95
95
  diff-lcs (1.6.2)
96
96
  drb (2.2.3)
97
- erb (6.0.0)
97
+ erb (6.0.6)
98
98
  erubi (1.13.1)
99
- faraday (2.14.0)
99
+ faraday (2.14.3)
100
100
  faraday-net_http (>= 2.0, < 3.5)
101
101
  json
102
102
  logger
103
- faraday-follow_redirects (0.4.0)
103
+ faraday-follow_redirects (0.5.0)
104
104
  faraday (>= 1, < 3)
105
- faraday-net_http (3.4.2)
105
+ faraday-net_http (3.4.4)
106
106
  net-http (~> 0.5)
107
- globalid (1.3.0)
107
+ globalid (1.4.0)
108
108
  activesupport (>= 6.1)
109
- i18n (1.14.7)
109
+ i18n (1.15.2)
110
110
  concurrent-ruby (~> 1.0)
111
- io-console (0.8.1)
112
- irb (1.15.3)
111
+ io-console (0.8.2)
112
+ irb (1.18.0)
113
113
  pp (>= 0.6.0)
114
+ prism (>= 1.3.0)
114
115
  rdoc (>= 4.0.0)
115
116
  reline (>= 0.4.2)
116
- json (2.17.1)
117
- json-jwt (1.17.0)
117
+ json (2.21.1)
118
+ json-jwt (1.17.1)
118
119
  activesupport (>= 4.2)
119
120
  aes_key_wrap
120
121
  base64
@@ -122,22 +123,24 @@ GEM
122
123
  faraday (~> 2.0)
123
124
  faraday-follow_redirects
124
125
  logger (1.7.0)
125
- loofah (2.24.1)
126
+ loofah (2.25.2)
126
127
  crass (~> 1.0.2)
127
128
  nokogiri (>= 1.12.0)
128
- mail (2.9.0)
129
+ mail (2.9.1)
129
130
  logger
130
131
  mini_mime (>= 0.1.1)
131
132
  net-imap
132
133
  net-pop
133
134
  net-smtp
134
- marcel (1.1.0)
135
+ marcel (1.2.1)
135
136
  mini_mime (1.1.5)
136
137
  mini_portile2 (2.8.9)
137
- minitest (5.26.2)
138
- net-http (0.8.0)
138
+ minitest (6.0.6)
139
+ drb (~> 2.0)
140
+ prism (~> 1.5)
141
+ net-http (0.9.1)
139
142
  uri (>= 0.11.1)
140
- net-imap (0.5.12)
143
+ net-imap (0.6.6)
141
144
  date
142
145
  net-protocol
143
146
  net-pop (0.1.2)
@@ -147,58 +150,61 @@ GEM
147
150
  net-smtp (0.5.1)
148
151
  net-protocol
149
152
  nio4r (2.7.5)
150
- nokogiri (1.18.10)
153
+ nokogiri (1.19.4)
151
154
  mini_portile2 (~> 2.8.2)
152
155
  racc (~> 1.4)
153
- pp (0.6.3)
156
+ pp (0.6.4)
154
157
  prettyprint
155
158
  prettyprint (0.2.0)
156
- psych (5.3.0)
157
- date
158
- stringio
159
+ prism (1.9.0)
159
160
  racc (1.8.1)
160
- rack (3.2.4)
161
- rack-session (2.1.1)
161
+ rack (3.2.6)
162
+ rack-session (2.1.2)
162
163
  base64 (>= 0.1.0)
163
164
  rack (>= 3.0.0)
164
165
  rack-test (2.2.0)
165
166
  rack (>= 1.3)
166
167
  rackup (2.3.1)
167
168
  rack (>= 3)
168
- rails (8.1.1)
169
- actioncable (= 8.1.1)
170
- actionmailbox (= 8.1.1)
171
- actionmailer (= 8.1.1)
172
- actionpack (= 8.1.1)
173
- actiontext (= 8.1.1)
174
- actionview (= 8.1.1)
175
- activejob (= 8.1.1)
176
- activemodel (= 8.1.1)
177
- activerecord (= 8.1.1)
178
- activestorage (= 8.1.1)
179
- activesupport (= 8.1.1)
169
+ rails (8.1.3.1)
170
+ actioncable (= 8.1.3.1)
171
+ actionmailbox (= 8.1.3.1)
172
+ actionmailer (= 8.1.3.1)
173
+ actionpack (= 8.1.3.1)
174
+ actiontext (= 8.1.3.1)
175
+ actionview (= 8.1.3.1)
176
+ activejob (= 8.1.3.1)
177
+ activemodel (= 8.1.3.1)
178
+ activerecord (= 8.1.3.1)
179
+ activestorage (= 8.1.3.1)
180
+ activesupport (= 8.1.3.1)
180
181
  bundler (>= 1.15.0)
181
- railties (= 8.1.1)
182
+ railties (= 8.1.3.1)
182
183
  rails-dom-testing (2.3.0)
183
184
  activesupport (>= 5.0.0)
184
185
  minitest
185
186
  nokogiri (>= 1.6)
186
- rails-html-sanitizer (1.6.2)
187
- loofah (~> 2.21)
187
+ rails-html-sanitizer (1.7.1)
188
+ loofah (~> 2.25, >= 2.25.2)
188
189
  nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
189
- railties (8.1.1)
190
- actionpack (= 8.1.1)
191
- activesupport (= 8.1.1)
190
+ railties (8.1.3.1)
191
+ actionpack (= 8.1.3.1)
192
+ activesupport (= 8.1.3.1)
192
193
  irb (~> 1.13)
193
194
  rackup (>= 1.0.0)
194
195
  rake (>= 12.2)
195
196
  thor (~> 1.0, >= 1.2.2)
196
197
  tsort (>= 0.2)
197
198
  zeitwerk (~> 2.6)
198
- rake (13.3.1)
199
- rdoc (6.17.0)
199
+ rake (13.4.2)
200
+ rbs (4.1.1)
201
+ logger
202
+ prism (>= 1.6.0)
203
+ tsort
204
+ rdoc (8.0.0)
200
205
  erb
201
- psych (>= 4.0.0)
206
+ prism (>= 1.6.0)
207
+ rbs (>= 4.0.0)
202
208
  tsort
203
209
  reline (0.6.3)
204
210
  io-console (~> 0.5)
@@ -211,25 +217,24 @@ GEM
211
217
  rspec-expectations (3.13.5)
212
218
  diff-lcs (>= 1.2.0, < 2.0)
213
219
  rspec-support (~> 3.13.0)
214
- rspec-mocks (3.13.7)
220
+ rspec-mocks (3.13.8)
215
221
  diff-lcs (>= 1.2.0, < 2.0)
216
222
  rspec-support (~> 3.13.0)
217
- rspec-support (3.13.6)
223
+ rspec-support (3.13.7)
218
224
  securerandom (0.4.1)
219
- stringio (3.1.9)
220
- thor (1.4.0)
221
- timecop (0.9.10)
222
- timeout (0.5.0)
225
+ thor (1.5.0)
226
+ timecop (0.9.11)
227
+ timeout (0.6.1)
223
228
  tsort (0.2.0)
224
229
  tzinfo (2.0.6)
225
230
  concurrent-ruby (~> 1.0)
226
231
  uri (1.1.1)
227
232
  useragent (0.16.11)
228
- websocket-driver (0.8.0)
233
+ websocket-driver (0.8.2)
229
234
  base64
230
235
  websocket-extensions (>= 0.1.0)
231
236
  websocket-extensions (0.1.5)
232
- zeitwerk (2.7.3)
237
+ zeitwerk (2.8.2)
233
238
 
234
239
  PLATFORMS
235
240
  ruby
@@ -238,7 +243,7 @@ DEPENDENCIES
238
243
  byebug (= 12.0.0)
239
244
  keycloak-api-rails!
240
245
  rspec (= 3.13.2)
241
- timecop (= 0.9.10)
246
+ timecop (= 0.9.11)
242
247
 
243
248
  BUNDLED WITH
244
249
  2.4.13
data/README.md CHANGED
@@ -5,7 +5,7 @@ This gem validates Keycloak JWT token for Ruby On Rails APIs.
5
5
  ## Install
6
6
 
7
7
  ```ruby
8
- gem "keycloak-api-rails", "1.0.0"
8
+ gem "keycloak-api-rails", "1.0.1"
9
9
  ```
10
10
 
11
11
  ## Token validation
@@ -63,7 +63,7 @@ All options have a default value. However, all of them can be changed in your in
63
63
  Create a `keycloak.rb` file in your Rails `config/initializers` folder. For instance:
64
64
 
65
65
  ```
66
- Keycloak.configure do |config|
66
+ KeycloakApiRails.configure do |config|
67
67
  config.server_url = ENV["KEYCLOAK_SERVER_URL"]
68
68
  config.realm_id = ENV["KEYCLOAK_REALM_ID"]
69
69
  config.logger = Rails.logger
@@ -77,7 +77,7 @@ end
77
77
  Or using opt-in configuration:
78
78
 
79
79
  ```ruby
80
- Keycloak.configure do |config|
80
+ KeycloakApiRails.configure do |config|
81
81
  config.server_url = ENV["KEYCLOAK_SERVER_URL"]
82
82
  config.realm_id = ENV["KEYCLOAK_REALM_ID"]
83
83
  config.logger = Rails.logger
@@ -163,14 +163,14 @@ This should output `https://api.pouet.io/api/more-pouets?authorizationToken=myTo
163
163
 
164
164
  ### Accessing Keycloak Service
165
165
 
166
- A lazy-loaded service KeycloakApiRails::Service can be accessed using `Keycloak.service`.
166
+ A lazy-loaded service KeycloakApiRails::Service can be accessed using `KeycloakApiRails.service`.
167
167
  For instance, to read a provided token:
168
168
  ```ruby
169
169
  class RenderTokenController < ApplicationController
170
170
  def show
171
171
  uri = request.env["REQUEST_URI"]
172
172
  headers = request.env
173
- token = Keycloak.service.read_token(uri, headers)
173
+ token = KeycloakApiRails.service.read_token(uri, headers)
174
174
  render json: { token: token }, status: :ok
175
175
  end
176
176
  end
@@ -200,10 +200,10 @@ let(:jwt) do
200
200
  token.sign($private_key, :RS256).to_s
201
201
  end
202
202
  ```
203
- * Finally, in the same `shared_context`, stub `Keycloak.public_key_resolver` to use a valid public key that is able to validate `jwt`:
203
+ * Finally, in the same `shared_context`, stub `KeycloakApiRails.public_key_resolver` to use a valid public key that is able to validate `jwt`:
204
204
  ```ruby
205
205
  before(:each) do
206
- public_key_resolver = Keycloak.public_key_resolver
206
+ public_key_resolver = KeycloakApiRails.public_key_resolver
207
207
  allow(public_key_resolver).to receive(:find_public_keys) { JSON::JWK::Set.new(JSON::JWK.new($private_key, kid: "default")) }
208
208
  end
209
209
  ```
@@ -216,7 +216,7 @@ From the `keycloak-rails-api` directory:
216
216
  $ docker build . -t keycloak-rails-api:test
217
217
  $ docker run -v `pwd`:/usr/src/app/ keycloak-rails-api:test bundle exec rspec spec
218
218
  ```
219
- rspe
219
+
220
220
  ## Next developments
221
221
 
222
222
  * Manage multiple realms
@@ -19,6 +19,6 @@ Gem::Specification.new do |spec|
19
19
  spec.add_dependency "json-jwt", ">= 1.11.0"
20
20
 
21
21
  spec.add_development_dependency "rspec", "3.13.2"
22
- spec.add_development_dependency "timecop", "0.9.10"
22
+ spec.add_development_dependency "timecop", "0.9.11"
23
23
  spec.add_development_dependency "byebug", "12.0.0"
24
24
  end
@@ -1,10 +1,8 @@
1
1
  module KeycloakApiRails
2
2
  module Authentication
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- if respond_to?(:helper_method)
7
- helper_method :keycloak_authenticate
3
+ def self.included(base)
4
+ if base.respond_to?(:helper_method)
5
+ base.helper_method :keycloak_authenticate
8
6
  end
9
7
  end
10
8
 
@@ -1,14 +1,13 @@
1
1
  module KeycloakApiRails
2
2
  class Configuration
3
- include ActiveSupport::Configurable
4
- config_accessor :server_url
5
- config_accessor :realm_id
6
- config_accessor :skip_paths
7
- config_accessor :opt_in
8
- config_accessor :token_expiration_tolerance_in_seconds
9
- config_accessor :public_key_cache_ttl
10
- config_accessor :custom_attributes
11
- config_accessor :logger
12
- config_accessor :ca_certificate_file
3
+ attr_accessor :server_url
4
+ attr_accessor :realm_id
5
+ attr_accessor :skip_paths
6
+ attr_accessor :opt_in
7
+ attr_accessor :token_expiration_tolerance_in_seconds
8
+ attr_accessor :public_key_cache_ttl
9
+ attr_accessor :custom_attributes
10
+ attr_accessor :logger
11
+ attr_accessor :ca_certificate_file
13
12
  end
14
13
  end
@@ -83,11 +83,11 @@ module KeycloakApiRails
83
83
  end
84
84
 
85
85
  def self.read_token_from_query_string(uri)
86
- if uri.present?
86
+ if uri && !uri.strip.empty?
87
87
  parsed_uri = URI.parse(uri)
88
88
  query = URI.decode_www_form(parsed_uri.query || "")
89
89
  query_string_token = query.detect { |param| param.first == QUERY_STRING_TOKEN_KEY }
90
- query_string_token&.second
90
+ query_string_token&.at(1)
91
91
  else
92
92
  ""
93
93
  end
@@ -24,7 +24,7 @@ module KeycloakApiRails
24
24
  private
25
25
 
26
26
  def public_keys_are_outdated?
27
- @cached_public_keys.nil? || @cached_public_key_retrieved_at.nil? || Time.now > (@cached_public_key_retrieved_at + @public_key_cache_ttl.seconds)
27
+ @cached_public_keys.nil? || @cached_public_key_retrieved_at.nil? || Time.now > (@cached_public_key_retrieved_at + @public_key_cache_ttl)
28
28
  end
29
29
  end
30
30
  end
@@ -53,8 +53,8 @@ module KeycloakApiRails
53
53
  end
54
54
 
55
55
  def expired?(token)
56
- token_expiration = Time.at(token["exp"]).to_datetime
57
- token_expiration < Time.now + @token_expiration_tolerance_in_seconds.seconds
56
+ token_expiration = Time.at(token["exp"])
57
+ token_expiration < Time.now + @token_expiration_tolerance_in_seconds
58
58
  end
59
59
  end
60
60
  end
@@ -1,3 +1,3 @@
1
1
  module KeycloakApiRails
2
- VERSION = "1.0.0"
2
+ VERSION = "1.0.1"
3
3
  end
@@ -0,0 +1,18 @@
1
+ require "rbconfig"
2
+
3
+ RSpec.describe "The library without ActiveSupport" do
4
+
5
+ let(:probe) { File.expand_path("../../support/activesupport_free_probe.rb", __FILE__) }
6
+
7
+ # RUBYOPT is cleared so that nothing set up by bundler is preloaded in the probe process.
8
+ def run_probe
9
+ output = IO.popen({ "RUBYOPT" => nil }, [RbConfig.ruby, probe], err: [:child, :out], &:read)
10
+ [$?.exitstatus, output]
11
+ end
12
+
13
+ it "loads and behaves the same way when ActiveSupport is not available" do
14
+ status, output = run_probe
15
+
16
+ expect(status).to eq(0), "The library relies on ActiveSupport:\n#{output}"
17
+ end
18
+ end
@@ -17,6 +17,44 @@ describe KeycloakApiRails::Authentication do
17
17
  }
18
18
  end
19
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
+
20
58
  describe "#keycloak_authenticate" do
21
59
  before do
22
60
  # Mock request object because we aren't using real request spec
@@ -0,0 +1,101 @@
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,4 +1,65 @@
1
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
+
2
63
  describe "#create_url_with_token" do
3
64
 
4
65
  let(:uri) { "http://www.an-url.io" }
@@ -80,9 +80,31 @@ RSpec.describe KeycloakApiRails::Service do
80
80
  end
81
81
  end
82
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
+
83
105
  context "and token is not expired" do
84
106
  let(:expiration_date) { Time.now + 2.days }
85
-
107
+
86
108
  context "and token is encrypted using RS256" do
87
109
  let(:algorithm) { :RS256 }
88
110
 
@@ -0,0 +1,131 @@
1
+ # Loads the library and exercises every code path that used to rely on ActiveSupport,
2
+ # in a process where ActiveSupport is *not* loaded.
3
+ #
4
+ # The regular test suite loads `rails/all`, which makes ActiveSupport available everywhere and
5
+ # therefore cannot detect that the library depends on it again. This probe is executed in a
6
+ # separate process by spec/keycloak-api-rails/activesupport_free_spec.rb.
7
+ #
8
+ # The library files are required one by one on purpose: `lib/keycloak-api-rails.rb` pulls in
9
+ # `json/jwt`, which depends on ActiveSupport. The point of this probe is that *our* code does not
10
+ # use ActiveSupport, not that it never gets loaded by a third party.
11
+
12
+ require "uri"
13
+ require "date"
14
+ require "logger"
15
+
16
+ LIB = File.expand_path("../../../lib/keycloak-api-rails", __FILE__)
17
+
18
+ # Minimal stand-in for the parts of lib/keycloak-api-rails.rb the required files call at runtime.
19
+ module KeycloakApiRails
20
+ class << self
21
+ attr_accessor :config
22
+ end
23
+ end
24
+
25
+ require "#{LIB}/configuration"
26
+ require "#{LIB}/helper"
27
+ require "#{LIB}/authentication"
28
+ require "#{LIB}/public_key_resolver"
29
+ require "#{LIB}/public_key_cached_resolver"
30
+ require "#{LIB}/service"
31
+
32
+ $failures = []
33
+
34
+ def assert(description)
35
+ $failures << description unless yield
36
+ rescue => e
37
+ $failures << "#{description} (raised #{e.class}: #{e.message})"
38
+ end
39
+
40
+ assert("ActiveSupport is not loaded by the library") { !defined?(ActiveSupport) }
41
+
42
+ ### Configuration -- used to be ActiveSupport::Configurable
43
+ configuration = KeycloakApiRails::Configuration.new
44
+ KeycloakApiRails.config = configuration
45
+
46
+ %i[
47
+ server_url realm_id skip_paths opt_in token_expiration_tolerance_in_seconds
48
+ public_key_cache_ttl custom_attributes logger ca_certificate_file
49
+ ].each do |name|
50
+ assert("Configuration##{name} is nil until it is assigned") { configuration.public_send(name).nil? }
51
+ configuration.public_send("#{name}=", "a value for #{name}")
52
+ assert("Configuration##{name} returns what was assigned") { configuration.public_send(name) == "a value for #{name}" }
53
+ end
54
+
55
+ ### Authentication -- used to be ActiveSupport::Concern
56
+ declared_helper_methods = []
57
+ controller_with_helpers = Class.new do
58
+ define_singleton_method(:helper_method) { |*names| declared_helper_methods.concat(names) }
59
+ end
60
+ controller_with_helpers.include(KeycloakApiRails::Authentication)
61
+ assert("Authentication declares keycloak_authenticate as a helper method") do
62
+ declared_helper_methods == [:keycloak_authenticate]
63
+ end
64
+ assert("Authentication can be included in a class without helper_method") do
65
+ Class.new.include(KeycloakApiRails::Authentication)
66
+ true
67
+ end
68
+ assert("Authentication keeps its methods protected") do
69
+ KeycloakApiRails::Authentication.protected_instance_methods.sort ==
70
+ %i[authentication_failed authentication_succeeded keycloak_authenticate]
71
+ end
72
+
73
+ ### Helper#read_token_from_query_string -- used to be `present?` and `second`
74
+ {
75
+ nil => "",
76
+ "" => "",
77
+ " " => "",
78
+ "http://www.an-url.io/health" => nil,
79
+ "http://www.an-url.io/health?firstName=ouioui" => nil,
80
+ "http://www.an-url.io/health?authorizationToken=aToken" => "aToken",
81
+ "http://www.an-url.io/health?a=1&authorizationToken=aToken&b=2" => "aToken",
82
+ }.each do |uri, expected|
83
+ assert("Helper.read_token_from_query_string(#{uri.inspect}) returns #{expected.inspect}") do
84
+ KeycloakApiRails::Helper.read_token_from_query_string(uri) == expected
85
+ end
86
+ end
87
+
88
+ ### Service#expired? -- used to be `to_datetime` and `seconds`
89
+ configuration.token_expiration_tolerance_in_seconds = 10
90
+ configuration.skip_paths = {}
91
+ configuration.opt_in = false
92
+ configuration.logger = ::Logger.new(File::NULL)
93
+ service = KeycloakApiRails::Service.new(nil)
94
+ now = Time.now
95
+
96
+ {
97
+ "expiring in one hour" => [(now + 3600).to_i, false],
98
+ "expired two days ago" => [(now - 172800).to_i, true],
99
+ "expiring within the tolerance (5s < 10s)" => [(now + 5).to_i, true],
100
+ "expiring after the tolerance (30s > 10s)" => [(now + 30).to_i, false],
101
+ }.each do |description, (exp, expected)|
102
+ assert("Service#expired? returns #{expected} for a token #{description}") do
103
+ service.send(:expired?, { "exp" => exp }) == expected
104
+ end
105
+ end
106
+
107
+ ### PublicKeyCachedResolver -- used to be `seconds`
108
+ ttl = 86400
109
+ resolver = KeycloakApiRails::PublicKeyCachedResolver.new(nil, "a-realm", ttl)
110
+ assert("PublicKeyCachedResolver considers an empty cache as outdated") do
111
+ resolver.send(:public_keys_are_outdated?) == true
112
+ end
113
+ resolver.instance_variable_set(:@cached_public_keys, "a-public-key")
114
+ resolver.instance_variable_set(:@cached_public_key_retrieved_at, Time.now - (ttl - 10))
115
+ assert("PublicKeyCachedResolver keeps a cache that is within its TTL") do
116
+ resolver.send(:public_keys_are_outdated?) == false
117
+ end
118
+ resolver.instance_variable_set(:@cached_public_key_retrieved_at, Time.now - (ttl + 10))
119
+ assert("PublicKeyCachedResolver invalidates a cache that outlived its TTL") do
120
+ resolver.send(:public_keys_are_outdated?) == true
121
+ end
122
+
123
+ assert("ActiveSupport has not been loaded while exercising the library") { !defined?(ActiveSupport) }
124
+
125
+ if $failures.empty?
126
+ puts "The library behaves correctly without ActiveSupport"
127
+ exit 0
128
+ else
129
+ $failures.each { |failure| puts "FAILED: #{failure}" }
130
+ exit 1
131
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keycloak-api-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorent Lempereur
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-12-10 00:00:00.000000000 Z
11
+ date: 2026-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - '='
60
60
  - !ruby/object:Gem::Version
61
- version: 0.9.10
61
+ version: 0.9.11
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - '='
67
67
  - !ruby/object:Gem::Version
68
- version: 0.9.10
68
+ version: 0.9.11
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: byebug
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -109,11 +109,14 @@ files:
109
109
  - lib/keycloak-api-rails/service.rb
110
110
  - lib/keycloak-api-rails/token_error.rb
111
111
  - lib/keycloak-api-rails/version.rb
112
+ - spec/keycloak-api-rails/activesupport_free_spec.rb
112
113
  - spec/keycloak-api-rails/authentication_spec.rb
114
+ - spec/keycloak-api-rails/configuration_spec.rb
113
115
  - spec/keycloak-api-rails/helper_spec.rb
114
116
  - spec/keycloak-api-rails/public_key_cached_resolver_spec.rb
115
117
  - spec/keycloak-api-rails/service_spec.rb
116
118
  - spec/spec_helper.rb
119
+ - spec/support/activesupport_free_probe.rb
117
120
  - spec/support/public_key_cached_resolver_stub.rb
118
121
  - spec/support/public_key_resolver_stub.rb
119
122
  - spec/support/rails_helper.rb