keycloak-api-rails 1.1.2 → 2.0.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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KeycloakApiRails
2
4
 
3
5
  class Middleware
@@ -8,36 +10,43 @@ module KeycloakApiRails
8
10
  def call(env)
9
11
  method = env["REQUEST_METHOD"]
10
12
  path = env["PATH_INFO"]
11
- uri = env["REQUEST_URI"]
12
13
 
13
14
  if service.need_middleware_authentication?(method, path, env)
14
15
  logger.debug("Start authentication for #{method} : #{path}")
15
- token = service.read_token(uri, env)
16
- decoded_token = service.decode_and_verify(token)
17
- authentication_succeeded(env, decoded_token)
16
+ begin
17
+ authenticate(env)
18
+ rescue TokenError => e
19
+ logger.debug("The error causing the Token to fail: #{e.original_error&.message || e.message}")
20
+ return authentication_failed(e.message)
21
+ rescue HTTPError, MissingPublicKeysError => e
22
+ logger.error("KeycloakApiRails: no token can be verified for #{method} : #{path}. #{e.class}: #{e.message}")
23
+ return authentication_unavailable
24
+ end
18
25
  else
19
26
  logger.debug("Skip authentication for #{method} : #{path}")
20
- @app.call(env)
21
27
  end
22
- rescue TokenError => e
23
- logger.debug("The error causing the Token to fail: #{e.original_error&.message || e.message}")
24
- authentication_failed(e.message)
28
+
29
+ @app.call(env)
30
+ end
31
+
32
+ private
33
+
34
+ def authenticate(env)
35
+ token = service.read_token(Helper.request_uri(env), env)
36
+ decoded_token = service.decode_and_verify(token)
37
+ Helper.assign_token(env, decoded_token, config.custom_attributes)
25
38
  end
26
39
 
27
40
  def authentication_failed(message)
28
- [401, {"Content-Type" => "application/json"}, [ { error: message }.to_json]]
29
- end
30
-
31
- def authentication_succeeded(env, decoded_token)
32
- Helper.assign_current_user_id(env, decoded_token)
33
- Helper.assign_current_authorized_party(env, decoded_token)
34
- Helper.assign_current_user_email(env, decoded_token)
35
- Helper.assign_current_user_locale(env, decoded_token)
36
- Helper.assign_current_user_custom_attributes(env, decoded_token, config.custom_attributes)
37
- Helper.assign_realm_roles(env, decoded_token)
38
- Helper.assign_resource_roles(env, decoded_token)
39
- Helper.assign_keycloak_token(env, decoded_token)
40
- @app.call(env)
41
+ # Rack 3 requires header names to be lowercase.
42
+ [401, { "content-type" => "application/json" }, [{ error: message }.to_json]]
43
+ end
44
+
45
+ def authentication_unavailable
46
+ [503,
47
+ { "content-type" => "application/json",
48
+ "retry-after" => PublicKeyCachedResolver::FAILED_REFRESH_RETRY_DELAY_IN_SECONDS.to_s },
49
+ [{ error: "Authentication is temporarily unavailable" }.to_json]]
41
50
  end
42
51
 
43
52
  def service
@@ -1,30 +1,90 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KeycloakApiRails
2
4
  class PublicKeyCachedResolver
3
- attr_reader :cached_public_key_retrieved_at
5
+ FAILED_REFRESH_RETRY_DELAY_IN_SECONDS = 10
4
6
 
5
- def initialize(http_client, realm_id, public_key_cache_ttl)
6
- @resolver = PublicKeyResolver.new(http_client, realm_id)
7
- @public_key_cache_ttl = public_key_cache_ttl
8
- @cached_public_keys = nil
9
- @cached_public_key_retrieved_at = nil
10
- end
7
+ class RealmCache
8
+ attr_reader :cached_public_key_retrieved_at
11
9
 
12
- def self.from_configuration(http_client, configuration)
13
- PublicKeyCachedResolver.new(http_client, configuration.realm_id, configuration.public_key_cache_ttl)
14
- end
10
+ def initialize(http_client, realm_id)
11
+ @resolver = PublicKeyResolver.new(http_client, realm_id)
12
+ @cached_public_keys = nil
13
+ @cached_public_key_retrieved_at = nil
14
+ @last_refresh_failure_at = nil
15
+ @last_refresh_error = nil
16
+ @mutex = Mutex.new
17
+ end
18
+
19
+ def find_public_keys(public_key_cache_ttl, logger)
20
+ @mutex.synchronize do
21
+ raise @last_refresh_error if refresh_recently_failed_without_any_cache?
15
22
 
