keycloak-api-rails 0.12.4 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +28 -0
- data/CHANGELOG.md +13 -0
- data/Dockerfile +1 -1
- data/Gemfile.lock +159 -138
- data/README.md +19 -19
- data/keycloak-api-rails.gemspec +4 -4
- data/lib/keycloak-api-rails/authentication.rb +9 -11
- data/lib/keycloak-api-rails/configuration.rb +10 -11
- data/lib/keycloak-api-rails/helper.rb +3 -3
- data/lib/keycloak-api-rails/http_client.rb +2 -2
- data/lib/keycloak-api-rails/middleware.rb +4 -4
- data/lib/keycloak-api-rails/public_key_cached_resolver.rb +2 -2
- data/lib/keycloak-api-rails/public_key_resolver.rb +1 -1
- data/lib/keycloak-api-rails/railtie.rb +2 -2
- data/lib/keycloak-api-rails/service.rb +7 -7
- data/lib/keycloak-api-rails/version.rb +2 -2
- data/lib/keycloak-api-rails.rb +4 -4
- data/spec/keycloak-api-rails/activesupport_free_spec.rb +18 -0
- data/spec/keycloak-api-rails/authentication_spec.rb +42 -4
- data/spec/keycloak-api-rails/configuration_spec.rb +101 -0
- data/spec/keycloak-api-rails/helper_spec.rb +63 -2
- data/spec/keycloak-api-rails/public_key_cached_resolver_spec.rb +3 -3
- data/spec/keycloak-api-rails/service_spec.rb +30 -8
- data/spec/support/activesupport_free_probe.rb +131 -0
- data/spec/support/public_key_cached_resolver_stub.rb +1 -1
- data/spec/support/public_key_resolver_stub.rb +1 -1
- metadata +16 -12
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
module
|
|
1
|
+
module KeycloakApiRails
|
|
2
2
|
class Configuration
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module
|
|
1
|
+
module KeycloakApiRails
|
|
2
2
|
class Helper
|
|
3
3
|
|
|
4
4
|
CURRENT_USER_ID_KEY = "keycloak:keycloak_id"
|
|
@@ -83,11 +83,11 @@ module Keycloak
|
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
def self.read_token_from_query_string(uri)
|
|
86
|
-
if uri.
|
|
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&.
|
|
90
|
+
query_string_token&.at(1)
|
|
91
91
|
else
|
|
92
92
|
""
|
|
93
93
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module
|
|
1
|
+
module KeycloakApiRails
|
|
2
2
|
class HTTPClient
|
|
3
3
|
def initialize(configuration, logger)
|
|
4
4
|
@server_url = configuration.server_url
|
|
@@ -20,7 +20,7 @@ module Keycloak
|
|
|
20
20
|
response.value
|
|
21
21
|
JSON.parse(response.body)
|
|
22
22
|
rescue
|
|
23
|
-
@logger.error("
|
|
23
|
+
@logger.error("KeycloakApiRails responded with an error when calling '#{path}'. Status #{response.code}. Payload: #{response.body}")
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module
|
|
1
|
+
module KeycloakApiRails
|
|
2
2
|
|
|
3
3
|
class Middleware
|
|
4
4
|
def initialize(app)
|
|
@@ -41,15 +41,15 @@ module Keycloak
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def service
|
|
44
|
-
|
|
44
|
+
KeycloakApiRails.service
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def logger
|
|
48
|
-
|
|
48
|
+
KeycloakApiRails.logger
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def config
|
|
52
|
-
|
|
52
|
+
KeycloakApiRails.config
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module
|
|
1
|
+
module KeycloakApiRails
|
|
2
2
|
class PublicKeyCachedResolver
|
|
3
3
|
attr_reader :cached_public_key_retrieved_at
|
|
4
4
|
|
|
@@ -24,7 +24,7 @@ module Keycloak
|
|
|
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
|
|
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
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
module
|
|
1
|
+
module KeycloakApiRails
|
|
2
2
|
class Railtie < Rails::Railtie
|
|
3
3
|
railtie_name :keycloak_api_rails
|
|
4
4
|
|
|
5
5
|
initializer("keycloak.insert_middleware") do |app|
|
|
6
|
-
app.config.middleware.use(
|
|
6
|
+
app.config.middleware.use(KeycloakApiRails::Middleware)
|
|
7
7
|
end
|
|
8
8
|
end
|
|
9
9
|
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
module
|
|
1
|
+
module KeycloakApiRails
|
|
2
2
|
class Service
|
|
3
3
|
|
|
4
4
|
def initialize(key_resolver)
|
|
5
5
|
@key_resolver = key_resolver
|
|
6
|
-
@skip_paths =
|
|
7
|
-
@opt_in =
|
|
8
|
-
@logger =
|
|
9
|
-
@token_expiration_tolerance_in_seconds =
|
|
6
|
+
@skip_paths = KeycloakApiRails.config.skip_paths
|
|
7
|
+
@opt_in = KeycloakApiRails.config.opt_in
|
|
8
|
+
@logger = KeycloakApiRails.config.logger
|
|
9
|
+
@token_expiration_tolerance_in_seconds = KeycloakApiRails.config.token_expiration_tolerance_in_seconds
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def decode_and_verify(token)
|
|
@@ -53,8 +53,8 @@ module Keycloak
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def expired?(token)
|
|
56
|
-
token_expiration = Time.at(token["exp"])
|
|
57
|
-
token_expiration < Time.now + @token_expiration_tolerance_in_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
|
-
module
|
|
2
|
-
VERSION = "0.
|
|
1
|
+
module KeycloakApiRails
|
|
2
|
+
VERSION = "1.0.1"
|
|
3
3
|
end
|
data/lib/keycloak-api-rails.rb
CHANGED
|
@@ -15,10 +15,10 @@ require_relative "keycloak-api-rails/service"
|
|
|
15
15
|
require_relative "keycloak-api-rails/middleware"
|
|
16
16
|
require_relative "keycloak-api-rails/railtie" if defined?(Rails)
|
|
17
17
|
|
|
18
|
-
module
|
|
18
|
+
module KeycloakApiRails
|
|
19
19
|
|
|
20
20
|
def self.configure
|
|
21
|
-
yield @configuration ||=
|
|
21
|
+
yield @configuration ||= KeycloakApiRails::Configuration.new
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def self.config
|
|
@@ -26,7 +26,7 @@ module Keycloak
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def self.http_client
|
|
29
|
-
@http_client ||=
|
|
29
|
+
@http_client ||= KeycloakApiRails::HTTPClient.new(config, logger)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def self.public_key_resolver
|
|
@@ -34,7 +34,7 @@ module Keycloak
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def self.service
|
|
37
|
-
@service ||=
|
|
37
|
+
@service ||= KeycloakApiRails::Service.new(public_key_resolver)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def self.logger
|
|
@@ -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
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
require "spec_helper"
|
|
2
2
|
|
|
3
|
-
describe
|
|
3
|
+
describe KeycloakApiRails::Authentication do
|
|
4
4
|
class ExampleController < ActionController::Base
|
|
5
|
-
include
|
|
5
|
+
include KeycloakApiRails::Authentication
|
|
6
6
|
# Mark protected methods public so they may be called in tests
|
|
7
|
-
public(*
|
|
7
|
+
public(*KeycloakApiRails::Authentication.protected_instance_methods)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
let(:controller) { ExampleController.new }
|
|
@@ -17,6 +17,44 @@ describe Keycloak::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
|
|
@@ -24,7 +62,7 @@ describe Keycloak::Authentication do
|
|
|
24
62
|
end
|
|
25
63
|
|
|
26
64
|
it "it authenticates with request header" do
|
|
27
|
-
expect_any_instance_of(
|
|
65
|
+
expect_any_instance_of(KeycloakApiRails::Service).to receive(:decode_and_verify).with(token).and_return("A User")
|
|
28
66
|
expect(controller).to receive(:authentication_succeeded)
|
|
29
67
|
controller.keycloak_authenticate
|
|
30
68
|
end
|
|
@@ -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,11 +1,72 @@
|
|
|
1
|
-
RSpec.describe
|
|
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" }
|
|
5
66
|
let(:token) { "aToken" }
|
|
6
67
|
|
|
7
68
|
before(:each) do
|
|
8
|
-
@url_with_token =
|
|
69
|
+
@url_with_token = KeycloakApiRails::Helper.create_url_with_token(uri, token)
|
|
9
70
|
end
|
|
10
71
|
|
|
11
72
|
context "when the uri has no query string yet" do
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
RSpec.describe
|
|
1
|
+
RSpec.describe KeycloakApiRails::Service do
|
|
2
2
|
|
|
3
3
|
let(:public_key_cache_ttl) { 86400 }
|
|
4
4
|
let(:server_url) { "whatever:8080" }
|
|
5
5
|
let(:realm_id) { "pouet" }
|
|
6
|
-
let!(:resolver) {
|
|
6
|
+
let!(:resolver) { KeycloakApiRails::PublicKeyCachedResolver.new(server_url, realm_id, public_key_cache_ttl) }
|
|
7
7
|
|
|
8
8
|
before(:each) do
|
|
9
|
-
resolver.instance_variable_set(:@resolver,
|
|
9
|
+
resolver.instance_variable_set(:@resolver, KeycloakApiRails::PublicKeyResolverStub.new)
|
|
10
10
|
now = Time.local(2018, 1, 9, 12, 0, 0)
|
|
11
11
|
Timecop.freeze(now)
|
|
12
12
|
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
RSpec.describe
|
|
1
|
+
RSpec.describe KeycloakApiRails::Service do
|
|
2
2
|
|
|
3
3
|
let!(:private_key) { OpenSSL::PKey::RSA.generate(2048) }
|
|
4
4
|
let!(:public_key) { private_key.public_key }
|
|
5
|
-
let!(:key_resolver) {
|
|
6
|
-
let!(:service) {
|
|
5
|
+
let!(:key_resolver) { KeycloakApiRails::PublicKeyCachedResolverStub.new(public_key) }
|
|
6
|
+
let!(:service) { KeycloakApiRails::Service.new(key_resolver) }
|
|
7
7
|
|
|
8
8
|
before(:each) do
|
|
9
9
|
now = Time.local(2018, 1, 9, 12, 0, 0)
|
|
@@ -17,7 +17,7 @@ RSpec.describe Keycloak::Service do
|
|
|
17
17
|
describe "#decode_and_verify" do
|
|
18
18
|
def create_token(private_key, expiration_date, algorithm)
|
|
19
19
|
claim = {
|
|
20
|
-
iss: "
|
|
20
|
+
iss: "KeycloakApiRails",
|
|
21
21
|
exp: expiration_date,
|
|
22
22
|
nbf: Time.local(2018, 1, 1, 0, 0, 0)
|
|
23
23
|
}
|
|
@@ -80,9 +80,31 @@ RSpec.describe Keycloak::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
|
|
|
@@ -111,7 +133,7 @@ RSpec.describe Keycloak::Service do
|
|
|
111
133
|
|
|
112
134
|
|
|
113
135
|
before(:each) do
|
|
114
|
-
|
|
136
|
+
KeycloakApiRails.config.skip_paths = {
|
|
115
137
|
post: [/^\/skip/],
|
|
116
138
|
get: [/^\/skip/]
|
|
117
139
|
}
|
|
@@ -178,8 +200,8 @@ RSpec.describe Keycloak::Service do
|
|
|
178
200
|
|
|
179
201
|
context "when configured as opt_in" do
|
|
180
202
|
before do
|
|
181
|
-
|
|
182
|
-
service2 =
|
|
203
|
+
KeycloakApiRails.config.opt_in = true
|
|
204
|
+
service2 = KeycloakApiRails::Service.new(key_resolver)
|
|
183
205
|
@result = service2.need_middleware_authentication?(method, path, headers)
|
|
184
206
|
end
|
|
185
207
|
|
|
@@ -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
|