verikloak 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d175a574b17bd1ce5e096dbb69769d12a470f49c8444a5246881afd61d06fa52
4
- data.tar.gz: 3c59a660bb142c194b6e0db2d338952c41c8a88afc479382203b4d767ab7691c
3
+ metadata.gz: 24bc2b5eb7f699c9f8bc4970a2b0b6cdca87b7c7e37d9718c51d85d8cf58a8cc
4
+ data.tar.gz: 439ec2fb34a67140476aff243addfaac8cb8d5ac18ef7b6915d3a9ba6dc5947d
5
5
  SHA512:
6
- metadata.gz: 92edd8a2df134f115c3f887683246278c5778291bb86bfff22ebc873b2b92d6ff09d9c85a1f798e2f49caa09adc8c3ef18ffc83309f95799997ebbdb54b58228
7
- data.tar.gz: 0e223f0055e69448899be53fc22c9ac32c5d43c410aa041daa38761ed13a00aae8d655bcb6eaff521927a5a029937fcd480c7b90dcfccddf8e6b028aaf0bf555
6
+ metadata.gz: 61be82820e149b89c7e5dead4bc79aef1da0959b85ba447e7bf9d6a346efb331f378030620c2ee42d2b41e5c3e21d89346f405ad16b6264f9452fc49771fc2d4
7
+ data.tar.gz: 9bec7c747971154c59321505fdd2817487e09013eb8e32d3defe86a7dc38a4731c5e01d113639cd3eae1a33e9073b932dfa5d9a243b65a1609d9b3549cb48129
data/CHANGELOG.md CHANGED
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.3.0] - 2025-12-31
11
+
12
+ ### Added
13
+ - **NEW**: `issuer` parameter for `Middleware#initialize` to optionally override the discovered issuer
14
+ - When provided, the configured issuer takes precedence over the OIDC discovery document's issuer
15
+ - This enables compatibility with `verikloak-rails` which passes `issuer` from configuration
16
+ - If not provided, the middleware continues to use the issuer from OIDC discovery (existing behavior)
17
+
18
+ ### Changed
19
+ - Internal issuer handling now distinguishes between `@configured_issuer` (user-provided) and `@issuer` (discovered/effective)
20
+ - When `jwks_cache` is injected, discovery is only fetched once (and skipped entirely if `issuer` is provided)
21
+
22
+ ## [0.2.1] - 2025-09-23
23
+
24
+ ### Changed
25
+ - **BREAKING**: `JwksCache` is now thread-safe with Mutex synchronization around all cache operations
26
+ - Middleware code organization: split large modules into focused, single-responsibility components:
27
+ - `SkipPathMatcher`: Path matching and normalization logic
28
+ - `MiddlewareAudienceResolution`: Audience resolution with dynamic callable support
29
+ - `MiddlewareConfiguration`: Configuration validation and logging utilities
30
+ - `MiddlewareDecoderCache`: LRU cache management for TokenDecoder instances
31
+ - `MiddlewareTokenVerification`: JWT verification and JWKs management
32
+ - `MiddlewareErrorMapping`: Error-to-HTTP status code mapping
33
+
34
+ ### Fixed
35
+ - Removed duplicate method definitions that were causing code bloat
36
+ - Audience callable parameter detection now handles edge cases more reliably
37
+ - Thread-safety issues in concurrent environments resolved
38
+
10
39
  ## [0.2.0] - 2025-09-22
11
40
 
12
41
  ### Added
@@ -17,8 +46,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
17
46
  ### Changed
18
47
  - Update gem version to 0.2.0 to stay aligned with the rest of the Verikloak ecosystem gems.
19
48
 
20
- ---
21
-
22
49
  ## [0.1.5] - 2025-09-21
23
50
 
24
51
  ### Added
@@ -31,15 +58,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
31
58
  ### Dependencies
32
59
  - Declare `faraday-retry` as a runtime dependency so the default HTTP connection can load the retry middleware.
33
60
 
34
- ---
35
-
36
61
  ## [0.1.4] - 2025-09-20
37
62
 
38
63
  ### Chore
