keycloak-api-rails 1.0.1 → 1.1.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: 72a9cf5907ae0256e7acfbf9bd9e453dcbcaeae5fd4c5ce146780e3ef5ec2644
4
- data.tar.gz: ac22d1edfbf8d1701ac82a213f3052ca32976dd766841111066600910497d58d
3
+ metadata.gz: 1f53bfe86f8f72911bac95192b9a21fc0cf1ec01868e00f2cdd8edfcc57811e3
4
+ data.tar.gz: 2ad644c2561e191e9dd858055a9442df69fff0af02d078360164f1d41283f633
5
5
  SHA512:
6
- metadata.gz: e73d57fcaf904c534048ff6d6e611913298b173be1e47b01b31c7c6cb351f2e8d74b00a57027ce03063b63391f2200c9c8d866a18a7cb632856acb77bdb5d494
7
- data.tar.gz: be4136364d974a0ebcba62a31f526341cdef89f0f87f991dde409cb40a36303f0ae4a781599929caf830cf89a58ce01b20e2c7209ef7613c9e0d465b831bbb17
6
+ metadata.gz: da17e6a76b47460200ea397c58346dbdd43975f7aecec34e973b77ad15ebd12739d2c5d533064af24204d5be7bd4cc0f3c74b3df98e6d2623289a0d66f1398ad
7
+ data.tar.gz: 1f1f0ed249c2963fec160fdbdb6910a8387024f54d9956c35729fb5438037b541989046aee9dab2936dcc585d8f5e9ca6cd1b3247c20fb4dcea62ff7e2fae253
@@ -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
@@ -0,0 +1,52 @@
1
+ name: Release
2
+
3
+ # Publishing is triggered by pushing a version tag, e.g.:
4
+ # git tag -a v1.2.0 -m "Version 1.2.0" && git push origin v1.2.0
5
+ #
6
+ # No RubyGems API key is stored in this repository: the gem is pushed through
7
+ # RubyGems' Trusted Publishing, which exchanges a short-lived GitHub OIDC token
8
+ # for a scoped RubyGems credential. This requires a trusted publisher declared on
9
+ # https://rubygems.org/gems/keycloak-api-rails pointing to this repository, this
10
+ # workflow file name and the 'release' environment.
11
+ on:
12
+ push:
13
+ tags: [ "v*" ]
14
+
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ release:
20
+ name: Push gem to RubyGems.org
21
+ # Pinned rather than 'ubuntu-latest', for the same reason as in ci.yml.
22
+ runs-on: ubuntu-24.04
23
+
24
+ environment: release
25
+
26
+ permissions:
27
+ contents: write # 'rake release' pushes the version tag when it does not exist yet
28
+ id-token: write # mandatory to request the OIDC token used by trusted publishing
29
+
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+
33
+ - name: Set up Ruby
34
+ uses: ruby/setup-ruby@v1
35
+ with:
36
+ ruby-version: '3.4'
37
+ bundler-cache: true
38
+
39
+ - name: Check that the tag matches KeycloakApiRails::VERSION
40
+ run: |
41
+ tag="${GITHUB_REF_NAME#v}"
42
+ version="$(ruby -Ilib -r keycloak-api-rails/version -e 'print KeycloakApiRails::VERSION')"
43
+ if [ "$tag" != "$version" ]; then
44
+ echo "::error::Tag '${GITHUB_REF_NAME}' does not match KeycloakApiRails::VERSION '${version}'"
45
+ exit 1
46
+ fi
47
+
48
+ - name: Run tests
49
+ run: bundle exec rspec
50
+
51
+ # Builds the gem, pushes it to RubyGems.org and waits for it to be available.
52
+ - uses: rubygems/release-gem@v1
data/.gitignore CHANGED
@@ -1,4 +1,9 @@
1
1
  .bundle/