16
- def find_public_keys
17
- if public_keys_are_outdated?
23
+ refresh_public_keys(logger) if public_keys_are_outdated?(public_key_cache_ttl)
24
+ @cached_public_keys
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def refresh_public_keys(logger)
18
31
  @cached_public_keys = @resolver.find_public_keys
19
32
  @cached_public_key_retrieved_at = Time.now
33
+ @last_refresh_failure_at = nil
34
+ @last_refresh_error = nil
35
+ rescue StandardError => e
36
+ @last_refresh_failure_at = Time.now
37
+ @last_refresh_error = e
38
+ raise if @cached_public_keys.nil?
39
+
40
+ logger&.warn("KeycloakApiRails: could not refresh the public keys (#{e.class}: #{e.message}). Keeping the ones retrieved at #{@cached_public_key_retrieved_at}.")
41
+ end
42
+
43
+ def refresh_recently_failed_without_any_cache?
44
+ @cached_public_keys.nil? &&
45
+ !@last_refresh_failure_at.nil? &&
46
+ Time.now <= @last_refresh_failure_at + FAILED_REFRESH_RETRY_DELAY_IN_SECONDS
20
47
  end
21
- @cached_public_keys
48
+
49
+ def public_keys_are_outdated?(public_key_cache_ttl)
50
+ @cached_public_keys.nil? ||
51
+ @cached_public_key_retrieved_at.nil? ||
52
+ (Time.now > @cached_public_key_retrieved_at + public_key_cache_ttl &&
53
+ (@last_refresh_failure_at.nil? ||
54
+ Time.now > @last_refresh_failure_at + FAILED_REFRESH_RETRY_DELAY_IN_SECONDS))
55
+ end
56
+ end
57
+
58
+ def initialize(http_client, realm_id, public_key_cache_ttl, logger = nil)
59
+ @http_client = http_client
60
+ @realm_id = realm_id
61
+ @public_key_cache_ttl = public_key_cache_ttl
62
+ @logger = logger
63
+ @caches = {}
64
+ @caches_mutex = Mutex.new
65
+ end
66
+
67
+ def self.from_configuration(http_client, configuration)
68
+ new(http_client, configuration.realm_id, configuration.public_key_cache_ttl, configuration.logger)
69
+ end
70
+
71
+ def find_public_keys(realm_id = nil)
72
+ target_realm = realm_id || @realm_id
73
+ cache_for(target_realm).find_public_keys(@public_key_cache_ttl, @logger)
74
+ end
75
+
76
+ # Keep this method backward-compatible for testing
77
+ def cached_public_key_retrieved_at(realm_id = nil)
78
+ target_realm = realm_id || @realm_id
79
+ cache_for(target_realm).cached_public_key_retrieved_at
22
80
  end
23
81
 
24
82
  private
25
83
 
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)
84
+ def cache_for(realm_id)
85
+ @caches_mutex.synchronize do
86
+ @caches[realm_id] ||= RealmCache.new(@http_client, realm_id)
87
+ end
28
88
  end
29
89
  end
30
90
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KeycloakApiRails
2
4
  class PublicKeyResolver
3
5
  def initialize(http_client, realm_id)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KeycloakApiRails
2
4
  class Railtie < Rails::Railtie
3
5
  railtie_name :keycloak_api_rails
@@ -5,5 +7,20 @@ module KeycloakApiRails
5
7
  initializer("keycloak.insert_middleware") do |app|
6
8
  app.config.middleware.use(KeycloakApiRails::Middleware)
7
9
  end
10
+
11
+ # Runs once every initializer has run, config/initializers/keycloak.rb included, so that a
12
+ # misconfiguration fails at boot instead of on the first request reaching the middleware.
13
+ config.after_initialize do
14
+ keycloak_configuration = KeycloakApiRails.config
15
+ keycloak_configuration.validate!
16
+
17
+ unless keycloak_configuration.server_configured?
18
+ keycloak_configuration.logger.warn(
19
+ "KeycloakApiRails: 'server_url' and 'realm_id' are not both configured. No token can be " \
20
+ "verified until they are, unless the public key resolver is replaced -- as " \
21
+ "\"keycloak-api-rails/testing\" does."
22
+ )
23
+ end
24
+ end
8
25
  end
9
- end
26
+ end
@@ -1,38 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
1
4
  module KeycloakApiRails