39
64
  - Bump dev dependency `rexml` to 3.4.2 (PR #15).
40
65
 
41
- ---
42
-
43
66
  ## [0.1.3] - 2025-09-15
44
67
 
45
68
  ### Changed
@@ -49,8 +72,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
49
72
  - Bump dev dependency `rubocop` to 1.80.2 (PR #13).
50
73
  - Bump dev dependency `rubocop-rspec` to 3.7.0 (PR #12).
51
74
 
52
- ---
53
-
54
75
  ## [0.1.2] - 2025-08-31
55
76
 
56
77
  ### Added
@@ -63,8 +84,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
63
84
  - Middleware: TokenDecoder instances are now cached per JWKs fetch for performance improvement.
64
85
  - Internal: RuboCop style fixes (`HashExcept`, `HashTransformKeys`, long line splits).
65
86
 
66
- ---
67
-
68
87
  ## [0.1.1] - 2025-08-24
69
88
 
70
89
  ### Changed
@@ -72,8 +91,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
72
91
  - Updated dependency constraints in gemspec (`json` ~> 2.6, `jwt` ~> 2.7) for better compatibility control
73
92
  - Updated README badges (Gem version, Ruby version, downloads)
74
93
 
75
- ---
76
-
77
94
  ## [0.1.0] - 2025-08-17
78
95
 
79
96
  ### Added
data/README.md CHANGED
@@ -41,10 +41,42 @@ Add the middleware in `config/application.rb`:
41
41
  ```ruby
42
42
  config.middleware.use Verikloak::Middleware,
43
43
  discovery_url: "https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration",
44
- audience: "your-client-id",
45
- skip_paths: ['/skip_path']
44
+ audience: "your-client-id"
46
45
  ```
47
46
 
47
+ For production environments, use environment variables:
48
+
49
+ ```ruby
50
+ config.middleware.use Verikloak::Middleware,
51
+ discovery_url: ENV.fetch("DISCOVERY_URL"),
52
+ audience: ENV.fetch("CLIENT_ID"),
53
+ skip_paths: ['/', '/health', '/public/*', '/rails/*']
54
+ ```
55
+
56
+ #### Advanced configuration
57
+
58
+ For more complex setups, Verikloak supports additional configuration options:
59
+
60
+ ```ruby
61
+ config.middleware.use Verikloak::Middleware,
62
+ discovery_url: ENV.fetch("DISCOVERY_URL"),
63
+ audience: ENV.fetch("CLIENT_ID"),
64
+ issuer: "https://custom.issuer.example.com/realms/myrealm", # Optional: override discovered issuer
65
+ skip_paths: ['/', '/health', '/public/*', '/rails/*'],
66
+ leeway: 30, # Allow 30 seconds clock skew
67
+ token_env_key: "rack.session.token", # Custom token storage key
68
+ user_env_key: "rack.session.claims", # Custom user claims storage key
69
+ realm: "my-api", # Custom realm for WWW-Authenticate header
70
+ logger: Rails.logger # Logger for internal errors
71
+ ```
72
+
73
+ **Additional middleware options:**
74
+
75
+ - `token_env_key` (default: `"verikloak.token"`) — where the raw JWT is stored in the Rack env
76
+ - `user_env_key` (default: `"verikloak.user"`) — where decoded claims are stored
77
+ - `realm` (default: `"verikloak"`) — value used in the `WWW-Authenticate` header for 401 responses
78
+ - `logger` — an object responding to `error` (and optionally `debug`) that receives unexpected 500-level failures
79
+
48
80
  #### Handling Authentication Failures
49
81
 
50
82
  When you use the Rack middleware, authentication failures are automatically converted into JSON error responses (for example, `401` for token issues, `503` for JWKs/discovery errors). In most cases **you do not need to add custom `rescue_from` handlers** in Rails controllers.
@@ -84,40 +116,25 @@ All Verikloak errors inherit from `Verikloak::Error`:
84
116
  - `Verikloak::JwksCacheError` – JWKs fetch/parse/cache (`503 Service Unavailable`)
85
117
  - `Verikloak::MiddlewareError` – header/infra issues surfaced by the middleware (usually `401`, sometimes `503`)
86
118
 
87
- #### Recommended: use environment variables in production
119
+ ### Standalone Rack app
88
120
 
89
121
  ```ruby
90
- config.middleware.use Verikloak::Middleware,
91
- discovery_url: ENV.fetch("DISCOVERY_URL"),
92
- audience: ENV.fetch("CLIENT_ID"),
93
- skip_paths: ['/', '/health', '/public/*', '/rails/*']
94
- ```
95
- This makes the configuration secure and flexible across environments.
96
-
97
- #### Advanced middleware options
98
-
99
- `Verikloak::Middleware` exposes a few optional knobs that help integrate with
100
- different Rack stacks:
122
+ # config.ru example for a standalone Rack app
123
+ require "verikloak"
101
124
 
102
- - `token_env_key` (default: `"verikloak.token"`) — where the raw JWT is stored in the Rack env
103
- - `user_env_key` (default: `"verikloak.user"`) — where decoded claims are stored
104
- - `realm` (default: `"verikloak"`) — value used in the `WWW-Authenticate` header for 401 responses
105
- - `logger` — an object responding to `error` (and optionally `debug`) that receives unexpected 500-level failures
125
+ use Verikloak::Middleware,
126
+ discovery_url: "https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration",
127
+ audience: "my-client-id"
106
128
 
107
- ```ruby
108
- config.middleware.use Verikloak::Middleware,
109
- discovery_url: ENV.fetch("DISCOVERY_URL"),
110
- audience: ENV.fetch("CLIENT_ID"),
111
- token_env_key: "rack.session.token",
112
- user_env_key: "rack.session.claims",
113
- realm: "my-api",
114
- logger: Rails.logger
129
+ run ->(env) {
130
+ user = env["verikloak.user"] # Decoded JWT claims hash (if token is valid)
131
+ [200, { "Content-Type" => "application/json" }, [user.to_json]]
132
+ }
115
133
  ```
116
134
 
117
135
  ### Accessing claims in controllers
118
136
 
119
- Once the middleware is enabled, Verikloak adds the decoded token and raw JWT to the Rack environment.
120
- You can access them in any Rails controller:
137
+ Once the middleware is enabled, Verikloak adds the decoded token and raw JWT to the Rack environment:
121
138
 
122
139
  ```ruby
123
140
  class Api::V1::NotesController < ApplicationController
@@ -131,22 +148,6 @@ class Api::V1::NotesController < ApplicationController
131
148
  end
132
149
  ```
133
150
 
134
- ### Standalone Rack app
135
-
136
- ```ruby
137
- # config.ru example for a standalone Rack app
138
- require "verikloak"
139
-
140
- use Verikloak::Middleware,
141
- discovery_url: "https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration",
142
- audience: "my-client-id"
143
-
144
- run ->(env) {
145
- user = env["verikloak.user"] # Decoded JWT claims hash (if token is valid)
146
- [200, { "Content-Type" => "application/json" }, [user.to_json]]
147
- }
148
- ```
149
-
150
151
  ## How It Works
151
152
 
152
153
  1. Extracts the `Authorization: Bearer <token>` header
@@ -194,26 +195,7 @@ Verikloak returns JSON error responses in a consistent format with structured er
194
195
  }
195
196
  ```
196
197
 
197
- ```json
198
- {
199
- "error": "jwks_parse_failed",
200
- "message": "Failed to parse JWKs"
201
- }
202
- ```
203
-
204
- ```json
205
- {
206
- "error": "discovery_metadata_fetch_failed",
207
- "message": "Failed to fetch OIDC discovery document"
208
- }
209
- ```
210
-
211
- ```json
212
- {
213
- "error": "discovery_metadata_invalid",
214
- "message": "Failed to parse OIDC discovery document"
215
- }
216
- ```
198
+ For a full list of error cases and detailed explanations, please see the [ERRORS.md](ERRORS.md) file.
217
199
 
218
200
  ### Error Types
219
201
 
@@ -223,36 +205,40 @@ Verikloak returns JSON error responses in a consistent format with structured er
223
205
  | `expired_token` | 401 Unauthorized | The token has expired |
224
206
  | `missing_authorization_header` | 401 Unauthorized | The `Authorization` header is missing |
225
207
  | `invalid_authorization_header` | 401 Unauthorized | The `Authorization` header format is invalid |
226
- | `unsupported_algorithm` | 401 Unauthorized | The tokens signing algorithm is not supported |
208
+ | `unsupported_algorithm` | 401 Unauthorized | The token's signing algorithm is not supported |
227
209
  | `invalid_signature` | 401 Unauthorized | The token signature could not be verified |
228
210
  | `invalid_issuer` | 401 Unauthorized | Invalid `iss` claim |
229
211
  | `invalid_audience` | 401 Unauthorized | Invalid `aud` claim |
230
212
  | `not_yet_valid` | 401 Unauthorized | The token is not yet valid (`nbf` in the future) |
231
- | `jwks_fetch_failed` | 503 Service Unavailable | Failed to fetch JWKs |
232
- | `jwks_parse_failed` | 503 Service Unavailable | Failed to parse JWKs |
213
+ | `jwks_fetch_failed` | 503 Service Unavailable | Failed to fetch JWKs |
214
+ | `jwks_parse_failed` | 503 Service Unavailable | Failed to parse JWKs |
233
215
  | `jwks_cache_miss` | 503 Service Unavailable | JWKs cache is empty (e.g., 304 Not Modified without prior cache) |
234
216
  | `discovery_metadata_fetch_failed` | 503 Service Unavailable | Failed to fetch OIDC discovery document |
235
- | `discovery_metadata_invalid` | 503 Service Unavailable | Failed to parse OIDC discovery document |
217
+ | `discovery_metadata_invalid` | 503 Service Unavailable | Failed to parse OIDC discovery document |
236
218
  | `discovery_redirect_error` | 503 Service Unavailable | Discovery response was a redirect without a valid Location header |
237
219
  | `internal_server_error` | 500 Internal Server Error | Unexpected internal error (catch-all) |
238
220
 
239
221
  > **Note:** The `decode_with_public_key` method ensures consistent error codes for all JWT verification failures.
240
222
  > It may raise `invalid_signature`, `unsupported_algorithm`, `expired_token`, `invalid_issuer`, `invalid_audience`, or `not_yet_valid` depending on the verification outcome.
241
223
 
242
- For a full list of error cases and detailed explanations, please see the [ERRORS.md](ERRORS.md) file.
243
-
244
224
  ## Configuration Options
245
225
 
246
226
  | Key | Required | Description |
247
227
  | --------------- | -------- | ------------------------------------------- |
248
228
  | `discovery_url` | Yes | Full URL to your realm's OIDC discovery doc |
249
229
  | `audience` | Yes | Your client ID (checked against `aud`). Accepts a String or callable returning a String/Array per request. |
230
+ | `issuer` | No | Optional override for the expected `iss` claim; defaults to the discovery `issuer`. |
250
231
  | `skip_paths` | No | Array of paths or wildcards to skip authentication, e.g. `['/', '/health', '/public/*']`. **Note:** Regex patterns are not supported. |
251
232
  | `discovery` | No | Inject custom Discovery instance (advanced/testing) |
252
233
  | `jwks_cache` | No | Inject custom JwksCache instance (advanced/testing) |
253
234
  | `leeway` | No | Clock skew tolerance (seconds) applied during JWT verification. Defaults to `TokenDecoder::DEFAULT_LEEWAY`. |
254
235
  | `token_verify_options` | No | Hash of advanced JWT verification options passed through to TokenDecoder. For example: `{ verify_iat: false, leeway: 10, algorithms: ["RS256"] }`. If both `leeway:` and `token_verify_options[:leeway]` are set, the latter takes precedence. |
236
+ | `decoder_cache_limit` | No | Maximum number of `TokenDecoder` instances retained per middleware. Defaults to `128`. Set to `0` to disable caching or `nil` for an unlimited cache. |
255
237
  | `connection` | No | Inject a Faraday::Connection used for both Discovery and JWKs fetches. Defaults to a safe connection with timeouts and retries. |
238
+ | `token_env_key` | No | Rack env key for the raw JWT. Defaults to `verikloak.token`. |
239
+ | `user_env_key` | No | Rack env key for decoded claims. Defaults to `verikloak.user`. |
240
+ | `realm` | No | Value used in the `WWW-Authenticate` header. Defaults to `verikloak`. |
241
+ | `logger` | No | Logger for unexpected internal failures (responds to `error`, optionally `debug`). |
256
242
 
257
243
  #### Option: `skip_paths`
258
244
 
@@ -272,11 +258,11 @@ skip_paths: ['/', '/health', '/rails/*', '/public/src']
272
258
  Paths **not matched** by any `skip_paths` entry will require a valid JWT.
273
259
 
274
260
  **Note:** Regex patterns are not supported. Only literal paths and `*` wildcards are allowed.
275
- Internally, `*` expands to match nested paths, so patterns like `/rails/*` are valid. This differs from regex for example, `'/rails'` alone matches only `/rails`, while `'/rails/*'` covers both `/rails` and deeper subpaths.
261
+ Internally, `*` expands to match nested paths, so patterns like `/rails/*` are valid. This differs from regex; for example, `'/rails'` alone matches only `/rails`, while `'/rails/*'` covers both `/rails` and deeper subpaths.
276
262
 
277
263
  #### Option: `audience`
278
264
 
279
- The `audience` option may be either a static String or any callable object (Proc, lambda, object responding to `#call`). When a callable is provided it receives the Rack `env` and can return a different audience for each request. This is useful when a single gateway serves multiple downstream clients:
265
+ The `audience` option may be either a static String or any callable object (Proc, lambda, object responding to `#call`). When a callable is provided it receives the Rack `env` (either as a positional argument or keyword `env:`) and can return a different audience for each request. This is useful when a single gateway serves multiple downstream clients:
280
266
 
281
267
  ```ruby
282
268
  Verikloak::Middleware.new(app,
@@ -342,11 +328,39 @@ config.middleware.use Verikloak::Middleware,
342
328
  - `token_verify_options:` is passed directly to TokenDecoder (and ultimately to `JWT.decode`).
343
329
  - If both are set, `token_verify_options[:leeway]` takes precedence.
344
330
 
345
- #### Performance note
331
+ ## Performance & Caching
332
+
333
+ #### Decoder cache & performance
346
334
 
347
335
  Internally, Verikloak caches `TokenDecoder` instances per JWKs fetch to avoid reinitializing
348
- them on every request. This improves performance while still ensuring that keys are
349
- revalidated when JWKs is refreshed.
336
+ them on every request. The cache behaves like an LRU with a configurable size (`decoder_cache_limit`)
337
+ so long-running processes do not accumulate decoders for one-off audiences. When the underlying
338
+ JWK set rotates, the middleware now clears the cache to drop decoders that point at stale keys.
339
+
340
+ Set `decoder_cache_limit` to `0` if you prefer to construct a fresh decoder every time, or `nil`
341
+ when you want the cache to grow without bounds (e.g., in short-lived jobs).
342
+
343
+ #### Sharing caches across verikloak gems
344
+
345
+ When combining `verikloak` with companion gems (such as `verikloak-rails`, `verikloak-bff`, etc.),
346
+ reusing infrastructure objects avoids redundant HTTP calls:
347
+
348
+ ```ruby
349
+ connection = Verikloak::HTTP.default_connection
350
+ jwks_cache = Verikloak::JwksCache.new(jwks_uri: ENV['JWKS_URI'], connection: connection)
351
+
352
+ use Verikloak::Middleware,
353
+ discovery_url: ENV['DISCOVERY_URL'],
354
+ audience: ->(env) { env['verikloak.audience'] },
355
+ connection: connection,
356
+ jwks_cache: jwks_cache
357
+
358
+ # Rails initializer or service object can now reuse `connection` and `jwks_cache`
359
+ # when configuring other verikloak-* gems.
360
+ ```
361
+
362
+ Sharing the Faraday connection keeps retry/time-out policies consistent, while a single `JwksCache`
363
+ ensures all middleware layers refresh keys in unison.
350
364
 
351
365
  ## Architecture
352
366
 
@@ -50,6 +50,7 @@ module Verikloak
50
50
  @etag = nil
51
51
  @fetched_at = nil
52
52
  @max_age = nil
53
+ @mutex = Mutex.new
53
54
  end
54
55
 
55
56
  # Fetches the JWKs and updates the in-memory cache.
@@ -61,27 +62,31 @@ module Verikloak
61
62
  # @return [Array&lt;Hash&gt;] the cached JWKs after fetch/revalidation
62
63
  # @raise [JwksCacheError] on HTTP failures, invalid JSON, invalid structure, or cache miss on 304
63
64
  def fetch!
64
- return @cached_keys if fresh_by_ttl?
65
+ @mutex.synchronize do
66
+ return @cached_keys if fresh_by_ttl_locked?
65
67
 
66
- with_error_handling do
67
- # Build conditional request headers (ETag-based)
68
- headers = build_conditional_headers
69
- # Perform HTTP GET request
70
- response = @connection.get(@jwks_uri, nil, headers)
71
- # Handle HTTP response according to status code
72
- handle_response(response)
68
+ with_error_handling do
69
+ # Build conditional request headers (ETag-based)
70
+ headers = build_conditional_headers
71
+ # Perform HTTP GET request
72
+ response = @connection.get(@jwks_uri, nil, headers)
73
+ # Handle HTTP response according to status code
74
+ handle_response(response)
75
+ end
73
76
  end
74
77
  end
75
78
 
76
79
  # Returns the last cached JWKs without performing a network request.
77
80
  # @return [Array&lt;Hash&gt;, nil] cached keys, or nil if never fetched
78
81
  def cached
79
- @cached_keys
82
+ @mutex.synchronize { @cached_keys }
80
83
  end
81
84
 
82
85
  # Timestamp of the last successful fetch or revalidation.
83
86
  # @return [Time, nil]
84
- attr_reader :fetched_at
87
+ def fetched_at
88
+ @mutex.synchronize { @fetched_at }
89
+ end
85
90
 
86
91
  # Injected Faraday connection (for testing and shared config across the gem)
87
92
  # @return [Faraday::Connection]
@@ -94,7 +99,7 @@ module Verikloak
94
99
  #
95
100
  # @return [Boolean]
96
101
  def stale?
97
- !fresh_by_ttl?
102
+ @mutex.synchronize { !fresh_by_ttl_locked? }
98
103
  end
99
104
 
100
105
  # @api private
@@ -125,6 +130,12 @@ module Verikloak
125
130
  # True when cached keys are still fresh per `Cache-Control: max-age`.
126
131
  # @return [Boolean]
127
132
  def fresh_by_ttl?
133
+ @mutex.synchronize { fresh_by_ttl_locked? }
134
+ end
135
+
136
+ private
137
+
138
+ def fresh_by_ttl_locked?
128
139
  return false unless @cached_keys && @fetched_at && @max_age
129
140
 
130
141
  (Time.now - @fetched_at) < @max_age
@@ -86,6 +86,299 @@ module Verikloak
86
86
  end
87
87
  end
88
88
 
89
+ # @api private
90
+ #
91
+ # Internal mixin for audience resolution with dynamic callable support.
92
+ # Handles various callable signatures and parameter detection.
93
+ module MiddlewareAudienceResolution
94
+ private
95
+
96
+ # Resolves the expected audience for the current request.
97
+ #
98
+ # @param env [Hash] Rack environment.
99
+ # @return [String, Array<String>] The expected audience value.
100
+ # @raise [MiddlewareError] when the resolved audience is blank.
101
+ def resolve_audience(env)
102
+ source = @audience_source
103
+ value = if source.respond_to?(:call)
104
+ callable = source
105
+ call_with_optional_env(callable, env)
106
+ else
107
+ source
108
+ end
109
+
110
+ raise MiddlewareError.new('Audience is blank for the request', code: 'invalid_audience') if value.nil?
111
+
112
+ if value.is_a?(Array)
113
+ raise MiddlewareError.new('Audience is blank for the request', code: 'invalid_audience') if value.empty?
114
+
115
+ return value
116
+ end
117
+
118
+ normalized = value.to_s
119
+ raise MiddlewareError.new('Audience is blank for the request', code: 'invalid_audience') if normalized.empty?
120
+
121
+ normalized
122
+ end
123
+
124
+ # Invokes the audience callable, passing the Rack env only when required.
125
+ # Falls back to a zero-argument invocation if the callable raises
126
+ # `ArgumentError` due to an unexpected argument.
127
+ #
128
+ # @param callable [#call] Audience resolver callable.
129
+ # @param env [Hash] Rack environment.
130
+ # @param arity [Integer, nil] Callable arity when known, nil otherwise.
131
+ # @return [Object] Audience value returned by the callable.
132
+ # @raise [ArgumentError] when the callable raises for reasons other than arity mismatch.
133
+ def call_with_optional_env(callable, env)
134
+ params = callable_parameters(callable)
135
+
136
+ invocation_chain(params).each do |strategy|
137
+ return strategy.call(callable, env)
138
+ rescue ArgumentError => e
139
+ raise unless wrong_arity_error?(e)
140
+ end
141
+
142
+ callable.call
143
+ end
144
+
145
+ # Returns true when the ArgumentError message indicates a wrong arity.
146
+ #
147
+ # @param error [ArgumentError]
148
+ # @return [Boolean]
149
+ def wrong_arity_error?(error)
150
+ error.message.include?('wrong number of arguments')
151
+ end
152
+
153
+ # Extracts parameter information from a callable's call method.
154
+ #
155
+ # @param callable [#call] The callable object to inspect.
156
+ # @return [Array<Array>, nil] Parameter information as returned by Method#parameters,
157
+ # or nil if the method cannot be resolved.
158
+ def callable_parameters(callable)
159
+ callable.method(:call).parameters
160
+ rescue NameError
161
+ nil
162
+ end
163
+
164
+ # Builds a chain of invocation strategies based on callable parameters.
165
+ #
166
+ # @param params [Array<Array>, nil] Parameter information from Method#parameters.
167
+ # @return [Array<Proc>] Ordered array of lambda strategies to try when calling the callable.
168
+ def invocation_chain(params)
169
+ strategies = []
170
+
171
+ if params.nil?
172
+ # When parameters are unknown, try strategies in safe order:
173
+ # 1. Try with positional argument first (most common)
174
+ # 2. Try with no arguments as fallback
175
+ strategies << ->(callable, env) { callable.call(env) }
176
+ strategies << ->(callable, _env) { callable.call }
177
+ else
178
+ # When parameters are known, try most specific to least specific
179
+ strategies << ->(callable, env) { callable.call(env: env) } if accepts_keyword_env?(params)
180
+ strategies << ->(callable, env) { callable.call(env) } if accepts_positional_env?(params)
181
+ strategies << ->(callable, _env) { callable.call } if accepts_zero_arguments?(params)
182
+ end
183
+
184
+ strategies
185
+ end
186
+
187
+ # Determines if a callable accepts keyword arguments, specifically env: parameter.
188
+ #
189
+ # @param params [Array<Array>, nil] Parameter information from Method#parameters.
190
+ # @return [Boolean] true if the callable accepts keyword arguments including env.
191
+ def accepts_keyword_env?(params)
192
+ return false if params.nil?
193
+
194
+ params.any? do |type, name|
195
+ type == :keyrest ||
196
+ (%i[keyreq key].include?(type) && (name.nil? || name == :env))
197
+ end
198
+ end
199
+
200
+ # Determines if a callable accepts positional arguments.
201
+ #
202
+ # @param params [Array<Array>, nil] Parameter information from Method#parameters.
203
+ # @return [Boolean] true if the callable accepts positional arguments.
204
+ def accepts_positional_env?(params)
205
+ return false if params.nil?
206
+
207
+ params.any? { |type, _| %i[req opt rest].include?(type) }
208
+ end
209
+
210
+ # Determines if a callable accepts zero arguments (no required parameters).
211
+ #
212
+ # @param params [Array<Array>, nil] Parameter information from Method#parameters.
213
+ # @return [Boolean] true if the callable can be called with no arguments.
214
+ def accepts_zero_arguments?(params)
215
+ return false if params.nil?
216
+
217
+ # Only accepts zero arguments if parameters are empty
218
+ # or all parameters are optional/keyword/blocks
219
+ params.empty? || params.all? { |type, _| %i[opt key keyrest block].include?(type) }
220
+ end
221
+ end
222
+
223
+ # @api private
224
+ #
225
+ # Internal mixin for configuration validation and logging utilities.
226
+ # Extracted to keep the main Middleware class focused and under line limits.
227
+ module MiddlewareConfiguration
228
+ private
229
+
230
+ # Validates and normalizes the decoder cache limit configuration.
231
+ #
232
+ # @param limit [Integer, nil] The cache limit value to normalize.
233
+ # @return [Integer, nil] The normalized limit, or nil if no limit.
234
+ # @raise [ArgumentError] if the limit is negative or invalid.
235
+ def normalize_decoder_cache_limit(limit)
236
+ return nil if limit.nil?
237
+
238
+ value = Integer(limit)
239
+ raise ArgumentError, 'decoder_cache_limit must be zero or positive' if value.negative?
240
+
241
+ value
242
+ rescue ArgumentError, TypeError
243
+ raise ArgumentError, 'decoder_cache_limit must be zero or positive'
244
+ end
245
+
246
+ # Validates and normalizes environment key configuration.
247
+ #
248
+ # @param value [String, #to_s] The environment key to normalize.
249
+ # @param option_name [String] The name of the option for error messages.
250
+ # @return [String] The normalized environment key.
251
+ # @raise [ArgumentError] if the key is blank after normalization.
252
+ def normalize_env_key(value, option_name)
253
+ normalized = value.to_s.strip
254
+ raise ArgumentError, "#{option_name} cannot be blank" if normalized.empty?
255
+
256
+ normalized
257
+ end
258
+
259
+ # Validates and normalizes the realm configuration.
260
+ #
261
+ # @param value [String, #to_s, nil] The realm value to normalize.
262
+ # @return [String] The normalized realm, or DEFAULT_REALM if nil.
263
+ # @raise [ArgumentError] if the realm is blank after normalization.
264
+ def normalize_realm(value)
265
+ return DEFAULT_REALM if value.nil?
266
+
267
+ normalized = value.to_s.strip
268
+ raise ArgumentError, 'realm cannot be blank' if normalized.empty?
269
+
270
+ normalized
271
+ end
272
+
273
+ # Checks if a logger instance is available and responds to logging methods.
274
+ #
275
+ # @return [Boolean] true if a logger is available and can log messages.
276
+ def logger_available?
277
+ return false unless @logger
278
+
279
+ @logger.respond_to?(:error) || @logger.respond_to?(:warn) || @logger.respond_to?(:debug)
280
+ end
281
+
282
+ # Logs a message and backtrace using the configured logger.
283
+ #
284
+ # @param message [String] The primary error message to log.
285
+ # @param backtrace [String, nil] The backtrace information to log.
286
+ # @return [void]
287
+ def log_with_logger(message, backtrace)
288
+ log_message(@logger, message)
289
+ log_backtrace(@logger, backtrace)
290
+ end
291
+
292
+ # Logs a message using the most appropriate logger method.
293
+ #
294
+ # @param logger [Logger] The logger instance to use.
295
+ # @param message [String] The message to log.
296
+ # @return [void]
297
+ def log_message(logger, message)
298
+ if logger.respond_to?(:error)
299
+ logger.error(message)
300
+ elsif logger.respond_to?(:warn)
301
+ logger.warn(message)
302
+ end
303
+ end
304
+
305
+ # Logs backtrace information using the most appropriate logger method.
306
+ #
307
+ # @param logger [Logger] The logger instance to use.
308
+ # @param backtrace [String, nil] The backtrace information to log.
309
+ # @return [void]
310
+ def log_backtrace(logger, backtrace)
311
+ return unless backtrace
312
+
313
+ if logger.respond_to?(:debug)
314
+ logger.debug(backtrace)
315
+ elsif logger.respond_to?(:error)
316
+ logger.error(backtrace)
317
+ elsif logger.respond_to?(:warn)
318
+ logger.warn(backtrace)
319
+ end
320
+ end
321
+ end
322
+
323
+ # @api private
324
+ #
325
+ # Internal mixin for decoder cache management with LRU eviction.
326
+ # Handles TokenDecoder instance caching and cleanup.
327
+ module MiddlewareDecoderCache
328
+ private
329
+
330
+ # Stores a decoder in the cache and updates access order if tracking is enabled.
331
+ #
332
+ # @param cache_key [String] The cache key for the decoder
333
+ # @param decoder [TokenDecoder] The decoder instance to cache
334
+ # @return [TokenDecoder] The cached decoder instance
335
+ def store_decoder_cache(cache_key, decoder)
336
+ @decoder_cache[cache_key] = decoder
337
+ touch_decoder_cache(cache_key) if track_decoder_order?
338
+ decoder
339
+ end
340
+
341
+ # Prunes the decoder cache to stay within the configured limit.
342
+ # Removes the oldest entries when the cache size exceeds the limit.
343
+ #
344
+ # @return [void]
345
+ def prune_decoder_cache_if_needed
346
+ return unless track_decoder_order?
347
+
348
+ while @decoder_cache_order.length >= @decoder_cache_limit
349
+ oldest = @decoder_cache_order.shift
350
+ @decoder_cache.delete(oldest)
351
+ end
352
+ end
353
+
354
+ # Updates the access order for a cache entry to mark it as recently used.
355
+ # Moves the cache key to the end of the order queue for LRU tracking.
356
+ #
357
+ # @param cache_key [String] The cache key to mark as recently accessed
358
+ # @return [void]
359
+ def touch_decoder_cache(cache_key)
360
+ @decoder_cache_order.delete(cache_key)
361
+ @decoder_cache_order << cache_key
362
+ end
363
+
364
+ # Checks if decoder cache order tracking is enabled.
365
+ # Returns true if cache limit is set and positive.
366
+ #
367
+ # @return [Boolean] true if order tracking is enabled
368
+ def track_decoder_order?
369
+ @decoder_cache_limit&.positive?
370
+ end
371
+
372
+ # Clears all cached decoder instances and order tracking.
373
+ # Removes all entries from both the cache and order queue.
374
+ #
375
+ # @return [void]
376
+ def clear_decoder_cache
377
+ @decoder_cache.clear
378
+ @decoder_cache_order.clear
379
+ end
380
+ end
381
+
89
382
  # @api private
90
383
  #
91
384
  # Internal mixin for JWT verification and discovery/JWKs management.
@@ -108,6 +401,9 @@ module Verikloak
108
401
 
109
402
  # Returns a cached TokenDecoder instance for current inputs.
110
403
  # Cache key uses issuer, audience, leeway, token_verify_options, and JWKs fetched_at timestamp.
404
+ #
405
+ # @param audience [String, #call] The audience to create a decoder for
406
+ # @return [TokenDecoder] A decoder instance for the given audience
111
407
  def decoder_for(audience)
112
408
  keys = @jwks_cache.cached
113
409
  fetched_at = @jwks_cache.respond_to?(:fetched_at) ? @jwks_cache.fetched_at : nil
@@ -119,13 +415,23 @@ module Verikloak
119
415
  fetched_at
120
416
  ].hash
121
417
  @mutex.synchronize do
122
- @decoder_cache[cache_key] ||= TokenDecoder.new(
418
+ if (decoder = @decoder_cache[cache_key])
419
+ touch_decoder_cache(cache_key) if track_decoder_order?
420
+ return decoder
421
+ end
422
+
423
+ decoder = TokenDecoder.new(
123
424
  jwks: keys,
124
425
  issuer: @issuer,
125
426
  audience: audience,
126
427
  leeway: @leeway,
127
428
  options: @token_verify_options
128
429
  )
430
+
431
+ return decoder if @decoder_cache_limit&.zero?
432
+
433
+ prune_decoder_cache_if_needed
434
+ store_decoder_cache(cache_key, decoder)
129
435
  end
130
436
  end
131
437
 
@@ -141,8 +447,9 @@ module Verikloak
141
447
  # Decodes and verifies the JWT using the cached JWKs. On certain verification
142
448
  # failures (e.g., key rotation), it refreshes the JWKs and retries once.
143
449
  #
144
- # @param token [String]
145
- # @return [Hash] decoded JWT claims
450
+ # @param env [Hash] The Rack environment hash
451
+ # @param token [String] The JWT token to decode and verify
452
+ # @return [Hash] Decoded JWT claims
146
453
  # @raise [Verikloak::Error] bubbles up verification/fetch errors for centralized handling
147
454
  def decode_token(env, token)
148
455
  ensure_jwks_cache!
@@ -169,73 +476,6 @@ module Verikloak
169
476
  end
170
477
  end
171
478
 
172
- # Resolves the expected audience for the current request.
173
- #
174
- # @param env [Hash] Rack environment.
175
- # @return [String, Array<String>] The expected audience value.
176
- # @raise [MiddlewareError] when the resolved audience is blank.
177
- def resolve_audience(env)
178
- source = @audience_source
179
- value = if source.respond_to?(:call)
180
- callable = source
181
- arity = callable.respond_to?(:arity) ? callable.arity : safe_callable_arity(callable)
182
- call_with_optional_env(callable, env, arity)
183
- else
184
- source
185
- end
186
-
187
- raise MiddlewareError.new('Audience is blank for the request', code: 'invalid_audience') if value.nil?
188
-
189
- if value.is_a?(Array)
190
- raise MiddlewareError.new('Audience is blank for the request', code: 'invalid_audience') if value.empty?
191
-
192
- return value
193
- end
194
-
195
- normalized = value.to_s
196
- raise MiddlewareError.new('Audience is blank for the request', code: 'invalid_audience') if normalized.empty?
197
-
198
- normalized
199
- end
200
-
201
- # Invokes the audience callable, passing the Rack env only when required.
202
- # Falls back to a zero-argument invocation if the callable raises
203
- # `ArgumentError` due to an unexpected argument.
204
- #
205
- # @param callable [#call] Audience resolver callable.
206
- # @param env [Hash] Rack environment.
207
- # @param arity [Integer, nil] Callable arity when known, nil otherwise.
208
- # @return [Object] Audience value returned by the callable.
209
- # @raise [ArgumentError] when the callable raises for reasons other than arity mismatch.
210
- def call_with_optional_env(callable, env, arity)
211
- return callable.call if arity&.zero?
212
-
213
- callable.call(env)
214
- rescue ArgumentError => e
215
- raise unless arity.nil? && wrong_arity_error?(e)
216
-
217
- callable.call
218
- end
219
-
220
- # Safely obtains a callable's arity, returning nil when `#method(:call)`
221
- # cannot be resolved (e.g., BasicObject-based objects).
222
- #
223
- # @param callable [#call]
224
- # @return [Integer, nil]
225
- def safe_callable_arity(callable)
226
- callable.method(:call).arity
227
- rescue NameError
228
- nil
229
- end
230
-
231
- # Returns true when the ArgumentError message indicates a wrong arity.
232
- #
233
- # @param error [ArgumentError]
234
- # @return [Boolean]
235
- def wrong_arity_error?(error)
236
- error.message.include?('wrong number of arguments')
237
- end
238
-
239
479
  # Ensures that discovery metadata and JWKs cache are initialized and up-to-date.
240
480
  # This method is thread-safe.
241
481
  #
@@ -246,14 +486,21 @@ module Verikloak
246
486
  # @raise [Verikloak::DiscoveryError, Verikloak::JwksCacheError, Verikloak::MiddlewareError]
247
487
  def ensure_jwks_cache!
248
488
  @mutex.synchronize do
489
+ previous_keys_id = cached_keys_identity(@jwks_cache)
249
490
  if @jwks_cache.nil?
250
491
  config = @discovery.fetch!
251
- @issuer = config['issuer']
492
+ # Use configured issuer if provided, otherwise use discovered issuer
493
+ @issuer = @configured_issuer || config['issuer']
252
494
  jwks_uri = config['jwks_uri']
253
495
  @jwks_cache = JwksCache.new(jwks_uri: jwks_uri, connection: @connection)
496
+ elsif @configured_issuer.nil? && @issuer.nil?
497
+ # If jwks_cache was injected but no issuer configured and not yet discovered, fetch discovery to set issuer
498
+ config = @discovery.fetch!
499
+ @issuer = config['issuer']
254
500
  end
255
501
 
256
502
  @jwks_cache.fetch!
503
+ purge_decoder_cache_if_keys_changed(previous_keys_id)
257
504
  end
258
505
  rescue Verikloak::DiscoveryError, Verikloak::JwksCacheError => e
259
506
  # Re-raise so that specific error codes can be mapped in the middleware
@@ -261,6 +508,33 @@ module Verikloak
261
508
  rescue StandardError => e
262
509
  raise MiddlewareError.new("Failed to initialize JWKs cache: #{e.message}", code: 'jwks_fetch_failed')
263
510
  end
511
+
512
+ # Purges the decoder cache if the JWKs have changed since last check.
513
+ # Compares key set identity to detect key rotation and invalidate cached decoders.
514
+ #
515
+ # @param previous_keys_id [String, nil] The previous JWKs identity hash
516
+ # @return [void]
517
+ def purge_decoder_cache_if_keys_changed(previous_keys_id)
518
+ current_id = cached_keys_identity(@jwks_cache)
519
+ if (@last_cached_keys_id && current_id && @last_cached_keys_id != current_id) ||
520
+ (previous_keys_id && current_id && previous_keys_id != current_id)
521
+ clear_decoder_cache
522
+ end
523
+
524
+ @last_cached_keys_id = current_id if current_id
525
+ end
526
+
527
+ # Generates a unique identity hash for the current JWKs set.
528
+ # Used to detect changes in the key set for cache invalidation.
529
+ #
530
+ # @param cache [JwksCache] The JWKs cache instance
531
+ # @return [String, nil] A hash representing the current key set identity
532
+ def cached_keys_identity(cache)
533
+ return unless cache.respond_to?(:cached)
534
+
535
+ keys = cache.cached
536
+ keys&.__id__
537
+ end
264
538
  end
265
539
 
266
540
  # @api private
@@ -285,13 +559,17 @@ module Verikloak
285
559
 
286
560
  private
287
561
 
288
- # @param code [String, nil] short error code
562
+ # Determines if an error code should result in a 403 Forbidden response.
563
+ #
564
+ # @param code [String, nil] The error code to check
289
565
  # @return [Boolean] true if the error should be treated as a 403 Forbidden
290
566
  def forbidden?(code)
291
567
  code == 'forbidden'
292
568
  end
293
569
 
294
- # @param code [String, nil]
570
+ # Determines if an error code belongs to authentication-related errors.
571
+ #
572
+ # @param code [String, nil] The error code to check
295
573
  # @return [Boolean] true if the error belongs to {AUTH_ERROR_CODES}
296
574
  def auth_error?(code)
297
575
  code && AUTH_ERROR_CODES.include?(code)
@@ -347,6 +625,9 @@ module Verikloak
347
625
  class Middleware
348
626
  include MiddlewareErrorMapping
349
627
  include SkipPathMatcher
628
+ include MiddlewareConfiguration
629
+ include MiddlewareAudienceResolution
630
+ include MiddlewareDecoderCache
350
631
  include MiddlewareTokenVerification
351
632
 
352
633
  DEFAULT_REALM = 'verikloak'
@@ -357,6 +638,7 @@ module Verikloak
357
638
  # @param discovery_url [String] OIDC discovery endpoint URL
358
639
  # @param audience [String, #call] Expected `aud` claim. When a callable is provided it
359
640
  # receives the Rack env and may return a String or Array of audiences.
641
+ # @param issuer [String, nil] Optional issuer override (defaults to discovery `issuer`)
360
642
  # @param skip_paths [Array<String>] literal paths or wildcard patterns to bypass auth
361
643
  # @param discovery [Discovery, nil] custom discovery instance (for DI/tests)
362
644
  # @param jwks_cache [JwksCache, nil] custom JWKs cache instance (for DI/tests)
@@ -367,15 +649,19 @@ module Verikloak
367
649
  # to TokenDecoder.
368
650
  # e.g., { verify_iat: false, leeway: 10 }
369
651
  # rubocop:disable Metrics/ParameterLists
652
+ DEFAULT_DECODER_CACHE_LIMIT = 128
653
+
370
654
  def initialize(app,
371
655
  discovery_url:,
372
656
  audience:,
657
+ issuer: nil,
373
658
  skip_paths: [],
374
659
  discovery: nil,
375
660
  jwks_cache: nil,
376
661
  connection: nil,
377
662
  leeway: Verikloak::TokenDecoder::DEFAULT_LEEWAY,
378
663
  token_verify_options: {},
664
+ decoder_cache_limit: DEFAULT_DECODER_CACHE_LIMIT,
379
665
  token_env_key: DEFAULT_TOKEN_ENV_KEY,
380
666
  user_env_key: DEFAULT_USER_ENV_KEY,
381
667
  realm: DEFAULT_REALM,
@@ -387,9 +673,15 @@ module Verikloak
387
673
  @jwks_cache = jwks_cache
388
674
  @leeway = leeway
389
675
  @token_verify_options = token_verify_options || {}
390
- @issuer = nil
676
+ @decoder_cache_limit = normalize_decoder_cache_limit(decoder_cache_limit)
677
+ # Optional user-configured issuer (overrides discovery issuer when provided)
678
+ @configured_issuer = issuer
679
+ # Effective issuer; may be nil initially and set via discovery if not configured
680
+ @issuer = @configured_issuer
391
681
  @mutex = Mutex.new
392
682
  @decoder_cache = {}
683
+ @decoder_cache_order = []
684
+ @last_cached_keys_id = nil
393
685
  @token_env_key = normalize_env_key(token_env_key, 'token_env_key')
394
686
  @user_env_key = normalize_env_key(user_env_key, 'user_env_key')
395
687
  @realm = normalize_realm(realm)
@@ -421,14 +713,16 @@ module Verikloak
421
713
 
422
714
  # Returns the Faraday connection used for HTTP operations (Discovery/JWKs).
423
715
  # Exposed for tests; not part of public API.
716
+ #
717
+ # @return [Faraday::Connection] The HTTP connection instance
424
718
  def http_connection
425
719
  @connection
426
720
  end
427
721
 
428
722
  # Verifies the token, stores result in Rack env, and forwards to the downstream app.
429
723
  #
430
- # @param env [Hash]
431
- # @param token [String]
724
+ # @param env [Hash] The Rack environment hash
725
+ # @param token [String] The extracted JWT token
432
726
  # @return [Array(Integer, Hash, Array<String>)] Rack response triple
433
727
  def handle_request(env, token)
434
728
  claims = decode_token(env, token)
@@ -439,8 +733,8 @@ module Verikloak
439
733
 
440
734
  # Extracts the Bearer token from the `Authorization` header.
441
735
  #
442
- # @param env [Hash]
443
- # @return [String] the raw JWT string
736
+ # @param env [Hash] The Rack environment hash
737
+ # @return [String] The raw JWT string
444
738
  # @raise [Verikloak::MiddlewareError] when the header is missing or malformed
445
739
  def extract_token(env)
446
740
  auth = env['HTTP_AUTHORIZATION']
@@ -459,8 +753,8 @@ module Verikloak
459
753
 
460
754
  # Converts a raised error into a `[code, http_status]` tuple for response rendering.
461
755
  #
462
- # @param error [Exception]
463
- # @return [Array(String, Integer)]
756
+ # @param error [Exception] The exception to map
757
+ # @return [Array(String, Integer)] A tuple of error code and HTTP status
464
758
  def map_error(error)
465
759
  code = error.respond_to?(:code) ? error.code : nil
466
760
 
@@ -480,9 +774,9 @@ module Verikloak
480
774
 
481
775
  # Builds a JSON error response with RFC 6750 `WWW-Authenticate` header for 401.
482
776
  #
483
- # @param code [String]
484
- # @param message [String]
485
- # @param status [Integer]
777
+ # @param code [String] The error code to include in the response
778
+ # @param message [String] The error message to include in the response
779
+ # @param status [Integer] The HTTP status code for the response
486
780
  # @return [Array(Integer, Hash, Array<String>)] Rack response triple
487
781
  def error_response(code = 'unauthorized', message = 'Unauthorized', status = 401)
488
782
  body = { error: code, message: message }.to_json
@@ -509,52 +803,5 @@ module Verikloak
509
803
  warn backtrace if backtrace
510
804
  end
511
805
  end
512
-
513
- def logger_available?
514
- return false unless @logger
515
-
516
- @logger.respond_to?(:error) || @logger.respond_to?(:warn) || @logger.respond_to?(:debug)
517
- end
518
-
519
- def log_with_logger(message, backtrace)
520
- log_message(@logger, message)
521
- log_backtrace(@logger, backtrace)
522
- end
523
-
524
- def log_message(logger, message)
525
- if logger.respond_to?(:error)
526
- logger.error(message)
527
- elsif logger.respond_to?(:warn)
528
- logger.warn(message)
529
- end
530
- end
531
-
532
- def log_backtrace(logger, backtrace)
533
- return unless backtrace
534
-
535
- if logger.respond_to?(:debug)
536
- logger.debug(backtrace)
537
- elsif logger.respond_to?(:error)
538
- logger.error(backtrace)
539
- elsif logger.respond_to?(:warn)
540
- logger.warn(backtrace)
541
- end
542
- end
543
-
544
- def normalize_env_key(value, option_name)
545
- normalized = value.to_s.strip
546
- raise ArgumentError, "#{option_name} cannot be blank" if normalized.empty?
547
-
548
- normalized
549
- end
550
-
551
- def normalize_realm(value)
552
- return DEFAULT_REALM if value.nil?
553
-
554
- normalized = value.to_s.strip
555
- raise ArgumentError, 'realm cannot be blank' if normalized.empty?
556
-
557
- normalized
558
- end
559
806
  end
560
807
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Verikloak
4
4
  # Defines the current version of the Verikloak gem.
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: verikloak
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - taiyaky
@@ -109,7 +109,7 @@ metadata:
109
109
  source_code_uri: https://github.com/taiyaky/verikloak
110
110
  changelog_uri: https://github.com/taiyaky/verikloak/blob/main/CHANGELOG.md
111
111
  bug_tracker_uri: https://github.com/taiyaky/verikloak/issues
112
- documentation_uri: https://rubydoc.info/gems/verikloak/0.2.0
112
+ documentation_uri: https://rubydoc.info/gems/verikloak/0.3.0
113
113
  rubygems_mfa_required: 'true'
114
114
  rdoc_options: []
115
115
  require_paths: