keycloak-api-rails 1.1.1 → 1.1.2

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: 1f53bfe86f8f72911bac95192b9a21fc0cf1ec01868e00f2cdd8edfcc57811e3
4
- data.tar.gz: 2ad644c2561e191e9dd858055a9442df69fff0af02d078360164f1d41283f633
3
+ metadata.gz: 74dec51fa027308c2cf5b57ca67b7316b21677bf1592022f76988f232bbde911
4
+ data.tar.gz: 4836a7951bbf1559ff5eec0dfeb004ff8bcd2955cee7e06ee9e8a553743ad99e
5
5
  SHA512:
6
- metadata.gz: da17e6a76b47460200ea397c58346dbdd43975f7aecec34e973b77ad15ebd12739d2c5d533064af24204d5be7bd4cc0f3c74b3df98e6d2623289a0d66f1398ad
7
- data.tar.gz: 1f1f0ed249c2963fec160fdbdb6910a8387024f54d9956c35729fb5438037b541989046aee9dab2936dcc585d8f5e9ca6cd1b3247c20fb4dcea62ff7e2fae253
6
+ metadata.gz: 48b3307f4dff316dfa063deddf0d106bbcc409856c20f0af5c441bdc3b1163e9cf93f0c3f7e53fd1a90fcb7946744c2b1b8863fd5a61a0c4012fe3d7df6b5aee
7
+ data.tar.gz: 81aca7224bcae644dd35f6fb715bdb447b0da549bc3da0bdd311a6bea06adfba468936aabcdaeb305a44839c6770a46644abfdf81f2f828246414880e9ab52dd
data/CHANGELOG.md CHANGED
@@ -5,6 +5,11 @@ 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.1.2] - 2026-08-01
9
+
10
+ * Gem metadata
11
+ * The published gem no longer ships non-production code
12
+
8
13
  ## [1.1.1] - 2026-07-31
9
14
 
10
15
  * 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
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2018
1
+ Copyright (c) 2018 Lorent Lempereur
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -18,7 +18,7 @@ the recommended path.
18
18
  ## Install
19
19
 
20
20
  ```ruby
21
- gem "keycloak-api-rails", "1.1.1"
21
+ gem "keycloak-api-rails", "1.1.2"
22
22
  ```
23
23
 
24
24
  ## Token validation
@@ -9,10 +9,21 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ["lorent.lempereur.dev@gmail.com"]
10
10
  spec.homepage = "https://github.com/looorent/keycloak-api-rails"
11
11
  spec.summary = "Rails middleware that validates Authorization token emitted by Keycloak"
12
- spec.description = "Rails middleware that validates Authorization token emitted by Keycloak"
12
+ spec.description = "Rack middleware that validates the Keycloak JWT access token carried by every " \
13
+ "request of a Ruby on Rails API. It verifies the token signature against the " \
14
+ "realm public keys, exposes the authenticated user, its roles and the custom " \
15
+ "attributes of the token to the controllers, and lets some routes opt out of " \
16
+ "authentication."
13
17
  spec.license = "MIT"
14
18
 
15
- spec.files = `git ls-files -z`.split("\x0")
19
+ spec.metadata = {
20
+ "source_code_uri" => spec.homepage,
21
+ "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md",
22
+ "bug_tracker_uri" => "#{spec.homepage}/issues",
23
+ "rubygems_mfa_required" => "true"
24
+ }
25
+
26
+ spec.files = Dir.glob(["lib/**/*.rb", "CHANGELOG.md", "MIT-LICENSE", "README.md", "keycloak-api-rails.gemspec"])
16
27
  spec.require_paths = ["lib"]
17
28
 
18
29
  spec.required_ruby_version = ">= 2.7"
@@ -1,3 +1,3 @@
1
1
  module KeycloakApiRails
2
- VERSION = "1.1.1"
2
+ VERSION = "1.1.2"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keycloak-api-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorent Lempereur
@@ -93,23 +93,19 @@ dependencies:
93
93
  - - ">="