5
+ class MissingPublicKeysError < StandardError; end
6
+
2
7
  class Service
3
-
8
+
4
9
  def initialize(key_resolver)
10
+ configuration = KeycloakApiRails.config
5
11
  @key_resolver = key_resolver
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
12
+ @skip_paths = normalize_skip_paths(configuration.skip_paths, configuration.logger)
13
+ @opt_in = configuration.opt_in
14
+ @token_expiration_tolerance_in_seconds = configuration.token_expiration_tolerance_in_seconds
15
+ @expected_audiences = Array(configuration.expected_audience).map(&:to_s)
16
+ @expected_token_type = configuration.expected_token_type
17
+ @verify_not_before = configuration.verify_not_before
18
+ @allow_token_in_query_string = configuration.allow_token_in_query_string
10
19
  end
11
20
 
12
21
  def decode_and_verify(token)
13
- unless token.nil? || token&.empty?
14
- public_key = @key_resolver.find_public_keys
15
- decoded_token = JSON::JWT.decode(token, public_key)
16
-
17
- unless expired?(decoded_token)
18
- decoded_token.verify!(public_key)
19
- decoded_token
20
- else
21
- raise TokenError.expired(token)
22
- end
22
+ raise TokenError.no_token(token) if token.nil? || token.empty?
23
+
24
+ parts = token.to_s.split('.')
25
+ raise TokenError.invalid_format(token) if parts.length < 3
26
+
27
+ realm_id = extract_realm_from_token(token)
28
+ raise TokenError.invalid_realm(token) unless realm_allowed?(realm_id)
29
+
30
+ public_keys = @key_resolver.find_public_keys(realm_id)
31
+
32
+ if public_keys.nil?
33
+ raise MissingPublicKeysError, "No Keycloak public key is available to verify the token"
34
+ end
35
+
36
+ decoded_token = decode(token, public_keys)
37
+ verify_claims!(token, decoded_token, realm_id)
38
+ decoded_token
39
+ end
40
+
41
+ def extract_realm_from_token(token)
42
+ payload_segment = token.split('.', 3)[1]
43
+ return nil unless payload_segment
44
+
45
+ decoded_payload = Base64.urlsafe_decode64(payload_segment)
46
+ parsed_payload = JSON.parse(decoded_payload)
47
+ iss = parsed_payload['iss']
48
+ return nil unless iss
49
+
50
+ iss.split('/').last
51
+ rescue JSON::ParserError, ArgumentError
52
+ nil
53
+ end
54
+
55
+ def realm_allowed?(realm_id)
56
+ config_realm_id = KeycloakApiRails.config.realm_id
57
+ return true if config_realm_id.nil?
58
+ return false if realm_id.nil?
59
+
60
+ if config_realm_id.respond_to?(:call)
61
+ config_realm_id.call(realm_id)
62
+ elsif config_realm_id.is_a?(Array)
63
+ config_realm_id.include?(realm_id)
23
64
  else
24
- raise TokenError.no_token(token)
65
+ config_realm_id == realm_id
25
66
  end
26
- rescue JSON::JWT::VerificationFailed => e
27
- raise TokenError.verification_failed(token, e)
28
- rescue JSON::JWK::Set::KidNotFound => e
29
- raise TokenError.verification_failed(token, e)
30
- rescue JSON::JWT::InvalidFormat
31
- raise TokenError.invalid_format(token, e)
32
67
  end
33
68
 
34
69
  def read_token(uri, headers)
35
- Helper.read_token_from_query_string(uri) || Helper.read_token_from_headers(headers)
70
+ header_token = Helper.read_token_from_headers(headers)
71
+ if !header_token.empty?
72
+ header_token
73
+ elsif @allow_token_in_query_string
74
+ Helper.read_token_from_query_string(uri).to_s
75
+ else
76
+ ""
77
+ end
36
78
  end
37
79
 
38
80
  def need_middleware_authentication?(method, path, headers)
@@ -41,20 +83,86 @@ module KeycloakApiRails
41
83
 
42
84
  private
43
85
 