2
2
  log/*.log
3
3
  .byebug_history
4
- *.gem
4
+ *.gem
5
+ pkg/
6
+ # This gem supports several Ruby versions, each of them resolving a different set of
7
+ # dependencies (Ruby 2.7 caps Rails to 7.1, Ruby 3.1 to 7.2, Ruby 3.2+ resolves Rails 8.1).
8
+ # A single committed lockfile cannot satisfy all of them.
9
+ Gemfile.lock
data/CHANGELOG.md CHANGED
@@ -5,11 +5,18 @@ 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.1] - 2026-07-30
8
+ ## [1.1.1] - 2026-07-31
9
+
10
+ * New test helpers, in `keycloak-api-rails/testing`: `KeycloakApiRails::Testing.stub_public_keys!` validates the tokens forged by `keycloak_token` and `keycloak_auth_headers`, so that controller and request tests can be authenticated without a Keycloak server, and without the boilerplate the README used to describe. This file is not loaded by `keycloak-api-rails` and has to be required explicitly
11
+ * Publish the gem to RubyGems from Github Actions when a `v*` tag is pushed, using RubyGems' Trusted Publishing
12
+
13
+ ## [1.1.0] - 2026-07-31
9
14
 
10
15
  * Dependencies: Upgrade test dependency `timecop` to `0.9.11`
11
16
  * Upgrade Docker image to Ruby 3.4
12
17
  * Remove all usages of `ActiveSupport` from the library, replaced by plain Ruby
18
+ * 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)
19
+ * Test dependency `byebug` is no longer pinned to an exact version, since byebug 12 requires Ruby 3.1
13
20
 
14
21
  ## [1.0.0] - 2025-12-10
15
22
 
data/Dockerfile CHANGED
@@ -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.1"
21
+ gem "keycloak-api-rails", "1.1.1"
9
22
  ```
10
23
 
11
24
  ## Token validation
@@ -178,36 +191,60 @@ end
178
191
 
179
192
  ## Writing integration tests
180
193
 
181
- If you want to write controller tests in your codebase and that Keycloak is configured for these controllers, here is how to mock it.
194
+ If you want to write controller or request tests in your codebase and that Keycloak is configured for these controllers,
195
+ this gem provides the helpers that forge valid tokens, so that no Keycloak server is required.
196
+ These helpers are not loaded by `keycloak-api-rails`: they have to be required explicitly, so that nothing they define ends up in a production process.
182
197
  These lines are based on tests written using `rspec`.
183
198
 
184
- * First, create a private key. This key should be created once per test suite for performance matters.
185
199
  ```ruby
186
- config.before(:suite) do
187
- $private_key = OpenSSL::PKey::RSA.generate(1024)
200
+ # spec/rails_helper.rb
201
+ require "keycloak-api-rails/testing"
202
+
203
+ RSpec.configure do |config|
204
+ config.include KeycloakApiRails::Testing::Helpers
205
+ config.before(:suite) { KeycloakApiRails::Testing.stub_public_keys! }
188
206
  end
189
207
  ```
190
- * Then, in a `shared_context`, configure a lazy token based on your main user. (here, we assume you have a `user` variable with a `keycloak_id` property)
208
+
209
+ `stub_public_keys!` makes the library validate the tokens forged by these helpers, instead of fetching public keys from a Keycloak server.
210
+ `server_url` and `realm_id` therefore do not have to be configured in the test environment. Requests can then be authenticated with:
211
+
191
212
  ```ruby
192
- let(:jwt) do
193
- claims = {
194
- iat: Time.zone.now.to_i,
195
- exp: (Time.zone.now + 1.day).to_i,
196
- sub: user.keycloak_id,
197
- }
198
- token = JSON::JWT.new(claims)
199
- token.kid = "default"
200
- token.sign($private_key, :RS256).to_s
213
+ it "returns the current user" do
214
+ get "/me", headers: keycloak_auth_headers(sub: user.keycloak_id)
215
+
216
+ expect(response).to have_http_status(:ok)
201
217
  end
202
218
  ```
203
- * Finally, in the same `shared_context`, stub `KeycloakApiRails.public_key_resolver` to use a valid public key that is able to validate `jwt`:
219
+
220
+ `keycloak_auth_headers` returns an `Authorization` header, and `keycloak_token` returns the token alone.
221
+ Both accept the same optional parameters, one for each claim this library reads:
222
+
223
+ | Parameter | Claim | Default |
224
+ | --- | --- | --- |
225
+ | `sub` | `sub` | a random UUID |
226
+ | `email` | `email` | none |
227
+ | `locale` | `locale` | none |
228
+ | `authorized_party` | `azp` | none |
229
+ | `roles` | `realm_access.roles` | none |
230
+ | `resource_roles` | `resource_access` | none |
231
+ | `issued_at` | `iat` | now |
232
+ | `expires_at` | `exp` | one hour after `issued_at` |
233
+ | `claims` | any other claim | none |
234
+
235
+ For instance, to authenticate a request as a user having the realm role `admin`, the role `reader` on the client `a-client`,
236
+ and a `tenant_id` attribute declared in `config.custom_attributes`:
237
+
204
238
  ```ruby
205
- before(:each) do
206
- public_key_resolver = KeycloakApiRails.public_key_resolver
207
- allow(public_key_resolver).to receive(:find_public_keys) { JSON::JWK::Set.new(JSON::JWK.new($private_key, kid: "default")) }
208
- end
239
+ headers = keycloak_auth_headers(sub: user.keycloak_id,
240
+ roles: ["admin"],
241
+ resource_roles: { "a-client" => ["reader"] },
242
+ claims: { "tenant_id" => tenant.id })
209
243
  ```
210
244
 
245
+ An expired token is forged by passing an `expires_at` in the past, which is how a `401` can be tested.
246
+ Assigning `KeycloakApiRails.public_key_resolver = nil` restores the regular resolver.
247
+
211
248
  ## How to execute library tests
212
249
 
213
250
  From the `keycloak-rails-api` directory:
@@ -217,6 +254,25 @@ From the `keycloak-rails-api` directory:
217
254
  $ docker run -v `pwd`:/usr/src/app/ keycloak-rails-api:test bundle exec rspec spec
218
255
  ```
219
256
 
257
+ ## How to release a new version
258
+
259
+ Releases are published to [RubyGems](https://rubygems.org/gems/keycloak-api-rails) by GitHub Actions
260
+ (`.github/workflows/release.yml`), through [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/):
261
+ no API key is stored in this repository, the workflow exchanges a short-lived GitHub OIDC token for a
262
+ scoped RubyGems credential.
263
+
264
+ 1. Update `KeycloakApiRails::VERSION` in `lib/keycloak-api-rails/version.rb` and the `CHANGELOG.md`
265
+ 2. Commit and push these changes to `main`
266
+ 3. Tag the commit and push the tag:
267
+
268
+ ```
269
+ $ git tag -a v1.2.0 -m "Version 1.2.0"
270
+ $ git push origin v1.2.0
271
+ ```
272
+
273
+ The workflow then checks that the tag matches `KeycloakApiRails::VERSION`, runs the tests, builds the gem
274
+ and pushes it. It only publishes tags starting with `v`.
275
+
220
276
  ## Next developments
221
277
 
222
278
  * Manage multiple realms
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -15,10 +15,16 @@ 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
24
  spec.add_development_dependency "timecop", "0.9.11"
23
- spec.add_development_dependency "byebug", "12.0.0"
25
+ # Required by 'rake release', which builds and publishes the gem from the CI.
26
+ spec.add_development_dependency "rake", ">= 13.0"
27
+ # Not pinned to an exact version: byebug 12 requires Ruby >= 3.1, byebug 13 requires Ruby >= 3.2.
28
+ # Older Rubies resolve to byebug 11.
29
+ spec.add_development_dependency "byebug", ">= 11.1.3"
24
30
  end
@@ -0,0 +1,118 @@
1
+ # Test helpers for the applications that authenticate their requests with this library.
2
+ #
3
+ # This file is *not* loaded by "keycloak-api-rails": it has to be required explicitly, so that
4
+ # nothing it defines can end up in a production process.
5
+ #
6
+ # # spec/rails_helper.rb
7
+ # require "keycloak-api-rails/testing"
8
+ #
9
+ # RSpec.configure do |config|
10
+ # config.include KeycloakApiRails::Testing::Helpers
11
+ # config.before(:suite) { KeycloakApiRails::Testing.stub_public_keys! }
12
+ # end
13
+ #
14
+ # Requests can then be authenticated without a running Keycloak, and without configuring
15
+ # 'server_url' or 'realm_id':
16
+ #
17
+ # get "/me", headers: keycloak_auth_headers(sub: user.keycloak_id, roles: ["admin"])
18
+
19
+ require_relative "../keycloak-api-rails"
20
+ require "securerandom"
21
+
22
+ module KeycloakApiRails
23
+ module Testing
24
+
25
+ KEY_SIZE = 2048
26
+ KEY_ID = "keycloak-api-rails-testing"
27
+ ALGORITHM = :RS256
28
+ DEFAULT_VALIDITY_IN_SECONDS = 3600
29
+
30
+ # Returns the public keys forged by this module, instead of fetching them from a Keycloak server.
31
+ class PublicKeyResolver
32
+ def initialize(public_keys)
33
+ @public_keys = public_keys
34
+ end
35
+
36
+ def find_public_keys
37
+ @public_keys
38
+ end
39
+ end
40
+
41
+ class << self
42
+ # The RSA key pair used to sign the tokens forged by this module. It is generated once per
43
+ # process: generating a key is by far the slowest operation of a test suite that uses tokens.
44
+ def private_key
45
+ @private_key ||= OpenSSL::PKey::RSA.generate(KEY_SIZE)
46
+ end
47
+
48
+ def signing_key
49
+ @signing_key ||= JSON::JWK.new(private_key, kid: KEY_ID)
50
+ end
51
+
52
+ def public_keys
53
+ @public_keys ||= JSON::JWK::Set.new(JSON::JWK.new(private_key.public_key, kid: KEY_ID))
54
+ end
55
+
56
+ # Makes the library validate the tokens forged by this module. Assigning
57
+ # 'KeycloakApiRails.public_key_resolver = nil' restores the regular resolver.
58
+ def stub_public_keys!
59
+ KeycloakApiRails.public_key_resolver = PublicKeyResolver.new(public_keys)
60
+ end
61
+
62
+ # Forges a signed token, similar to the ones emitted by Keycloak. Every claim this library
63
+ # reads has a dedicated parameter; any other claim can be passed through 'claims', which is
64
+ # also how the attributes declared in 'config.custom_attributes' are set.
65
+ def token_for(sub: SecureRandom.uuid,
66
+ email: nil,
67
+ locale: nil,
68
+ authorized_party: nil,
69
+ roles: [],
70
+ resource_roles: {},
71
+ issued_at: Time.now,
72
+ expires_at: nil,
73
+ claims: {})
74
+ payload = {
75
+ "sub" => sub,
76
+ "email" => email,
77
+ "locale" => locale,
78
+ "azp" => authorized_party,
79
+ "iat" => issued_at.to_i,
80
+ "exp" => (expires_at || (issued_at.to_i + DEFAULT_VALIDITY_IN_SECONDS)).to_i
81
+ }.reject { |_, value| value.nil? }
82
+
83
+ payload["realm_access"] = { "roles" => roles.map(&:to_s) } unless roles.nil? || roles.empty?
84
+ payload["resource_access"] = build_resource_access(resource_roles) unless resource_roles.nil? || resource_roles.empty?
85
+
86
+ claims.each { |name, value| payload[name.to_s] = value }
87
+
88
+ JSON::JWT.new(payload).sign(signing_key, ALGORITHM).to_s
89
+ end
90
+
91
+ # The 'Authorization' header a client would send, e.g. { "Authorization" => "Bearer eyJ0..." }.
92
+ # It accepts the same parameters as '.token_for'.
93
+ def auth_headers_for(**options)
94
+ { "Authorization" => "Bearer #{token_for(**options)}" }
95
+ end
96
+
97
+ private
98
+
99
+ def build_resource_access(resource_roles)
100
+ resource_roles.inject({}) do |resource_access, (name, roles)|
101
+ resource_access[name.to_s] = { "roles" => Array(roles).map(&:to_s) }
102
+ resource_access
103
+ end
104
+ end
105
+ end
106
+
107
+ # Meant to be included in a test suite, e.g. 'config.include KeycloakApiRails::Testing::Helpers'.
108
+ module Helpers
109
+ def keycloak_token(**options)
110
+ KeycloakApiRails::Testing.token_for(**options)
111
+ end
112
+
113
+ def keycloak_auth_headers(**options)
114
+ KeycloakApiRails::Testing.auth_headers_for(**options)
115
+ end
116
+ end
117
+ end
118
+ end
@@ -1,3 +1,3 @@
1
1
  module KeycloakApiRails
2
- VERSION = "1.0.1"
2
+ VERSION = "1.1.1"
3
3
  end
@@ -33,6 +33,14 @@ module KeycloakApiRails
33
33
  @public_key_resolver ||= PublicKeyCachedResolver.from_configuration(http_client, config)
34
34
  end
35
35
 
36
+ # Mainly used by "keycloak-api-rails/testing" to validate tokens without a Keycloak server.
37
+ # Assigning nil restores the regular resolver. The memoized service is discarded, since it holds
38
+ # a reference to the resolver that is being replaced.
39
+ def self.public_key_resolver=(resolver)
40
+ @public_key_resolver = resolver
41
+ @service = nil
42
+ end
43
+
36
44
  def self.service
37
45
  @service ||= KeycloakApiRails::Service.new(public_key_resolver)
38
46
  end
@@ -205,6 +205,12 @@ RSpec.describe KeycloakApiRails::Service do
205
205
  @result = service2.need_middleware_authentication?(method, path, headers)
206
206
  end
207
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
+
208
214
  it "should return false" do
209
215
  expect(@result).to be false
210
216
  end
@@ -0,0 +1,102 @@
1
+ require_relative "../../lib/keycloak-api-rails/testing"
2
+
3
+ RSpec.describe KeycloakApiRails::Testing do
4
+ include KeycloakApiRails::Testing::Helpers
5
+
6
+ # The tokens are validated by the real middleware and the real service, so that these examples
7
+ # fail if what this module forges stops being what the library expects.
8
+ let(:authenticated_env) { {} }
9
+ let(:app) do
10
+ downstream = lambda do |env|
11
+ authenticated_env.replace(env)
12
+ [200, {}, ["OK"]]
13
+ end
14
+ KeycloakApiRails::Middleware.new(downstream)
15
+ end
16
+
17
+ def get(headers)
18
+ app.call({
19
+ "REQUEST_METHOD" => "GET",
20
+ "PATH_INFO" => "/me",
21
+ "REQUEST_URI" => "http://example.org/me",
22
+ "HTTP_AUTHORIZATION" => headers["Authorization"]
23
+ })
24
+ end
25
+
26
+ before(:each) do
27
+ # The middleware only authenticates a request when the configuration tells it to, and the
28
+ # configuration is global: these examples cannot rely on what the other ones leave behind.
29
+ KeycloakApiRails.config.opt_in = false
30
+ KeycloakApiRails.config.skip_paths = {}
31
+ KeycloakApiRails::Testing.stub_public_keys!
32
+ end
33
+
34
+ after(:each) do
35
+ KeycloakApiRails.public_key_resolver = nil
36
+ end
37
+
38
+ describe ".stub_public_keys!" do
39
+ it "should validate the tokens forged by this module" do
40
+ status, _headers, _body = get(keycloak_auth_headers)
41
+
42
+ expect(status).to eq(200)
43
+ end
44
+
45
+ it "should not require a Keycloak server to be configured" do
46
+ expect(KeycloakApiRails.config.server_url).to be_nil
47
+ expect(KeycloakApiRails.config.realm_id).to be_nil
48
+
49
+ status, _headers, _body = get(keycloak_auth_headers)
50
+
51
+ expect(status).to eq(200)
52
+ end
53
+ end
54
+
55
+ describe ".token_for" do
56
+ it "should authenticate the given user" do
57
+ get(keycloak_auth_headers(sub: "a-keycloak-id"))
58
+
59
+ expect(KeycloakApiRails::Helper.current_user_id(authenticated_env)).to eq("a-keycloak-id")
60
+ end
61
+
62
+ it "should generate a different user for each token" do
63
+ first_token = JSON::JWT.decode(keycloak_token, KeycloakApiRails::Testing.public_keys)
64
+ second_token = JSON::JWT.decode(keycloak_token, KeycloakApiRails::Testing.public_keys)
65
+
66
+ expect(first_token["sub"]).not_to eq(second_token["sub"])
67
+ end
68
+
69
+ it "should expose every claim read by the library" do
70
+ get(keycloak_auth_headers(sub: "a-keycloak-id",
71
+ email: "an-email@keycloak.io",
72
+ locale: "fr",
73
+ authorized_party: "a-client",
74
+ roles: ["admin", "user"],
75
+ resource_roles: { "a-client" => ["reader"] }))
76
+
77
+ expect(KeycloakApiRails::Helper.current_user_id(authenticated_env)).to eq("a-keycloak-id")
78
+ expect(KeycloakApiRails::Helper.current_user_email(authenticated_env)).to eq("an-email@keycloak.io")
79
+ expect(KeycloakApiRails::Helper.current_user_locale(authenticated_env)).to eq("fr")
80
+ expect(KeycloakApiRails::Helper.current_authorized_party(authenticated_env)).to eq("a-client")
81
+ expect(KeycloakApiRails::Helper.current_user_roles(authenticated_env)).to eq(["admin", "user"])
82
+ expect(KeycloakApiRails::Helper.current_resource_roles(authenticated_env)).to eq({ "a-client" => ["reader"] })
83
+ end
84
+
85
+ it "should expose the claims declared as custom attributes" do
86
+ KeycloakApiRails.config.custom_attributes = ["tenant_id"]
87
+
88
+ get(keycloak_auth_headers(claims: { "tenant_id" => "a-tenant" }))
89
+
90
+ expect(KeycloakApiRails::Helper.current_user_custom_attributes(authenticated_env)).to eq({ "tenant_id" => "a-tenant" })
91
+ ensure
92
+ KeycloakApiRails.config.custom_attributes = []
93
+ end
94
+
95
+ it "should forge a token that is expired when 'expires_at' is in the past" do
96
+ status, _headers, body = get(keycloak_auth_headers(issued_at: Time.now - 7200, expires_at: Time.now - 3600))
97
+
98
+ expect(status).to eq(401)
99
+ expect(body.first).to include("JWT token is expired")
100
+ end
101
+ end
102
+ 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.1
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorent Lempereur
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-07-30 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
@@ -66,20 +65,34 @@ dependencies:
66
65
  - - '='
67
66
  - !ruby/object:Gem::Version
68
67
  version: 0.9.11
68
+ - !ruby/object:Gem::Dependency
69
+ name: rake
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '13.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '13.0'
69
82
  - !ruby/object:Gem::Dependency
70
83
  name: byebug
71
84
  requirement: !ruby/object:Gem::Requirement
72
85
  requirements:
73
- - - '='
86
+ - - ">="
74
87
  - !ruby/object:Gem::Version
75
- version: 12.0.0
88
+ version: 11.1.3
76
89
  type: :development
77
90
  prerelease: false
78
91
  version_requirements: !ruby/object:Gem::Requirement
79
92
  requirements:
80
- - - '='
93
+ - - ">="
81
94
  - !ruby/object:Gem::Version
82
- version: 12.0.0
95
+ version: 11.1.3
83
96
  description: Rails middleware that validates Authorization token emitted by Keycloak
84
97
  email:
85
98
  - lorent.lempereur.dev@gmail.com
@@ -88,14 +101,15 @@ extensions: []
88
101
  extra_rdoc_files: []
89
102
  files:
90
103
  - ".github/workflows/ci.yml"
104
+ - ".github/workflows/release.yml"
91
105
  - ".gitignore"
92
106
  - ".rspec"
93
107
  - CHANGELOG.md
94
108
  - Dockerfile
95
109
  - Gemfile
96
- - Gemfile.lock
97
110
  - MIT-LICENSE
98
111
  - README.md
112
+ - Rakefile
99
113
  - keycloak-api-rails.gemspec
100
114
  - lib/keycloak-api-rails.rb
101
115
  - lib/keycloak-api-rails/authentication.rb
@@ -107,6 +121,7 @@ files:
107
121
  - lib/keycloak-api-rails/public_key_resolver.rb
108
122
  - lib/keycloak-api-rails/railtie.rb
109
123
  - lib/keycloak-api-rails/service.rb
124
+ - lib/keycloak-api-rails/testing.rb
110
125
  - lib/keycloak-api-rails/token_error.rb
111
126
  - lib/keycloak-api-rails/version.rb
112
127
  - spec/keycloak-api-rails/activesupport_free_spec.rb
@@ -115,6 +130,7 @@ files:
115
130
  - spec/keycloak-api-rails/helper_spec.rb
116
131
  - spec/keycloak-api-rails/public_key_cached_resolver_spec.rb
117
132
  - spec/keycloak-api-rails/service_spec.rb
133
+ - spec/keycloak-api-rails/testing_spec.rb
118
134
  - spec/spec_helper.rb
119
135
  - spec/support/activesupport_free_probe.rb
120
136
  - spec/support/public_key_cached_resolver_stub.rb
@@ -124,7 +140,6 @@ homepage: https://github.com/looorent/keycloak-api-rails
124
140
  licenses:
125
141
  - MIT
126
142
  metadata: {}
127
- post_install_message:
128
143
  rdoc_options: []
129
144
  require_paths:
130
145
  - lib
@@ -132,15 +147,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
132
147
  requirements:
133
148
  - - ">="
134
149
  - !ruby/object:Gem::Version
135
- version: '0'
150
+ version: '2.7'
136
151
  required_rubygems_version: !ruby/object:Gem::Requirement
137
152
  requirements:
138
153
  - - ">="
139
154
  - !ruby/object:Gem::Version
140
155
  version: '0'
141
156
  requirements: []
142
- rubygems_version: 3.0.3.1
143
- signing_key:
157
+ rubygems_version: 3.6.9
144
158
  specification_version: 4
145
159
  summary: Rails middleware that validates Authorization token emitted by Keycloak
146
160
  test_files: []
data/Gemfile.lock DELETED
@@ -1,249 +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.19)
12
- railties
13
- actioncable (8.1.3.1)
14
- actionpack (= 8.1.3.1)
15
- activesupport (= 8.1.3.1)
16
- nio4r (~> 2.0)
17
- websocket-driver (>= 0.6.1)
18
- zeitwerk (~> 2.6)
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
- mail (>= 2.8.0)
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
- mail (>= 2.8.0)
32
- rails-dom-testing (~> 2.2)
33
- actionpack (8.1.3.1)
34
- actionview (= 8.1.3.1)
35
- activesupport (= 8.1.3.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.3.1)
44
- action_text-trix (~> 2.1.15)
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
- globalid (>= 0.6.0)
50
- nokogiri (>= 1.8.5)
51
- actionview (8.1.3.1)
52
- activesupport (= 8.1.3.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.3.1)
58
- activesupport (= 8.1.3.1)
59
- globalid (>= 0.3.6)
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
- timeout (>= 0.4.0)
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
- marcel (~> 1.0)
72
- activesupport (8.1.3.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 (4.1.2)
88
- bindata (3.0.0)
89
- builder (3.3.0)
90
- byebug (12.0.0)
91
- concurrent-ruby (1.3.8)
92
- connection_pool (3.0.2)
93
- crass (1.0.7)
94
- date (3.5.1)
95
- diff-lcs (1.6.2)
96
- drb (2.2.3)
97
- erb (6.0.6)
98
- erubi (1.13.1)
99
- faraday (2.14.3)
100
- faraday-net_http (>= 2.0, < 3.5)
101
- json
102
- logger
103
- faraday-follow_redirects (0.5.0)
104
- faraday (>= 1, < 3)
105
- faraday-net_http (3.4.4)
106
- net-http (~> 0.5)
107
- globalid (1.4.0)
108
- activesupport (>= 6.1)
109
- i18n (1.15.2)
110
- concurrent-ruby (~> 1.0)
111
- io-console (0.8.2)
112
- irb (1.18.0)
113
- pp (>= 0.6.0)
114
- prism (>= 1.3.0)
115
- rdoc (>= 4.0.0)
116
- reline (>= 0.4.2)
117
- json (2.21.1)
118
- json-jwt (1.17.1)
119
- activesupport (>= 4.2)
120
- aes_key_wrap
121
- base64
122
- bindata
123
- faraday (~> 2.0)
124
- faraday-follow_redirects
125
- logger (1.7.0)
126
- loofah (2.25.2)
127
- crass (~> 1.0.2)
128
- nokogiri (>= 1.12.0)
129
- mail (2.9.1)
130
- logger
131
- mini_mime (>= 0.1.1)
132
- net-imap
133
- net-pop
134
- net-smtp
135
- marcel (1.2.1)
136
- mini_mime (1.1.5)
137
- mini_portile2 (2.8.9)
138
- minitest (6.0.6)
139
- drb (~> 2.0)
140
- prism (~> 1.5)
141
- net-http (0.9.1)
142
- uri (>= 0.11.1)
143
- net-imap (0.6.6)
144
- date
145
- net-protocol
146
- net-pop (0.1.2)
147
- net-protocol
148
- net-protocol (0.2.2)
149
- timeout
150
- net-smtp (0.5.1)
151
- net-protocol
152
- nio4r (2.7.5)
153
- nokogiri (1.19.4)
154
- mini_portile2 (~> 2.8.2)
155
- racc (~> 1.4)
156
- pp (0.6.4)
157
- prettyprint
158
- prettyprint (0.2.0)
159
- prism (1.9.0)
160
- racc (1.8.1)
161
- rack (3.2.6)
162
- rack-session (2.1.2)
163
- base64 (>= 0.1.0)
164
- rack (>= 3.0.0)
165
- rack-test (2.2.0)
166
- rack (>= 1.3)
167
- rackup (2.3.1)
168
- rack (>= 3)
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)
181
- bundler (>= 1.15.0)
182
- railties (= 8.1.3.1)
183
- rails-dom-testing (2.3.0)
184
- activesupport (>= 5.0.0)
185
- minitest
186
- nokogiri (>= 1.6)
187
- rails-html-sanitizer (1.7.1)
188
- loofah (~> 2.25, >= 2.25.2)
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)
190
- railties (8.1.3.1)
191
- actionpack (= 8.1.3.1)
192
- activesupport (= 8.1.3.1)
193
- irb (~> 1.13)
194
- rackup (>= 1.0.0)
195
- rake (>= 12.2)
196
- thor (~> 1.0, >= 1.2.2)
197
- tsort (>= 0.2)
198
- zeitwerk (~> 2.6)
199
- rake (13.4.2)
200
- rbs (4.1.1)
201
- logger
202
- prism (>= 1.6.0)
203
- tsort
204
- rdoc (8.0.0)
205
- erb
206
- prism (>= 1.6.0)
207
- rbs (>= 4.0.0)
208
- tsort
209
- reline (0.6.3)
210
- io-console (~> 0.5)
211
- rspec (3.13.2)
212
- rspec-core (~> 3.13.0)
213
- rspec-expectations (~> 3.13.0)
214
- rspec-mocks (~> 3.13.0)
215
- rspec-core (3.13.6)
216
- rspec-support (~> 3.13.0)
217
- rspec-expectations (3.13.5)
218
- diff-lcs (>= 1.2.0, < 2.0)
219
- rspec-support (~> 3.13.0)
220
- rspec-mocks (3.13.8)
221
- diff-lcs (>= 1.2.0, < 2.0)
222
- rspec-support (~> 3.13.0)
223
- rspec-support (3.13.7)
224
- securerandom (0.4.1)
225
- thor (1.5.0)
226
- timecop (0.9.11)
227
- timeout (0.6.1)
228
- tsort (0.2.0)
229
- tzinfo (2.0.6)
230
- concurrent-ruby (~> 1.0)
231
- uri (1.1.1)
232
- useragent (0.16.11)
233
- websocket-driver (0.8.2)
234
- base64
235
- websocket-extensions (>= 0.1.0)
236
- websocket-extensions (0.1.5)
237
- zeitwerk (2.8.2)
238
-
239
- PLATFORMS
240
- ruby
241
-
242
- DEPENDENCIES
243
- byebug (= 12.0.0)
244
- keycloak-api-rails!
245
- rspec (= 3.13.2)
246
- timecop (= 0.9.11)
247
-
248
- BUNDLED WITH
249
- 2.4.13