94
94
  - !ruby/object:Gem::Version
95
95
  version: 11.1.3
96
- description: Rails middleware that validates Authorization token emitted by Keycloak
96
+ description: Rack middleware that validates the Keycloak JWT access token carried
97
+ by every request of a Ruby on Rails API. It verifies the token signature against
98
+ the realm public keys, exposes the authenticated user, its roles and the custom
99
+ attributes of the token to the controllers, and lets some routes opt out of authentication.
97
100
  email:
98
101
  - lorent.lempereur.dev@gmail.com
99
102
  executables: []
100
103
  extensions: []
101
104
  extra_rdoc_files: []
102
105
  files:
103
- - ".github/workflows/ci.yml"
104
- - ".github/workflows/release.yml"
105
- - ".gitignore"
106
- - ".rspec"
107
106
  - CHANGELOG.md
108
- - Dockerfile
109
- - Gemfile
110
107
  - MIT-LICENSE
111
108
  - README.md
112
- - Rakefile
113
109
  - keycloak-api-rails.gemspec
114
110
  - lib/keycloak-api-rails.rb
115
111
  - lib/keycloak-api-rails/authentication.rb
@@ -124,22 +120,14 @@ files:
124
120
  - lib/keycloak-api-rails/testing.rb
125
121
  - lib/keycloak-api-rails/token_error.rb
126
122
  - lib/keycloak-api-rails/version.rb
127
- - spec/keycloak-api-rails/activesupport_free_spec.rb
128
- - spec/keycloak-api-rails/authentication_spec.rb
129
- - spec/keycloak-api-rails/configuration_spec.rb
130
- - spec/keycloak-api-rails/helper_spec.rb
131
- - spec/keycloak-api-rails/public_key_cached_resolver_spec.rb
132
- - spec/keycloak-api-rails/service_spec.rb
133
- - spec/keycloak-api-rails/testing_spec.rb
134
- - spec/spec_helper.rb
135
- - spec/support/activesupport_free_probe.rb
136
- - spec/support/public_key_cached_resolver_stub.rb
137
- - spec/support/public_key_resolver_stub.rb
138
- - spec/support/rails_helper.rb
139
123
  homepage: https://github.com/looorent/keycloak-api-rails
140
124
  licenses:
141
125
  - MIT
142
- metadata: {}
126
+ metadata:
127
+ source_code_uri: https://github.com/looorent/keycloak-api-rails
128
+ changelog_uri: https://github.com/looorent/keycloak-api-rails/blob/main/CHANGELOG.md
129
+ bug_tracker_uri: https://github.com/looorent/keycloak-api-rails/issues
130
+ rubygems_mfa_required: 'true'
143
131
  rdoc_options: []
144
132
  require_paths:
145
133
  - lib