86
+ def decode(token, public_keys)
87
+ decoded_token = JSON::JWT.decode(token, public_keys)
88
+ decoded_token.verify!(public_keys)
89
+ decoded_token
90
+ rescue JSON::JWT::VerificationFailed, JSON::JWK::Set::KidNotFound => e
91
+ raise TokenError.verification_failed(token, e)
92
+ rescue JSON::JWT::InvalidFormat => e
93
+ raise TokenError.invalid_format(token, e)
94
+ rescue StandardError => e
95
+ raise TokenError.unknown(token, e)
96
+ end
97
+
98
+ # RFC 7519 requires 'exp' and 'nbf' to be NumericDates.
99
+ def verify_claims!(token, decoded_token, realm_id)
100
+ raise TokenError.missing_claim(token, "exp") unless decoded_token.key?("exp")
101
+ raise TokenError.invalid_claim(token, "exp") unless decoded_token["exp"].is_a?(Numeric)
102
+ raise TokenError.invalid_claim(token, "nbf") if not_before_is_invalid?(decoded_token)
103
+ raise TokenError.expired(token) if expired?(decoded_token)
104
+ raise TokenError.not_yet_valid(token) if not_yet_valid?(decoded_token)
105
+ raise TokenError.invalid_audience(token) unless audience_valid?(decoded_token)
106
+ raise TokenError.invalid_token_type(token) unless token_type_valid?(decoded_token)
107
+
108
+ if KeycloakApiRails.config.server_url
109
+ expected_iss = File.join(KeycloakApiRails.config.server_url.to_s, "realms", realm_id.to_s)
110
+ raise TokenError.invalid_realm(token) unless decoded_token["iss"] == expected_iss
111
+ end
112
+ end
113
+
114
+ def not_before_is_invalid?(token)
115
+ @verify_not_before && token.key?("nbf") && !token["nbf"].is_a?(Numeric)
116
+ end
117
+
118
+ # Anything that is not a regexp is discarded rather than matched: 'String#match' compiles its
119
+ # argument into a regexp, so a String would be matched against the path of the request instead of
120
+ # the other way around, and would open every path that is a sub-pattern of it. The railtie rejects
121
+ # such a configuration when the application boots; a Rack application running without Rails never
122
+ # calls 'validate!', so the paths keep being authenticated here.
123
+ def normalize_skip_paths(skip_paths, logger)
124
+ (skip_paths || {}).each_with_object({}) do |(method, paths), normalized|
125
+ regexps, discarded = Array(paths).partition { |path| path.is_a?(Regexp) }
126
+
127
+ unless discarded.empty?
128
+ logger&.warn("KeycloakApiRails: 'skip_paths[#{method.inspect}]' declares #{discarded.map(&:inspect).join(', ')}, which are not regexps. They are ignored, and the paths they were meant to open keep being authenticated.")
129
+ end
130
+
131
+ normalized[method.to_s.upcase] = regexps
132
+ end
133
+ end
134
+
44
135
  def should_skip?(method, path)
45
- method_symbol = method&.downcase&.to_sym
46
- skip_paths = @skip_paths[method_symbol]
47
- !skip_paths.nil? && !skip_paths.empty? && !skip_paths.find_index { |skip_path| skip_path.match(path) }.nil?
136
+ skip_paths = @skip_paths[method]
137
+ !skip_paths.nil? && skip_paths.any? { |skip_path| skip_path.match?(path) }
48
138
  end
49
139
 
50
140
  def is_preflight?(method, headers)
51
- method_symbol = method&.downcase&.to_sym
52
- method_symbol == :options && !headers["HTTP_ACCESS_CONTROL_REQUEST_METHOD"].nil?
141
+ method == "OPTIONS" && !headers["HTTP_ACCESS_CONTROL_REQUEST_METHOD"].nil?
53
142
  end
54
143
 
55
144
  def expired?(token)
56
145
  token_expiration = Time.at(token["exp"])
57
146
  token_expiration < Time.now + @token_expiration_tolerance_in_seconds
58
147
  end
148
+
149
+ def not_yet_valid?(token)
150
+ return false unless @verify_not_before
151
+
152
+ not_before = token["nbf"]
153
+ !not_before.nil? && Time.at(not_before) > Time.now
154
+ end
155
+
156
+ def audience_valid?(token)
157
+ return true if @expected_audiences.empty?
158
+
159
+ Array(token["aud"]).any? { |audience| @expected_audiences.include?(audience.to_s) }
160
+ end
161
+
162
+ def token_type_valid?(token)
163
+ return true if @expected_token_type.nil?
164
+
165
+ token["typ"].to_s.casecmp?(@expected_token_type)
166
+ end
59
167
  end
60
168
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Test helpers for the applications that authenticate their requests with this library.
2
4
  #
3
5
  # This file is *not* loaded by "keycloak-api-rails": it has to be required explicitly, so that
@@ -33,24 +35,26 @@ module KeycloakApiRails
33
35
  @public_keys = public_keys
34
36
  end
35
37
 
36
- def find_public_keys
38
+ def find_public_keys(realm_id = nil)
37
39
  @public_keys
38
40
  end
39
41
  end
40
42
 
43
+ MONITOR = Monitor.new
44
+
41
45
  class << self
42
46
  # The RSA key pair used to sign the tokens forged by this module. It is generated once per
43
47
  # process: generating a key is by far the slowest operation of a test suite that uses tokens.
44
48
  def private_key
45
- @private_key ||= OpenSSL::PKey::RSA.generate(KEY_SIZE)
49
+ MONITOR.synchronize { @private_key ||= OpenSSL::PKey::RSA.generate(KEY_SIZE) }
46
50
  end
47
51
 
48
52
  def signing_key
49
- @signing_key ||= JSON::JWK.new(private_key, kid: KEY_ID)
53
+ MONITOR.synchronize { @signing_key ||= JSON::JWK.new(private_key, kid: KEY_ID) }
50
54
  end
51
55
 
52
56
  def public_keys
53
- @public_keys ||= JSON::JWK::Set.new(JSON::JWK.new(private_key.public_key, kid: KEY_ID))
57
+ MONITOR.synchronize { @public_keys ||= JSON::JWK::Set.new(JSON::JWK.new(private_key.public_key, kid: KEY_ID)) }
54
58
  end
55
59
 
56
60
  # Makes the library validate the tokens forged by this module. Assigning
@@ -83,6 +87,12 @@ module KeycloakApiRails
83
87
  payload["realm_access"] = { "roles" => roles.map(&:to_s) } unless roles.nil? || roles.empty?
84
88
  payload["resource_access"] = build_resource_access(resource_roles) unless resource_roles.nil? || resource_roles.empty?
85
89
 
90
+ unless claims.key?(:iss) || claims.key?("iss")
91
+ config_realm_id = KeycloakApiRails.config.realm_id
92
+ realm_id = config_realm_id.is_a?(String) ? config_realm_id : "master"
93
+ payload["iss"] = File.join(KeycloakApiRails.config.server_url.to_s, "realms", realm_id)
94
+ end
95
+
86
96
  claims.each { |name, value| payload[name.to_s] = value }
87
97
 
88
98
  JSON::JWT.new(payload).sign(signing_key, ALGORITHM).to_s
@@ -1,30 +1,58 @@
1
- class TokenError < StandardError
2
- attr_reader :token, :reason, :original_error
3
-
4
- def initialize(token, reason, message, original_error)
5
- super(message)
6
- @token = token
7
- @reason = reason
8
- @original_error = original_error
9
- end
1
+ # frozen_string_literal: true
10
2
 
11
- def self.verification_failed(token, original_error)
12
- TokenError.new(token, :verification_failed, "Failed to verify JWT token", original_error)
13
- end
3
+ module KeycloakApiRails
4
+ class TokenError < StandardError
5
+ attr_reader :token, :reason, :original_error
14
6
 
15
- def self.invalid_format(token, original_error)
16
- TokenError.new(token, :invalid_format, "Wrong JWT Format", original_error)
17
- end
7
+ def initialize(token, reason, message, original_error = nil)
8
+ super(message)
9
+ @token = token
10
+ @reason = reason
11
+ @original_error = original_error
12
+ end
18
13
 
19
- def self.no_token(token)
20
- TokenError.new(token, :no_token, "No JWT token provided", nil)
21
- end
14
+ def self.verification_failed(token, original_error)
15
+ new(token, :verification_failed, "Failed to verify JWT token", original_error)
16
+ end
22
17
 
23
- def self.expired(token)
24
- TokenError.new(token, :expired, "JWT token is expired", nil)
25
- end
18
+ def self.invalid_format(token, original_error = nil)
19
+ new(token, :invalid_format, "Wrong JWT Format", original_error)
20
+ end
21
+
22
+ def self.invalid_realm(token)
23
+ new(token, :invalid_realm, "JWT token does not have a valid realm")
24
+ end
25
+
26
+ def self.no_token(token)
27
+ new(token, :no_token, "No JWT token provided")
28
+ end
29
+
30
+ def self.expired(token)
31
+ new(token, :expired, "JWT token is expired")
32
+ end
33
+
34
+ def self.not_yet_valid(token)
35
+ new(token, :not_yet_valid, "JWT token is not valid yet")
36
+ end
37
+
38
+ def self.invalid_audience(token)
39
+ new(token, :invalid_audience, "JWT token has been issued for another audience")
40
+ end
41
+
42
+ def self.invalid_token_type(token)
43
+ new(token, :invalid_token_type, "JWT token is not of the expected type")
44
+ end
45
+
46
+ def self.missing_claim(token, claim)
47
+ new(token, :missing_claim, "JWT token does not carry the mandatory claim '#{claim}'")
48
+ end
49
+
50
+ def self.invalid_claim(token, claim)
51
+ new(token, :invalid_claim, "JWT token carries an invalid '#{claim}' claim: it must be a number of seconds since the Epoch")
52
+ end
26
53
 
