keycloak-api-rails 1.0.0 → 1.1.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: c8848fb5696ab7e2962d8491b2df8e17c1bc88a3f08bb4b328b96e872b5858de
4
- data.tar.gz: 9b2f8541d0a2e26f59cec67fcd6573e34c898db12a769335faa701a9c15b66be
3
+ metadata.gz: cb67f66191b532371ed77f5677c4959a3d01cd8d4fa270dd984a119d796077b0
4
+ data.tar.gz: 64f29e21b50fb2d6f46d2a136c299aa9864c78aea977c9e4f7dc6a4927a28dff
5
5
  SHA512:
6
- metadata.gz: e7f9ca820ba2eb32fca2bcbf72c9a19e70ed2bc62e95f6f059fcd848f7aafbb40847017a012a93ff7b5f55fe029b4e7cd353f2376eb90c7a5c0b11f4fac4e310
7
- data.tar.gz: 57a3a77dec4b4daeb697ee3b3d8f4ed4ad349c2b68e60ca7001e46be2809a358c1e24e6dd1a3e2d31270be9e0e8daaac0176511e7009ce9cca4ea591e8040ba7
6
+ metadata.gz: 2db5967b4c4b7e678763d015dd05567103cafb42bce12f0018c241df45400f8c4538309a59dd7526994bc14cf587aef3b7c51040d89ca2599c91c409b6acf8a5
7
+ data.tar.gz: 847fc92280c17a54d7f10976197b4ea0b37fea4217ee7ef9dd6d7089399b261831a07d3d56d07ffce2827aaf86c4332da680b3eaa044a3c35d0cb2347f123043
@@ -11,11 +11,18 @@ permissions:
11
11
 
12
12
  jobs:
13
13
  test:
14
- runs-on: ubuntu-latest
14
+ name: Ruby ${{ matrix.ruby-version }}
15
+ # Pinned rather than 'ubuntu-latest': ruby/setup-ruby has no prebuilt Rubies for
16
+ # ubuntu-26.04 yet, so the oldest versions of the matrix would break once the
17
+ # 'latest' alias moves on.
18
+ runs-on: ubuntu-24.04
15
19
 
16
20
  strategy:
21
+ fail-fast: false
17
22
  matrix:
18
- ruby-version: ['3.2']
23
+ # Each version resolves the newest dependencies it supports: Rails 7.1 on Ruby 2.7
24
+ # and 3.0, Rails 7.2 on Ruby 3.1, Rails 8.1 on Ruby 3.2 and above.
25
+ ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0']
19
26
 
20
27
  steps:
21
28
  - uses: actions/checkout@v4
data/.gitignore CHANGED
@@ -1,4 +1,8 @@
1
1
  .bundle/