@@ -1,35 +0,0 @@
1
- name: Ruby
2
-
3
- on:
4
- push:
5
- branches: [ "main" ]
6
- pull_request:
7
- branches: [ "main" ]
8
-
9
- permissions:
10
- contents: read
11
-
12
- jobs:
13
- test:
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
19
-
20
- strategy:
21
- fail-fast: false
22
- matrix:
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']
26
-
27
- steps:
28
- - uses: actions/checkout@v4
29
- - name: Set up Ruby
30
- uses: ruby/setup-ruby@v1
31
- with:
32
- ruby-version: ${{ matrix.ruby-version }}
33
- bundler-cache: true # runs 'bundle install' and caches installed gems automatically
34
- - name: Run tests
35
- run: bundle exec rspec
@@ -1,52 +0,0 @@
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 DELETED
@@ -1,9 +0,0 @@
1
- .bundle/
2
- log/*.log
3
- .byebug_history
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/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --color
2
- --require spec_helper
data/Dockerfile DELETED
@@ -1,13 +0,0 @@
1
- FROM ruby:3.4.10-slim-bookworm
2
-
3
- RUN apt-get update -qq && apt-get install -y build-essential git ruby-dev && apt-get clean && \
4
- mkdir -p /usr/src/app/lib/keycloak-api-rails
5
-
6
- WORKDIR /usr/src/app
7
-
8
- COPY Gemfile /usr/src/app/
9
- COPY keycloak-api-rails.gemspec /usr/src/app/
10
- COPY lib/keycloak-api-rails/version.rb /usr/src/app/lib/keycloak-api-rails/
11
- RUN bundle install
12
- COPY . /usr/src/app
13
- RUN bundle install
data/Gemfile DELETED
@@ -1,3 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- gemspec
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task default: :spec
@@ -1,18 +0,0 @@
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
@@ -1,70 +0,0 @@
1
- require "spec_helper"
2
-
3
- describe KeycloakApiRails::Authentication do
4
- class ExampleController < ActionController::Base
5
- include KeycloakApiRails::Authentication
6
- # Mark protected methods public so they may be called in tests
7
- public(*KeycloakApiRails::Authentication.protected_instance_methods)
8
- end
9
-
10
- let(:controller) { ExampleController.new }
11
- let(:token) { 'keycloak_valid_token'}
12
- let(:headers) do
13
- {
14
- 'REQUEST_METHOD' => :get,
15
- 'REQUEST_URI' => 'http://www.an-url.io',
16
- 'HTTP_AUTHORIZATION' => "Bearer #{token}"
17
- }
18
- end
19
-
20
- describe "when included" do
21
- it "declares keycloak_authenticate as a helper method when the base class supports helpers" do
22
- declared_helper_methods = []
23
- base = Class.new do
24
- define_singleton_method(:helper_method) { |*names| declared_helper_methods.concat(names) }
25
- end
26
-
27
- base.include(KeycloakApiRails::Authentication)
28
-
29
- expect(declared_helper_methods).to eq [:keycloak_authenticate]
30
- end
31
-
32
- it "declares the helper method on a real controller" do
33
- expect(ExampleController._helper_methods).to include :keycloak_authenticate
34
- end
35
-
36
- it "does not fail when the base class does not support helpers" do
37
- expect {
38
- Class.new.include(KeycloakApiRails::Authentication)
39
- }.to_not raise_error
40
- end
41
-
42
- it "exposes its methods as protected instance methods" do
43
- expect(KeycloakApiRails::Authentication.protected_instance_methods).to match_array %i[
44
- keycloak_authenticate
45
- authentication_failed
46
- authentication_succeeded
47
- ]
48
- end
49
-
50
- it "does not add any public method to the including class" do
51
- base = Class.new.include(KeycloakApiRails::Authentication)
52
-
53
- expect(base.public_instance_methods).to_not include :keycloak_authenticate
54
- expect(base.protected_instance_methods).to include :keycloak_authenticate
55
- end
56
- end
57
-
58
- describe "#keycloak_authenticate" do
59
- before do
60
- # Mock request object because we aren't using real request spec
61
- allow(controller).to receive(:request).and_return(double("request", env: headers ))
62
- end
63
-
64
- it "it authenticates with request header" do
65
- expect_any_instance_of(KeycloakApiRails::Service).to receive(:decode_and_verify).with(token).and_return("A User")
66
- expect(controller).to receive(:authentication_succeeded)
67
- controller.keycloak_authenticate
68
- end
69
- end
70
- end
@@ -1,101 +0,0 @@
1
- RSpec.describe KeycloakApiRails::Configuration do
2
-
3
- # Every attribute that used to be declared through ActiveSupport's `config_accessor`.
4
- let(:attribute_names) do
5
- %i[
6
- server_url
7
- realm_id
8
- skip_paths
9
- opt_in
10
- token_expiration_tolerance_in_seconds
11
- public_key_cache_ttl
12
- custom_attributes
13
- logger
14
- ca_certificate_file
15
- ]
16
- end
17
-
18
- describe "attributes" do
19
- let(:configuration) { KeycloakApiRails::Configuration.new }
20
-
21
- it "exposes a reader and a writer for each attribute" do
22
- attribute_names.each do |name|
23
- expect(configuration).to respond_to name
24
- expect(configuration).to respond_to "#{name}="
25
- end
26
- end
27
-
28
- it "returns nil for each attribute that has not been assigned" do
29
- attribute_names.each do |name|
30
- expect(configuration.public_send(name)).to be_nil
31
- end
32
- end
33
-
34
- it "returns the value that has been assigned" do
35
- attribute_names.each do |name|
36
- configuration.public_send("#{name}=", "a value for #{name}")
37
- expect(configuration.public_send(name)).to eq "a value for #{name}"
38
- end
39
- end
40
-
41
- it "does not respond to an unknown attribute" do
42
- expect(configuration).to_not respond_to :not_a_configuration_attribute
43
- expect {
44
- configuration.not_a_configuration_attribute
45
- }.to raise_error NoMethodError
46
- end
47
- end
48
-
49
- describe "#configure" do
50
- after(:each) do
51
- KeycloakApiRails.load_configuration
52
- end
53
-
54
- it "yields the configuration" do
55
- expect { |block| KeycloakApiRails.configure(&block) }.to yield_with_args KeycloakApiRails::Configuration
56
- end
57
-
58
- it "keeps the values assigned within the block" do
59
- KeycloakApiRails.configure do |config|
60
- config.server_url = "http://keycloak:8080"
61
- config.realm_id = "a-realm"
62
- config.ca_certificate_file = "/etc/ssl/a-certificate.pem"
63
- end
64
-
65
- expect(KeycloakApiRails.config.server_url).to eq "http://keycloak:8080"
66
- expect(KeycloakApiRails.config.realm_id).to eq "a-realm"
67
- expect(KeycloakApiRails.config.ca_certificate_file).to eq "/etc/ssl/a-certificate.pem"
68
- end
69
-
70
- it "always returns the same configuration instance" do
71
- expect(KeycloakApiRails.config).to be KeycloakApiRails.config
72
- end
73
- end
74
-
75
- describe "#load_configuration" do
76
- before(:each) do
77
- KeycloakApiRails.load_configuration
78
- end
79
-
80
- after(:each) do
81
- KeycloakApiRails.load_configuration
82
- end
83
-
84
- it "applies the documented default values" do
85
- config = KeycloakApiRails.config
86
-
87
- expect(config.server_url).to be_nil
88
- expect(config.realm_id).to be_nil
89
- expect(config.logger).to be_a ::Logger
90
- expect(config.skip_paths).to eq({})
91
- expect(config.opt_in).to be false
92
- expect(config.token_expiration_tolerance_in_seconds).to eq 10
93
- expect(config.public_key_cache_ttl).to eq 86400
94
- expect(config.custom_attributes).to eq []
95
- end
96
-
97
- it "exposes the logger through KeycloakApiRails.logger" do
98
- expect(KeycloakApiRails.logger).to be KeycloakApiRails.config.logger
99
- end
100
- end
101
- end
@@ -1,94 +0,0 @@
1
- RSpec.describe KeycloakApiRails::Helper do
2
- describe "#read_token_from_query_string" do
3
-
4
- subject { KeycloakApiRails::Helper.read_token_from_query_string(uri) }
5
-
6
- context "when the uri is nil" do
7
- let(:uri) { nil }
8
- it "returns an empty token" do
9
- expect(subject).to eq ""
10
- end
11
- end
12
-
13
- context "when the uri is empty" do
14
- let(:uri) { "" }
15
- it "returns an empty token" do
16
- expect(subject).to eq ""
17
- end
18
- end
19
-
20
- context "when the uri only contains whitespaces" do
21
- let(:uri) { " " }
22
- it "returns an empty token" do
23
- expect(subject).to eq ""
24
- end
25
- end
26
-
27
- context "when the uri has no query string" do
28
- let(:uri) { "http://www.an-url.io/health" }
29
- it "returns no token" do
30
- expect(subject).to be_nil
31
- end
32
- end
33
-
34
- context "when the uri has a query string but no token" do
35
- let(:uri) { "http://www.an-url.io/health?firstName=ouioui" }
36
- it "returns no token" do
37
- expect(subject).to be_nil
38
- end
39
- end
40
-
41
- context "when the uri has a token in the query string" do
42
- let(:uri) { "http://www.an-url.io/health?authorizationToken=aToken" }
43
- it "returns the token" do
44
- expect(subject).to eq "aToken"
45
- end
46
- end
47
-
48
- context "when the uri has a token among other query string parameters" do
49
- let(:uri) { "http://www.an-url.io/health?firstName=ouioui&authorizationToken=aToken&lastName=nonnon" }
50
- it "returns the token" do
51
- expect(subject).to eq "aToken"
52
- end
53
- end
54
-
55
- context "when the uri has an empty token in the query string" do
56
- let(:uri) { "http://www.an-url.io/health?authorizationToken=" }
57
- it "returns an empty token" do
58
- expect(subject).to eq ""
59
- end
60
- end
61
- end
62
-
63
- describe "#create_url_with_token" do
64
-
65
- let(:uri) { "http://www.an-url.io" }
66
- let(:token) { "aToken" }
67
-
68
- before(:each) do
69
- @url_with_token = KeycloakApiRails::Helper.create_url_with_token(uri, token)
70
- end
71
-
72
- context "when the uri has no query string yet" do
73
- it "returns an url with the provided token" do
74
- expect(@url_with_token).to eq "#{uri}?authorizationToken=#{token}"
75
- end
76
- end
77
-
78
- context "when the uri already has no query strings" do
79
- context "but no token yet" do
80
- let(:uri) { "http://www.an-url.io?firstName=ouioui&lastName=nonnon" }
81
- it "returns an url with all the query string and the token" do
82
- expect(@url_with_token).to eq "#{uri}&authorizationToken=#{token}"
83
- end
84
- end
85
-
86
- context "including a token" do
87
- let(:uri) { "http://www.an-url.io?authorizationToken=ouioui&lastName=nonnon" }
88
- it "returns an url with all the query string and the new token" do
89
- expect(@url_with_token).to eq "http://www.an-url.io?lastName=nonnon&authorizationToken=#{token}"
90
- end
91
- end
92
- end
93
- end
94
- end
@@ -1,80 +0,0 @@
1
- RSpec.describe KeycloakApiRails::Service do
2
-
3
- let(:public_key_cache_ttl) { 86400 }
4
- let(:server_url) { "whatever:8080" }
5
- let(:realm_id) { "pouet" }
6
- let!(:resolver) { KeycloakApiRails::PublicKeyCachedResolver.new(server_url, realm_id, public_key_cache_ttl) }
7
-
8
- before(:each) do
9
- resolver.instance_variable_set(:@resolver, KeycloakApiRails::PublicKeyResolverStub.new)
10
- now = Time.local(2018, 1, 9, 12, 0, 0)
11
- Timecop.freeze(now)
12
- end
13
-
14
- after(:each) do
15
- Timecop.return
16
- end
17
-
18
- describe "#find_public_key" do
19
- context "when there is no public key in cache yet" do
20
- before(:each) do
21
- @public_key = resolver.find_public_keys
22
- end
23
-
24
- it "returns a valid public key" do
25
- expect(@public_key).to_not be_nil
26
- end
27
-
28
- it "sets the current time to the resolver" do
29
- expect(resolver.cached_public_key_retrieved_at).to eq Time.now
30
- end
31
- end
32
-
33
- context "when there is already a public key in cache" do
34
- before(:each) do
35
- @first_public_key = resolver.find_public_keys
36
- @first_cached_public_key_retrieved_at = resolver.cached_public_key_retrieved_at
37
- end
38
-
39
- context "and no need to refresh it" do
40
- before(:each) do
41
- Timecop.freeze(Time.now + public_key_cache_ttl.seconds - 10.seconds)
42
- @second_public_key = resolver.find_public_keys
43
- @second_cached_public_key_retrieved_at = resolver.cached_public_key_retrieved_at
44
- end
45
-
46
- it "returns a valid public key" do
47
- expect(@second_public_key).to_not be_nil
48
- end
49
-
50
- it "does not refresh the public key" do
51
- expect(@second_public_key).to eq @first_public_key
52
- end
53
-
54
- it "does not refresh the public key retrieval time" do
55
- expect(@first_cached_public_key_retrieved_at).to eq @second_cached_public_key_retrieved_at
56
- end
57
- end
58
-
59
- context "and its TTL has expired" do
60
- before(:each) do
61
- Timecop.freeze(Time.now + public_key_cache_ttl.seconds + 10.seconds)
62
- @second_public_key = resolver.find_public_keys
63
- @second_cached_public_key_retrieved_at = resolver.cached_public_key_retrieved_at
64
- end
65
-
66
- it "returns a valid public key" do
67
- expect(@second_public_key).to_not be_nil
68
- end
69
-
70
- it "refreshes the public key" do
71
- expect(@second_public_key).to_not eq @first_public_key
72
- end
73
-
74
- it "refreshes the public key retrieval time" do
75
- expect(@first_cached_public_key_retrieved_at).to_not eq @second_cached_public_key_retrieved_at
76
- end
77
- end
78
- end
79
- end
80
- end
@@ -1,281 +0,0 @@
1
- RSpec.describe KeycloakApiRails::Service do
2
-
3
- let!(:private_key) { OpenSSL::PKey::RSA.generate(2048) }
4
- let!(:public_key) { private_key.public_key }
5
- let!(:key_resolver) { KeycloakApiRails::PublicKeyCachedResolverStub.new(public_key) }
6
- let!(:service) { KeycloakApiRails::Service.new(key_resolver) }
7
-
8
- before(:each) do
9
- now = Time.local(2018, 1, 9, 12, 0, 0)
10
- Timecop.freeze(now)
11
- end
12
-
13
- after(:each) do
14
- Timecop.return
15
- end
16
-
17
- describe "#decode_and_verify" do
18
- def create_token(private_key, expiration_date, algorithm)
19
- claim = {
20
- iss: "KeycloakApiRails",
21
- exp: expiration_date,
22
- nbf: Time.local(2018, 1, 1, 0, 0, 0)
23
- }
24
- jws = JSON::JWT.new(claim).sign(private_key, algorithm)
25
- jws.to_s
26
- end
27
-
28
- context "when token is nil" do
29
- let(:token) { nil }
30
- it "should raise an error :no_token" do
31
- expect {
32
- service.decode_and_verify(token)
33
- }.to raise_error(TokenError, "No JWT token provided")
34
- end
35
- end
36
-
37
- context "when token is an empty string" do
38
- let(:token) { "" }
39
- it "should raise an error :no_token" do
40
- expect {
41
- service.decode_and_verify(token)
42
- }.to raise_error(TokenError, "No JWT token provided")
43
- end
44
- end
45
-
46
- context "when token is in an invalid format" do
47
- let(:token) { "coucou" }
48
- it "should raise an error :invalid_format" do
49
- expect {
50
- service.decode_and_verify(token)
51
- }.to raise_error(TokenError, "Wrong JWT Format")
52
- end
53
- end
54
-
55
- context "when token is in a valid format" do
56
- let(:algorithm) { :RS256 }
57
- let(:expiration_date) { 1.week.from_now }
58
-
59
- context "and token is generated by another private key" do
60
- let(:another_private_key) { OpenSSL::PKey::RSA.generate(1024) }
61
- let(:token) { create_token(another_private_key, expiration_date, algorithm) }
62
-
63
- it "should raise an error :verification_failed" do
64
- expect {
65
- service.decode_and_verify(token)
66
- }.to raise_error(TokenError, "Failed to verify JWT token")
67
- end
68
- end
69
-
70
- context "and token is generated by the right private key" do
71
- let(:token) { create_token(private_key, expiration_date, algorithm) }
72
-
73
- context "and token is expired" do
74
- let(:expiration_date) { Time.now - 2.days }
75
-
76
- it "should raise an error :expiration_date" do
77
- expect {
78
- service.decode_and_verify(token)
79
- }.to raise_error(TokenError, "JWT token is expired")
80
- end
81
- end
82
-
83
- # The service reads `token_expiration_tolerance_in_seconds` when it is instantiated:
84
- # a token expiring within that tolerance is already considered as expired.
85
- context "and token expires within the expiration tolerance" do
86
- let(:expiration_date) { Time.now + 5 }
87
-
88
- it "should raise an error :expiration_date" do
89
- expect(KeycloakApiRails.config.token_expiration_tolerance_in_seconds).to eq 10
90
- expect {
91
- service.decode_and_verify(token)
92
- }.to raise_error(TokenError, "JWT token is expired")
93
- end
94
- end
95
-
96
- context "and token expires just after the expiration tolerance" do
97
- let(:expiration_date) { Time.now + 30 }
98
-
99
- it "should return a not-nil decoded token" do
100
- expect(KeycloakApiRails.config.token_expiration_tolerance_in_seconds).to eq 10
101
- expect(service.decode_and_verify(token)).to_not be_nil
102
- end
103
- end
104
-
105
- context "and token is not expired" do
106
- let(:expiration_date) { Time.now + 2.days }
107
-
108
- context "and token is encrypted using RS256" do
109
- let(:algorithm) { :RS256 }
110
-
111
- it "should return a not-nil decoded token" do
112
- expect(service.decode_and_verify(token)).to_not be_nil
113
- end
114
- end
115
-
116
- context "and token is encrypted using RS512" do
117
- let(:algorithm) { :RS512 }
118
-
119
- it "should return a not-nil decoded token" do
120
- expect(service.decode_and_verify(token)).to_not be_nil
121
- end
122
- end
123
- end
124
- end
125
- end
126
- end
127
-
128
- describe "#need_middleware_authentication?" do
129
-
130
- let(:method) { nil }
131
- let(:path) { nil }
132
- let(:headers) { {} }
133
-
134
-
135
- before(:each) do
136
- KeycloakApiRails.config.skip_paths = {
137
- post: [/^\/skip/],
138
- get: [/^\/skip/]
139
- }
140
- @result = service.need_middleware_authentication?(method, path, headers)
141
- end
142
-
143
- context "when method is nil" do
144
- let(:method) { nil }
145
- let(:path) { "/do-not-skip" }
146
- it "should return true" do
147
- expect(@result).to be true
148
- end
149
- end
150
-
151
- context "when path is nil" do
152
- let(:method) { :get }
153
- let(:path) { nil }
154
- it "should return true" do
155
- expect(@result).to be true
156
- end
157
- end
158
-
159
- context "when method does not match the configuration" do
160
- let(:method) { :put }
161
- let(:path) { "/skip" }
162
- it "should return true" do
163
- expect(@result).to be true
164
- end
165
- end
166
-
167
- context "when path does not match the configuration" do
168
- let(:method) { :get }
169
- let(:path) { "/do-not-skip" }
170
- it "should return true" do
171
- expect(@result).to be true
172
- end
173
- end
174
-
175
- context "when method [get] and path do match the configuration" do
176
- let(:method) { :get }
177
- let(:path) { "/skip" }
178
- it "should return false" do
179
- expect(@result).to be false
180
- end
181
- end
182
-
183
-
184
- context "when method [post] and path do match the configuration" do
185
- let(:method) { :get }
186
- let(:path) { "/skip" }
187
- it "should return false" do
188
- expect(@result).to be false
189
- end
190
- end
191
-
192
- context "when the request is preflight" do
193
- let(:method) { :options }
194
- let(:headers) { { "HTTP_ACCESS_CONTROL_REQUEST_METHOD" => ["Authorization"] } }
195
- let(:path) { "/do-not-skip" }
196
- it "should return false" do
197
- expect(@result).to be false
198
- end
199
- end
200
-
201
- context "when configured as opt_in" do
202
- before do
203
- KeycloakApiRails.config.opt_in = true
204
- service2 = KeycloakApiRails::Service.new(key_resolver)
205
- @result = service2.need_middleware_authentication?(method, path, headers)
206
- end
207
-
208
- # The configuration is global: leaving 'opt_in' enabled would disable the authentication
209
- # of every example running afterwards.
210
- after do
211
- KeycloakApiRails.config.opt_in = false
212
- end
213
-
214
- it "should return false" do
215
- expect(@result).to be false
216
- end
217
- end
218
- end
219
-
220
- describe "#read_token" do
221
- let(:query_string) { "" }
222
- let(:url) { "http://api.service.com/api/health?aParameter=true#{query_string}" }
223
- let(:headers) { {} }
224
- let(:header_token) { "header_token" }
225
- let(:query_string_token) { "query_string_token" }
226
-
227
- before(:each) do
228
- @token = service.read_token(url, headers)
229
- end
230
-
231
- context "when the token is provided in the Authorization headers" do
232
- let(:headers) do
233
- {
234
- "HTTP_AUTHORIZATION" => "Bearer #{header_token}"
235
- }
236
- end
237
- context "and not in the query string" do
238
- let(:query_string) { "" }
239
- it "returns the header token" do
240
- expect(@token).to eq header_token
241
- end
242
- end
243
-
244
- context "and also in the query string" do
245
- let(:query_string) { "&authorizationToken=#{query_string_token}" }
246
- it "returns the query string token" do
247
- expect(@token).to eq query_string_token
248
- end
249
- end
250
- end
251
-
252
- context "when the token is not provided in the Authorization headers" do
253
- let(:headers) do
254
- {
255
- "ANOTHER_HEADER" => header_token
256
- }
257
- end
258
-
259
- context "and not in the query string" do
260
- let(:query_string) { "" }
261
- it "returns an empty token" do
262
- expect(@token).to eq ""
263
- end
264
- end
265
-
266
- context "and query string is nil" do
267
- let(:query_string) { nil }
268
- it "returns an empty token" do
269
- expect(@token).to eq ""
270
- end
271
- end
272
-
273
- context "but in the query string" do
274
- let(:query_string) { "&authorizationToken=#{query_string_token}" }
275
- it "returns the query string token" do
276
- expect(@token).to eq query_string_token
277
- end
278
- end
279
- end
280
- end
281
- end
@@ -1,102 +0,0 @@
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
data/spec/spec_helper.rb DELETED
@@ -1,14 +0,0 @@
1
- require_relative "../lib/keycloak-api-rails"
2
- require_relative "support/rails_helper"
3
- require_relative "support/public_key_cached_resolver_stub"
4
- require_relative "support/public_key_resolver_stub"
5
- require "timecop"
6
- require "byebug"
7
-
8
- RSpec.configure do |config|
9
- config.include RailsHelper
10
-
11
- config.expect_with :rspec do |expectations|
12
- expectations.include_chain_clauses_in_custom_matcher_descriptions = true
13
- end
14
- end
@@ -1,131 +0,0 @@
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
@@ -1,11 +0,0 @@
1
- module KeycloakApiRails
2
- class PublicKeyCachedResolverStub
3
- def initialize(public_key)
4
- @public_key = public_key
5
- end
6
-
7
- def find_public_keys
8
- @public_key
9
- end
10
- end
11
- end
@@ -1,7 +0,0 @@
1
- module KeycloakApiRails
2
- class PublicKeyResolverStub
3
- def find_public_keys
4
- OpenSSL::PKey::RSA.generate(1024).public_key
5
- end
6
- end
7
- end
@@ -1,4 +0,0 @@
1
- require "rails/all"
2
-
3
- module RailsHelper
4
- end