27
- def self.unknown(token)
28
- TokenError.new
54
+ def self.unknown(token, original_error)
55
+ new(token, :unknown, "Failed to read JWT token", original_error)
56
+ end
29
57
  end
30
58
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KeycloakApiRails
2
- VERSION = "1.1.2"
4
+ VERSION = "2.0.2"
3
5
  end
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "logger"
2
4
  require "json/jwt"
3
5
  require "uri"
4
6
  require "date"
7
+ require "monitor"
5
8
  require "net/http"
6
9
 
7
10
  require_relative "keycloak-api-rails/authentication"
@@ -17,32 +20,50 @@ require_relative "keycloak-api-rails/railtie" if defined?(Rails)
17
20
 
18
21
  module KeycloakApiRails
19
22
 
23
+ # These objects are memoized lazily, on the first request each process serves -- which several
24
+ # threads of a threaded server reach at the same time. A Monitor rather than a Mutex: the
25
+ # memoizations nest, 'service' needing 'public_key_resolver', which needs 'http_client'.
26
+ MONITOR = Monitor.new
27
+
20
28
  def self.configure
21
- yield @configuration ||= KeycloakApiRails::Configuration.new
29
+ MONITOR.synchronize do
30
+ yield @configuration ||= KeycloakApiRails::Configuration.new
31
+ discard_configured_objects
32
+ end
33
+ end
34
+
35
+ def self.discard_configured_objects
36
+ @http_client = nil
37
+ @public_key_resolver = nil unless @public_key_resolver_assigned
38
+ @service = nil
22
39
  end
40
+ private_class_method :discard_configured_objects
23
41
 
24
42
  def self.config
25
43
  @configuration
26
44
  end
27
45
 
28
46
  def self.http_client
29
- @http_client ||= KeycloakApiRails::HTTPClient.new(config, logger)
47
+ MONITOR.synchronize { @http_client ||= KeycloakApiRails::HTTPClient.new(config, logger) }
30
48
  end
31
49
 
32
50
  def self.public_key_resolver
33
- @public_key_resolver ||= PublicKeyCachedResolver.from_configuration(http_client, config)
51
+ MONITOR.synchronize { @public_key_resolver ||= PublicKeyCachedResolver.from_configuration(http_client, config) }
34
52
  end
35
53
 
36
54
  # Mainly used by "keycloak-api-rails/testing" to validate tokens without a Keycloak server.
37
55
  # Assigning nil restores the regular resolver. The memoized service is discarded, since it holds
38
56
  # a reference to the resolver that is being replaced.
39
57
  def self.public_key_resolver=(resolver)
40
- @public_key_resolver = resolver
41
- @service = nil
58
+ MONITOR.synchronize do
59
+ @public_key_resolver = resolver
60
+ @public_key_resolver_assigned = !resolver.nil?
61
+ @service = nil
62
+ end
42
63
  end
43
64
 
44
65
  def self.service
45
- @service ||= KeycloakApiRails::Service.new(public_key_resolver)
66
+ MONITOR.synchronize { @service ||= KeycloakApiRails::Service.new(public_key_resolver) }
46
67
  end
47
68
 
48
69
  def self.logger
@@ -59,6 +80,13 @@ module KeycloakApiRails
59
80
  config.token_expiration_tolerance_in_seconds = 10
60
81
  config.public_key_cache_ttl = 86400
61
82
  config.custom_attributes = []
83
+ config.ca_certificate_file = nil
84
+ config.expected_audience = nil
85
+ config.expected_token_type = nil
86
+ config.verify_not_before = false
87
+ config.allow_token_in_query_string = false
88
+ config.http_open_timeout = 5
89
+ config.http_read_timeout = 5
62
90
  end
63
91
  end
64
92