2
2
  log/*.log
3
3
  .byebug_history
4
- *.gem
4
+ *.gem
5
+ # This gem supports several Ruby versions, each of them resolving a different set of
6
+ # dependencies (Ruby 2.7 caps Rails to 7.1, Ruby 3.1 to 7.2, Ruby 3.2+ resolves Rails 8.1).
7
+ # A single committed lockfile cannot satisfy all of them.
8
+ Gemfile.lock
data/CHANGELOG.md CHANGED
@@ -5,7 +5,15 @@ 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.1.0] - 2026-07-31
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
+ * Support Ruby 2.7 up to 4.0: declare `required_ruby_version >= 2.7` and stop committing `Gemfile.lock`, so that each Ruby version resolves the dependencies it supports (Rails 7.1 on Ruby 2.7 and 3.0, Rails 7.2 on Ruby 3.1, Rails 8.1 on Ruby 3.2 and above)
14
+ * Test dependency `byebug` is no longer pinned to an exact version, since byebug 12 requires Ruby 3.1
15
+
16
+ ## [1.0.0] - 2025-12-10
9
17
 
10
18
  * *[BREAKING CHANGE!]* Rename top-level module from `Keycloak` to `KeycloakApiRails` to avoid conflicts with other gems. (thanks to @eilers)
11
19
  * *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
@@ -6,7 +6,6 @@ RUN apt-get update -qq && apt-get install -y build-essential git ruby-dev && apt
6
6
  WORKDIR /usr/src/app
7
7
 
8
8
  COPY Gemfile /usr/src/app/
9
- COPY Gemfile.lock /usr/src/app/
10
9
  COPY keycloak-api-rails.gemspec /usr/src/app/
11
10
  COPY lib/keycloak-api-rails/version.rb /usr/src/app/lib/keycloak-api-rails/
12
11
  RUN bundle install
data/README.md CHANGED
@@ -2,10 +2,23 @@
2
2
 
3
3
  This gem validates Keycloak JWT token for Ruby On Rails APIs.
4
4
 
5
+ ## Requirements
6
+
7
+ * Ruby `>= 2.7`
8
+ * Rails `>= 4.2`
9
+
10
+ Every push is tested against Ruby 2.7, 3.0, 3.1, 3.2, 3.3, 3.4 and 4.0. Each of them installs the
11
+ most recent dependencies it supports, so the test suite runs against Rails 7.1 (Ruby 2.7 and 3.0),
12
+ Rails 7.2 (Ruby 3.1) and Rails 8.1 (Ruby 3.2 and above).
13
+
14
+ Ruby 2.7 and 3.0 reached their end of life and no longer receive security fixes. They are supported
15
+ here so that applications running on Rails 7.0 and 7.1 can use this gem, but upgrading Ruby remains
16
+ the recommended path.
17
+
5
18
  ## Install
6
19
 
7
20
  ```ruby
8
- gem "keycloak-api-rails", "1.0.0"
21
+ gem "keycloak-api-rails", "1.1.0"
9
22
  ```
10
23
 
11
24
  ## Token validation
@@ -63,7 +76,7 @@ All options have a default value. However, all of them can be changed in your in
63
76
  Create a `keycloak.rb` file in your Rails `config/initializers` folder. For instance:
64
77
 
65
78
  ```
66
- Keycloak.configure do |config|
79
+ KeycloakApiRails.configure do |config|
67
80
  config.server_url = ENV["KEYCLOAK_SERVER_URL"]
68
81
  config.realm_id = ENV["KEYCLOAK_REALM_ID"]
69
82
  config.logger = Rails.logger
@@ -77,7 +90,7 @@ end
77
90
  Or using opt-in configuration:
78
91
 
79
92
  ```ruby
80
- Keycloak.configure do |config|
93
+ KeycloakApiRails.configure do |config|
81
94
  config.server_url = ENV["KEYCLOAK_SERVER_URL"]
82
95
  config.realm_id = ENV["KEYCLOAK_REALM_ID"]
83
96
  config.logger = Rails.logger
@@ -163,14 +176,14 @@ This should output `https://api.pouet.io/api/more-pouets?authorizationToken=myTo
163
176
 
164
177
  ### Accessing Keycloak Service
165
178
 
166
- A lazy-loaded service KeycloakApiRails::Service can be accessed using `Keycloak.service`.
179
+ A lazy-loaded service KeycloakApiRails::Service can be accessed using `KeycloakApiRails.service`.
167
180
  For instance, to read a provided token:
168
181
  ```ruby
169
182
  class RenderTokenController < ApplicationController
170
183
  def show
171
184
  uri = request.env["REQUEST_URI"]
172
185
  headers = request.env
173
- token = Keycloak.service.read_token(uri, headers)
186
+ token = KeycloakApiRails.service.read_token(uri, headers)
174
187
  render json: { token: token }, status: :ok
175
188
  end
176
189
  end
@@ -200,10 +213,10 @@ let(:jwt) do
200
213
  token.sign($private_key, :RS256).to_s
201
214
  end
202
215
  ```
203
- * Finally, in the same `shared_context`, stub `Keycloak.public_key_resolver` to use a valid public key that is able to validate `jwt`:
216
+ * Finally, in the same `shared_context`, stub `KeycloakApiRails.public_key_resolver` to use a valid public key that is able to validate `jwt`:
204
217
  ```ruby
205
218
  before(:each) do
206
- public_key_resolver = Keycloak.public_key_resolver
219
+ public_key_resolver = KeycloakApiRails.public_key_resolver
207
220
  allow(public_key_resolver).to receive(:find_public_keys) { JSON::JWK::Set.new(JSON::JWK.new($private_key, kid: "default")) }
208
221
  end
209
222
  ```
@@ -216,7 +229,7 @@ From the `keycloak-rails-api` directory:
216
229
  $ docker build . -t keycloak-rails-api:test
217
230
  $ docker run -v `pwd`:/usr/src/app/ keycloak-rails-api:test bundle exec rspec spec
218
231
  ```
219
- rspe
232
+
220
233
  ## Next developments
221
234
 
222
235
  * Manage multiple realms
@@ -15,10 +15,14 @@ Gem::Specification.new do |spec|
15
15
  spec.files = `git ls-files -z`.split("\x0")
16
16
  spec.require_paths = ["lib"]
17
17
 
18
+ spec.required_ruby_version = ">= 2.7"
19
+
18
20
  spec.add_dependency "rails", ">= 4.2"
19
21
  spec.add_dependency "json-jwt", ">= 1.11.0"
20
22
 
21
23
  spec.add_development_dependency "rspec", "3.13.2"
22
- spec.add_development_dependency "timecop", "0.9.10"
23
- spec.add_development_dependency "byebug", "12.0.0"
24
+ spec.add_development_dependency "timecop", "0.9.11"
25
+ # Not pinned to an exact version: byebug 12 requires Ruby >= 3.1, byebug 13 requires Ruby >= 3.2.
26
+ # Older Rubies resolve to byebug 11.
27
+ spec.add_development_dependency "byebug", ">= 11.1.3"
24
28
  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.1.0"
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,13 @@
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.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorent Lempereur
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-12-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -58,28 +57,28 @@ dependencies:
58
57
  requirements:
59
58
  - - '='
60
59
  - !ruby/object:Gem::Version
61
- version: 0.9.10
60
+ version: 0.9.11
62
61
  type: :development
63
62
  prerelease: false
64
63
  version_requirements: !ruby/object:Gem::Requirement
65
64
  requirements:
66
65
  - - '='
67
66
  - !ruby/object:Gem::Version
68
- version: 0.9.10
67
+ version: 0.9.11
69
68
  - !ruby/object:Gem::Dependency
70
69
  name: byebug
71
70
  requirement: !ruby/object:Gem::Requirement
72
71
  requirements:
73
- - - '='
72
+ - - ">="
74
73
  - !ruby/object:Gem::Version
75
- version: 12.0.0
74
+ version: 11.1.3
76
75
  type: :development
77
76
  prerelease: false
78
77
  version_requirements: !ruby/object:Gem::Requirement
79
78
  requirements:
80
- - - '='
79
+ - - ">="
81
80
  - !ruby/object:Gem::Version
82
- version: 12.0.0
81
+ version: 11.1.3
83
82
  description: Rails middleware that validates Authorization token emitted by Keycloak
84
83
  email:
85
84
  - lorent.lempereur.dev@gmail.com
@@ -93,7 +92,6 @@ files:
93
92
  - CHANGELOG.md
94
93
  - Dockerfile
95
94
  - Gemfile
96
- - Gemfile.lock
97
95
  - MIT-LICENSE
98
96
  - README.md
99
97
  - keycloak-api-rails.gemspec
@@ -109,11 +107,14 @@ files:
109
107
  - lib/keycloak-api-rails/service.rb
110
108
  - lib/keycloak-api-rails/token_error.rb
111
109
  - lib/keycloak-api-rails/version.rb
110
+ - spec/keycloak-api-rails/activesupport_free_spec.rb
112
111
  - spec/keycloak-api-rails/authentication_spec.rb
112
+ - spec/keycloak-api-rails/configuration_spec.rb
113
113
  - spec/keycloak-api-rails/helper_spec.rb
114
114
  - spec/keycloak-api-rails/public_key_cached_resolver_spec.rb
115
115
  - spec/keycloak-api-rails/service_spec.rb
116
116
  - spec/spec_helper.rb
117
+ - spec/support/activesupport_free_probe.rb
117
118
  - spec/support/public_key_cached_resolver_stub.rb
118
119
  - spec/support/public_key_resolver_stub.rb
119
120
  - spec/support/rails_helper.rb
@@ -121,7 +122,6 @@ homepage: https://github.com/looorent/keycloak-api-rails
121
122
  licenses:
122
123
  - MIT
123
124
  metadata: {}
124
- post_install_message:
125
125
  rdoc_options: []
126
126
  require_paths:
127
127
  - lib
@@ -129,15 +129,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
129
129
  requirements:
130
130
  - - ">="
131
131
  - !ruby/object:Gem::Version
132
- version: '0'
132
+ version: '2.7'
133
133
  required_rubygems_version: !ruby/object:Gem::Requirement
134
134
  requirements:
135
135
  - - ">="
136
136
  - !ruby/object:Gem::Version
137
137
  version: '0'
138
138
  requirements: []
139
- rubygems_version: 3.0.3.1
140
- signing_key:
139
+ rubygems_version: 4.0.17
141
140
  specification_version: 4
142
141
  summary: Rails middleware that validates Authorization token emitted by Keycloak
143
142
  test_files: []
data/Gemfile.lock DELETED
@@ -1,244 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- keycloak-api-rails (1.0.0)
5
- json-jwt (>= 1.11.0)
6
- rails (>= 4.2)
7
-
8
- GEM
9
- remote: https://rubygems.org/
10
- specs:
11
- action_text-trix (2.1.15)
12
- railties
13
- actioncable (8.1.1)
14
- actionpack (= 8.1.1)
15
- activesupport (= 8.1.1)
16
- nio4r (~> 2.0)
17
- websocket-driver (>= 0.6.1)
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)
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)
31
- mail (>= 2.8.0)
32
- rails-dom-testing (~> 2.2)
33
- actionpack (8.1.1)
34
- actionview (= 8.1.1)
35
- activesupport (= 8.1.1)
36
- nokogiri (>= 1.8.5)
37
- rack (>= 2.2.4)
38
- rack-session (>= 1.0.1)
39
- rack-test (>= 0.6.3)
40
- rails-dom-testing (~> 2.2)
41
- rails-html-sanitizer (~> 1.6)
42
- useragent (~> 0.16)
43
- actiontext (8.1.1)
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)
49
- globalid (>= 0.6.0)
50
- nokogiri (>= 1.8.5)
51
- actionview (8.1.1)
52
- activesupport (= 8.1.1)
53
- builder (~> 3.1)
54
- erubi (~> 1.11)
55
- rails-dom-testing (~> 2.2)
56
- rails-html-sanitizer (~> 1.6)
57
- activejob (8.1.1)
58
- activesupport (= 8.1.1)
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)
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)
71
- marcel (~> 1.0)
72
- activesupport (8.1.1)
73
- base64
74
- bigdecimal
75
- concurrent-ruby (~> 1.0, >= 1.3.1)
76
- connection_pool (>= 2.2.5)
77
- drb
78
- i18n (>= 1.6, < 2)
79
- json
80
- logger (>= 1.4.2)
81
- minitest (>= 5.1)
82
- securerandom (>= 0.3)
83
- tzinfo (~> 2.0, >= 2.0.5)
84
- uri (>= 0.13.1)
85
- aes_key_wrap (1.1.0)
86
- base64 (0.3.0)
87
- bigdecimal (3.3.1)
88
- bindata (2.5.1)
89
- builder (3.3.0)
90
- byebug (12.0.0)
91
- concurrent-ruby (1.3.5)
92
- connection_pool (3.0.2)
93
- crass (1.0.6)
94
- date (3.5.1)
95
- diff-lcs (1.6.2)
96
- drb (2.2.3)
97
- erb (6.0.0)
98
- erubi (1.13.1)
99
- faraday (2.14.0)
100
- faraday-net_http (>= 2.0, < 3.5)
101
- json
102
- logger
103
- faraday-follow_redirects (0.4.0)
104
- faraday (>= 1, < 3)
105
- faraday-net_http (3.4.2)
106
- net-http (~> 0.5)
107
- globalid (1.3.0)
108
- activesupport (>= 6.1)
109
- i18n (1.14.7)
110
- concurrent-ruby (~> 1.0)
111
- io-console (0.8.1)
112
- irb (1.15.3)
113
- pp (>= 0.6.0)
114
- rdoc (>= 4.0.0)
115
- reline (>= 0.4.2)
116
- json (2.17.1)
117
- json-jwt (1.17.0)
118
- activesupport (>= 4.2)
119
- aes_key_wrap
120
- base64
121
- bindata
122
- faraday (~> 2.0)
123
- faraday-follow_redirects
124
- logger (1.7.0)
125
- loofah (2.24.1)
126
- crass (~> 1.0.2)
127
- nokogiri (>= 1.12.0)
128
- mail (2.9.0)
129
- logger
130
- mini_mime (>= 0.1.1)
131
- net-imap
132
- net-pop
133
- net-smtp
134
- marcel (1.1.0)
135
- mini_mime (1.1.5)
136
- mini_portile2 (2.8.9)
137
- minitest (5.26.2)
138
- net-http (0.8.0)
139
- uri (>= 0.11.1)
140
- net-imap (0.5.12)
141
- date
142
- net-protocol
143
- net-pop (0.1.2)
144
- net-protocol
145
- net-protocol (0.2.2)
146
- timeout
147
- net-smtp (0.5.1)
148
- net-protocol
149
- nio4r (2.7.5)
150
- nokogiri (1.18.10)
151
- mini_portile2 (~> 2.8.2)
152
- racc (~> 1.4)
153
- pp (0.6.3)
154
- prettyprint
155
- prettyprint (0.2.0)
156
- psych (5.3.0)
157
- date
158
- stringio
159
- racc (1.8.1)
160
- rack (3.2.4)
161
- rack-session (2.1.1)
162
- base64 (>= 0.1.0)
163
- rack (>= 3.0.0)
164
- rack-test (2.2.0)
165
- rack (>= 1.3)
166
- rackup (2.3.1)
167
- 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)
180
- bundler (>= 1.15.0)
181
- railties (= 8.1.1)
182
- rails-dom-testing (2.3.0)
183
- activesupport (>= 5.0.0)
184
- minitest
185
- nokogiri (>= 1.6)
186
- rails-html-sanitizer (1.6.2)
187
- loofah (~> 2.21)
188
- 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)
192
- irb (~> 1.13)
193
- rackup (>= 1.0.0)
194
- rake (>= 12.2)
195
- thor (~> 1.0, >= 1.2.2)
196
- tsort (>= 0.2)
197
- zeitwerk (~> 2.6)
198
- rake (13.3.1)
199
- rdoc (6.17.0)
200
- erb
201
- psych (>= 4.0.0)
202
- tsort
203
- reline (0.6.3)
204
- io-console (~> 0.5)
205
- rspec (3.13.2)
206
- rspec-core (~> 3.13.0)
207
- rspec-expectations (~> 3.13.0)
208
- rspec-mocks (~> 3.13.0)
209
- rspec-core (3.13.6)
210
- rspec-support (~> 3.13.0)
211
- rspec-expectations (3.13.5)
212
- diff-lcs (>= 1.2.0, < 2.0)
213
- rspec-support (~> 3.13.0)
214
- rspec-mocks (3.13.7)
215
- diff-lcs (>= 1.2.0, < 2.0)
216
- rspec-support (~> 3.13.0)
217
- rspec-support (3.13.6)
218
- securerandom (0.4.1)
219
- stringio (3.1.9)
220
- thor (1.4.0)
221
- timecop (0.9.10)
222
- timeout (0.5.0)
223
- tsort (0.2.0)
224
- tzinfo (2.0.6)
225
- concurrent-ruby (~> 1.0)
226
- uri (1.1.1)
227
- useragent (0.16.11)
228
- websocket-driver (0.8.0)
229
- base64
230
- websocket-extensions (>= 0.1.0)
231
- websocket-extensions (0.1.5)
232
- zeitwerk (2.7.3)
233
-
234
- PLATFORMS
235
- ruby
236
-
237
- DEPENDENCIES
238
- byebug (= 12.0.0)
239
- keycloak-api-rails!
240
- rspec (= 3.13.2)
241
- timecop (= 0.9.10)
242
-
243
- BUNDLED WITH
244
- 2.4.13