verikloak-audience 0.2.9 → 0.3.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/CHANGELOG.md +37 -0
- data/README.md +5 -3
- data/lib/generators/verikloak/audience/install/templates/initializer.rb.erb +21 -4
- data/lib/verikloak/audience/checker.rb +18 -1
- data/lib/verikloak/audience/configuration.rb +8 -2
- data/lib/verikloak/audience/middleware.rb +49 -5
- data/lib/verikloak/audience/railtie.rb +218 -193
- data/lib/verikloak/audience/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 400925f130b177647a3d7007cf4e43b566451e5b131968fbca2946d8a2f47d06
|
|
4
|
+
data.tar.gz: 89fa1b482fe9cdaa9278f56380cc59bba52325dd7db6b0e9aeb473e658568eb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d39de8ac4bfb587b968e4017e36edc03fbe49c4a7495963671aacb32a5d65fa06b39957922344b1dd3336614bb20b5ad8eeef9c7248eb6ea9c82dd772323072
|
|
7
|
+
data.tar.gz: bc01de7533c0388fc4e9f8ed35747b9136c04f166cbcfe0dbc75ff2895703aed960ed0d3845beb0dd3ecc2a3bffae786609864a59ae335672f6754a216adda4d
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.3.1] - 2026-01-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`:any_match` profile**: New audience validation profile that passes when at least one of the required audiences is present in the token.
|
|
14
|
+
- **`skip_paths` configuration**: Skip audience validation for specific paths (e.g., health checks). Supports exact matches, prefix matches, wildcard patterns, and Regexp.
|
|
15
|
+
- **Generator improvements**: Install generator now includes `Verikloak::Audience.configure` block with all four profiles documented.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Regexp support in `skip_paths`**: Fixed `NoMethodError` when passing Regexp patterns.
|
|
19
|
+
- **Warning message accuracy**: Unconfigured warning now correctly states "ALL requests will be rejected with 403".
|
|
20
|
+
- Configuration sync now happens before middleware insertion.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- README now correctly describes that the Railtie automatically inserts the middleware.
|
|
24
|
+
- Removed manual `insert_middleware` call from generated initializer (Railtie handles this automatically).
|
|
25
|
+
|
|
26
|
+
## [0.3.0] - 2026-01-01
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **Rails 8.x+ middleware insertion**: Fixed automatic middleware insertion not working in Rails 8.x+ due to queued middleware operations not being visible via `include?` checks.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Railtie initializer now specifies `before: :build_middleware_stack` to ensure middleware insertion is queued before stack construction.
|
|
33
|
+
- Middleware insertion now uses `app.config.middleware` instead of `app.middleware` for more reliable queuing in Rails 8.x+.
|
|
34
|
+
- Extracted `middleware_not_found_error?` method to handle error detection across Rails versions (Rails 7's `MiddlewareNotFound` and Rails 8+'s `RuntimeError`).
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- `@middleware_insertion_attempted` class variable to prevent duplicate middleware insertion when both railtie and generator initializer run.
|
|
38
|
+
- `reset_middleware_insertion_flag!` method for testing purposes.
|
|
39
|
+
- Support for alternative error message patterns (`"does not exist"`, `/middleware.*not found/i`) for future Rails version compatibility.
|
|
40
|
+
- Comprehensive test coverage for:
|
|
41
|
+
- `include?` early return when middleware is already present
|
|
42
|
+
- Duplicate insertion prevention via flag
|
|
43
|
+
- Rails 7 `MiddlewareNotFound` exception handling
|
|
44
|
+
- Alternative error message patterns
|
|
45
|
+
- Unexpected exception re-raising
|
|
46
|
+
|
|
10
47
|
## [0.2.9] - 2025-12-31
|
|
11
48
|
|
|
12
49
|
### Added
|
data/README.md
CHANGED
|
@@ -22,6 +22,7 @@ For the full error behaviour (response shapes, exception classes, logging hints)
|
|
|
22
22
|
|---------|---------|-----------------------------------------------------------|
|
|
23
23
|
| `:strict_single` *(recommended)* | Requires `aud` to match `required_aud` exactly (order-insensitive, no extras). | APIs where audiences are cleanly separated. Logged suggestion when the observed `aud` already equals the configured list. |
|
|
24
24
|
| `:allow_account` | Allows `account` in addition to required audiences (e.g. `["rails-api","account"]`). | SPA + API mixes where Keycloak always emits `account`. Suggested when strict mode fails and the log shows `profile=:allow_account`. |
|
|
25
|
+
| `:any_match` | Passes when at least one of the required audiences is present. | Shared APIs where multiple clients may have overlapping audiences. More permissive than `:strict_single`. |
|
|
25
26
|
| `:resource_or_aud` | Passes when `resource_access[client].roles` is present; otherwise falls back to `:allow_account`. | Services relying on resource roles. Suggested when logs output `profile=:resource_or_aud`. |
|
|
26
27
|
|
|
27
28
|
## Installation
|
|
@@ -30,13 +31,14 @@ For the full error behaviour (response shapes, exception classes, logging hints)
|
|
|
30
31
|
bundle add verikloak-audience
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
In Rails applications,
|
|
34
|
+
In Rails applications, run the generator to create a configuration initializer:
|
|
34
35
|
|
|
35
36
|
```bash
|
|
36
37
|
rails g verikloak:audience:install
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
This creates `config/initializers/verikloak_audience.rb`
|
|
40
|
+
This creates `config/initializers/verikloak_audience.rb` with configuration options.
|
|
41
|
+
The middleware is automatically inserted by the Railtie after `Verikloak::Middleware`.
|
|
40
42
|
|
|
41
43
|
## Manual Rack / Rails setup
|
|
42
44
|
|
|
@@ -58,7 +60,7 @@ See [`examples/rack.ru`](examples/rack.ru) for a full Rack sample. In Rails, alw
|
|
|
58
60
|
|
|
59
61
|
| Option | Type | Default | Description |
|
|
60
62
|
|--------|------|---------|-------------|
|
|
61
|
-
| `profile` | Symbol | `:strict_single` | Profile selector. Accepts `:strict_single`, `:allow_account`, or `:resource_or_aud`. |
|
|
63
|
+
| `profile` | Symbol | `:strict_single` | Profile selector. Accepts `:strict_single`, `:allow_account`, `:any_match`, or `:resource_or_aud`. |
|
|
62
64
|
| `required_aud` | Array/String/Symbol | `[]` | Required audience values; coerced to an array internally. |
|
|
63
65
|
| `resource_client` | String | `"rails-api"` | Keycloak client id used to look up `resource_access[client].roles`. |
|
|
64
66
|
| `env_claims_key` | String | `"verikloak.user"` | Rack env key where verified claims are stored. |
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
# Verikloak Audience Configuration
|
|
4
|
+
#
|
|
5
|
+
# Configure the audience validation profile and settings.
|
|
6
|
+
# This middleware provides stricter audience validation than core Verikloak.
|
|
7
|
+
#
|
|
8
|
+
# The middleware is automatically inserted after Verikloak::Middleware by the
|
|
9
|
+
# Railtie. No manual middleware insertion is required.
|
|
10
|
+
#
|
|
11
|
+
Verikloak::Audience.configure do |config|
|
|
12
|
+
# Audience validation profile
|
|
13
|
+
# :strict_single - Only exact single audience match (default, recommended)
|
|
14
|
+
# :allow_account - Allow "account" alongside your API audience
|
|
15
|
+
# :any_match - At least one required audience is present (more permissive)
|
|
16
|
+
# :resource_or_aud - Check resource_access roles first, fallback to :allow_account
|
|
17
|
+
# config.profile = :strict_single
|
|
18
|
+
|
|
19
|
+
# Required audience(s) - synced from verikloak-rails automatically
|
|
20
|
+
# config.required_aud = ['rails-api']
|
|
21
|
+
|
|
22
|
+
# Skip paths for audience validation (synced from verikloak-rails automatically)
|
|
23
|
+
# config.skip_paths = %w[/up /health]
|
|
7
24
|
end
|
|
@@ -7,7 +7,7 @@ module Verikloak
|
|
|
7
7
|
# This module provides predicate helpers used by the middleware to decide
|
|
8
8
|
# whether a given set of claims satisfies the configured profile.
|
|
9
9
|
module Checker
|
|
10
|
-
VALID_PROFILES = %i[strict_single allow_account resource_or_aud].freeze
|
|
10
|
+
VALID_PROFILES = %i[strict_single allow_account any_match resource_or_aud].freeze
|
|
11
11
|
|
|
12
12
|
module_function
|
|
13
13
|
|
|
@@ -33,6 +33,8 @@ module Verikloak
|
|
|
33
33
|
strict_single?(claims, cfg.required_aud_list)
|
|
34
34
|
when :allow_account
|
|
35
35
|
allow_account?(claims, cfg.required_aud_list)
|
|
36
|
+
when :any_match
|
|
37
|
+
any_match?(claims, cfg.required_aud_list)
|
|
36
38
|
when :resource_or_aud
|
|
37
39
|
resource_or_aud?(claims, cfg.resource_client.to_s, cfg.required_aud_list)
|
|
38
40
|
end
|
|
@@ -66,6 +68,20 @@ module Verikloak
|
|
|
66
68
|
extras.empty? && (required - aud).empty?
|
|
67
69
|
end
|
|
68
70
|
|
|
71
|
+
# Validate that at least one required audience is present in the token.
|
|
72
|
+
# More permissive than :strict_single; useful when multiple clients share audiences.
|
|
73
|
+
#
|
|
74
|
+
# @param claims [Hash]
|
|
75
|
+
# @param required [Array<String>]
|
|
76
|
+
# @return [Boolean]
|
|
77
|
+
def any_match?(claims, required)
|
|
78
|
+
aud = normalized_audiences(claims)
|
|
79
|
+
return false if required.empty?
|
|
80
|
+
|
|
81
|
+
# At least one of the required audiences must be present
|
|
82
|
+
aud.intersect?(required.map(&:to_s))
|
|
83
|
+
end
|
|
84
|
+
|
|
69
85
|
# Permit when resource roles exist for the client; otherwise fallback to
|
|
70
86
|
# {#allow_account?}.
|
|
71
87
|
#
|
|
@@ -92,6 +108,7 @@ module Verikloak
|
|
|
92
108
|
required = cfg.required_aud_list
|
|
93
109
|
return :strict_single if strict_single?(claims, required)
|
|
94
110
|
return :allow_account if allow_account?(claims, required)
|
|
111
|
+
return :any_match if any_match?(claims, required)
|
|
95
112
|
return :resource_or_aud if resource_or_aud?(claims, cfg.resource_client.to_s, required)
|
|
96
113
|
|
|
97
114
|
:strict_single
|
|
@@ -8,7 +8,7 @@ module Verikloak
|
|
|
8
8
|
#
|
|
9
9
|
# @!attribute [rw] profile
|
|
10
10
|
# The enforcement profile to use.
|
|
11
|
-
# @return [:strict_single, :allow_account, :resource_or_aud]
|
|
11
|
+
# @return [:strict_single, :allow_account, :any_match, :resource_or_aud]
|
|
12
12
|
# @!attribute [rw] required_aud
|
|
13
13
|
# Required audience(s). Can be a String/Symbol or an Array of them.
|
|
14
14
|
# @return [Array<String,Symbol>, String, Symbol]
|
|
@@ -21,12 +21,16 @@ module Verikloak
|
|
|
21
21
|
# @!attribute [rw] suggest_in_logs
|
|
22
22
|
# Whether to log a suggestion when audience validation fails.
|
|
23
23
|
# @return [Boolean]
|
|
24
|
+
# @!attribute [rw] skip_paths
|
|
25
|
+
# Paths to skip audience validation for (e.g., health checks).
|
|
26
|
+
# Synced from verikloak-rails when available.
|
|
27
|
+
# @return [Array<String, Regexp>]
|
|
24
28
|
class Configuration
|
|
25
29
|
DEFAULT_RESOURCE_CLIENT = 'rails-api'
|
|
26
30
|
DEFAULT_ENV_CLAIMS_KEY = 'verikloak.user'
|
|
27
31
|
|
|
28
32
|
attr_accessor :profile, :required_aud, :resource_client,
|
|
29
|
-
:suggest_in_logs
|
|
33
|
+
:suggest_in_logs, :skip_paths
|
|
30
34
|
attr_reader :env_claims_key
|
|
31
35
|
|
|
32
36
|
# Create a configuration with safe defaults.
|
|
@@ -38,6 +42,7 @@ module Verikloak
|
|
|
38
42
|
@resource_client = DEFAULT_RESOURCE_CLIENT
|
|
39
43
|
self.env_claims_key = DEFAULT_ENV_CLAIMS_KEY
|
|
40
44
|
@suggest_in_logs = true
|
|
45
|
+
@skip_paths = []
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
# Ensure `dup` produces an independent copy.
|
|
@@ -51,6 +56,7 @@ module Verikloak
|
|
|
51
56
|
@resource_client = safe_dup(source.resource_client)
|
|
52
57
|
self.env_claims_key = safe_dup(source.env_claims_key)
|
|
53
58
|
@suggest_in_logs = source.suggest_in_logs
|
|
59
|
+
@skip_paths = source.skip_paths&.dup || []
|
|
54
60
|
end
|
|
55
61
|
|
|
56
62
|
# Coerce `required_aud` into an array of strings.
|
|
@@ -15,11 +15,12 @@ module Verikloak
|
|
|
15
15
|
class Middleware
|
|
16
16
|
# @param app [#call] next Rack application
|
|
17
17
|
# @param opts [Hash] configuration overrides (see {Configuration})
|
|
18
|
-
# @option opts [Symbol] :profile
|
|
18
|
+
# @option opts [Symbol] :profile (:strict_single, :allow_account, :any_match, :resource_or_aud)
|
|
19
19
|
# @option opts [Array<String>,String,Symbol] :required_aud
|
|
20
20
|
# @option opts [String] :resource_client
|
|
21
21
|
# @option opts [String] :env_claims_key
|
|
22
22
|
# @option opts [Boolean] :suggest_in_logs
|
|
23
|
+
# @option opts [Array<String, Regexp>] :skip_paths
|
|
23
24
|
def initialize(app, **opts)
|
|
24
25
|
@app = app
|
|
25
26
|
@config = Verikloak::Audience.config.dup
|
|
@@ -32,6 +33,9 @@ module Verikloak
|
|
|
32
33
|
# @param env [Hash] Rack environment
|
|
33
34
|
# @return [Array(Integer, Hash, #each)] Rack response triple
|
|
34
35
|
def call(env)
|
|
36
|
+
# Skip audience validation for configured paths (e.g., health checks)
|
|
37
|
+
return @app.call(env) if skip_path?(env)
|
|
38
|
+
|
|
35
39
|
env_key = @config.env_claims_key
|
|
36
40
|
claims = env[env_key] || env[env_key&.to_sym] || {}
|
|
37
41
|
return @app.call(env) if Checker.ok?(claims, @config)
|
|
@@ -51,6 +55,36 @@ module Verikloak
|
|
|
51
55
|
|
|
52
56
|
private
|
|
53
57
|
|
|
58
|
+
# Check if the current request path should skip audience validation.
|
|
59
|
+
#
|
|
60
|
+
# @param env [Hash] Rack environment
|
|
61
|
+
# @return [Boolean]
|
|
62
|
+
def skip_path?(env)
|
|
63
|
+
paths = @config.skip_paths
|
|
64
|
+
return false if paths.nil? || paths.empty?
|
|
65
|
+
|
|
66
|
+
request_path = env['PATH_INFO'] || env['REQUEST_PATH'] || ''
|
|
67
|
+
paths.any? { |pattern| path_matches?(request_path, pattern) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Determine if a request path matches the given pattern.
|
|
71
|
+
# Supports exact matches and prefix matches (pattern ending with *).
|
|
72
|
+
#
|
|
73
|
+
# @param request_path [String]
|
|
74
|
+
# @param pattern [String, Regexp]
|
|
75
|
+
# @return [Boolean]
|
|
76
|
+
def path_matches?(request_path, pattern)
|
|
77
|
+
return false if pattern.nil?
|
|
78
|
+
return request_path.match?(pattern) if pattern.is_a?(Regexp)
|
|
79
|
+
return false if pattern.empty?
|
|
80
|
+
|
|
81
|
+
if pattern.end_with?('*')
|
|
82
|
+
request_path.start_with?(pattern.chomp('*'))
|
|
83
|
+
else
|
|
84
|
+
request_path == pattern || request_path.start_with?("#{pattern}/")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
54
88
|
# Apply provided options to the configuration instance.
|
|
55
89
|
#
|
|
56
90
|
# @param opts [Hash] raw overrides provided to the middleware
|
|
@@ -70,14 +104,24 @@ module Verikloak
|
|
|
70
104
|
|
|
71
105
|
# Determine whether configuration validation should run. This allows
|
|
72
106
|
# Rails generators to boot without a fully-populated configuration since
|
|
73
|
-
# the install task is responsible for creating it.
|
|
107
|
+
# the install task is responsible for creating it. Also skips validation
|
|
108
|
+
# when configuration is not explicitly set up.
|
|
74
109
|
#
|
|
75
110
|
# @return [Boolean]
|
|
76
111
|
def skip_validation?
|
|
77
|
-
|
|
78
|
-
|
|
112
|
+
# Skip if Railtie indicates generator mode
|
|
113
|
+
if defined?(::Verikloak::Audience::Railtie)
|
|
114
|
+
if ::Verikloak::Audience::Railtie.respond_to?(:skip_configuration_validation?) && ::Verikloak::Audience::Railtie.skip_configuration_validation?
|
|
115
|
+
return true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Skip if configuration is incomplete (no audiences configured)
|
|
119
|
+
if ::Verikloak::Audience::Railtie.respond_to?(:skip_unconfigured_validation?) && ::Verikloak::Audience::Railtie.skip_unconfigured_validation?
|
|
120
|
+
return true
|
|
121
|
+
end
|
|
122
|
+
end
|
|
79
123
|
|
|
80
|
-
|
|
124
|
+
false
|
|
81
125
|
end
|
|
82
126
|
|
|
83
127
|
# Emit a warning for failed audience checks using request-scoped loggers
|
|
@@ -4,69 +4,8 @@ require 'rails/railtie'
|
|
|
4
4
|
|
|
5
5
|
module Verikloak
|
|
6
6
|
module Audience
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
# This Railtie automatically inserts {Verikloak::Audience::Middleware}
|
|
10
|
-
# into the Rails middleware stack directly after {::Verikloak::Middleware}
|
|
11
|
-
# when it is present.
|
|
12
|
-
#
|
|
13
|
-
# If the core Verikloak middleware is not available, nothing is inserted.
|
|
14
|
-
# You may still customize the placement in your application via
|
|
15
|
-
# `config.middleware`.
|
|
16
|
-
#
|
|
17
|
-
# @example Configure placement and options
|
|
18
|
-
# # config/application.rb
|
|
19
|
-
# config.middleware.insert_after Verikloak::Middleware,
|
|
20
|
-
# Verikloak::Audience::Middleware,
|
|
21
|
-
# profile: :allow_account,
|
|
22
|
-
# required_aud: ['rails-api'],
|
|
23
|
-
# resource_client: 'rails-api',
|
|
24
|
-
# env_claims_key: 'verikloak.user',
|
|
25
|
-
# suggest_in_logs: true
|
|
26
|
-
class Railtie < ::Rails::Railtie
|
|
27
|
-
# Adds the audience middleware after the core Verikloak middleware
|
|
28
|
-
# when available.
|
|
29
|
-
#
|
|
30
|
-
# @param app [Rails::Application] the Rails application instance
|
|
31
|
-
initializer 'verikloak_audience.middleware' do |app|
|
|
32
|
-
# Insert automatically after core verikloak if present
|
|
33
|
-
self.class.insert_middleware(app)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
initializer 'verikloak_audience.configuration' do
|
|
37
|
-
config.after_initialize do
|
|
38
|
-
self.class.apply_verikloak_rails_configuration
|
|
39
|
-
next if Verikloak::Audience::Railtie.skip_configuration_validation?
|
|
40
|
-
|
|
41
|
-
Verikloak::Audience.config.validate!
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Performs the insertion into the middleware stack when the core
|
|
46
|
-
# Verikloak middleware is available and already present. Extracted for
|
|
47
|
-
# testability without requiring a full Rails boot process.
|
|
48
|
-
#
|
|
49
|
-
# Insert the audience middleware after the base Verikloak middleware when
|
|
50
|
-
# both are available on the stack.
|
|
51
|
-
#
|
|
52
|
-
# @param app [#middleware] An object exposing a Rack middleware stack via `#middleware`.
|
|
53
|
-
# @return [void]
|
|
54
|
-
def self.insert_middleware(app)
|
|
55
|
-
return unless defined?(::Verikloak::Middleware)
|
|
56
|
-
|
|
57
|
-
middleware_stack = app.middleware
|
|
58
|
-
return unless middleware_stack.respond_to?(:include?)
|
|
59
|
-
|
|
60
|
-
return if middleware_stack.include?(::Verikloak::Audience::Middleware)
|
|
61
|
-
|
|
62
|
-
unless middleware_stack.include?(::Verikloak::Middleware)
|
|
63
|
-
warn_missing_core_middleware
|
|
64
|
-
return
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
middleware_stack.insert_after ::Verikloak::Middleware, ::Verikloak::Audience::Middleware
|
|
68
|
-
end
|
|
69
|
-
|
|
7
|
+
# Warning messages for Railtie.
|
|
8
|
+
module RailtieWarnings
|
|
70
9
|
WARNING_MESSAGE = <<~MSG
|
|
71
10
|
[verikloak-audience] Skipping automatic middleware insertion because ::Verikloak::Middleware
|
|
72
11
|
is not present in the Rails middleware stack.
|
|
@@ -82,184 +21,270 @@ module Verikloak
|
|
|
82
21
|
middleware is inserted.
|
|
83
22
|
MSG
|
|
84
23
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
24
|
+
CORE_NOT_CONFIGURED_WARNING = <<~MSG
|
|
25
|
+
[verikloak-audience] Skipping automatic middleware insertion because verikloak-rails
|
|
26
|
+
is not fully configured (discovery_url is not set).
|
|
27
|
+
|
|
28
|
+
The audience middleware requires the core Verikloak middleware to be present in the stack.
|
|
29
|
+
Please configure verikloak-rails first by setting the discovery_url in your initializer.
|
|
30
|
+
MSG
|
|
31
|
+
|
|
32
|
+
UNCONFIGURED_WARNING = <<~MSG
|
|
33
|
+
[verikloak-audience] Skipping configuration validation because required_aud is not configured.
|
|
34
|
+
|
|
35
|
+
To use verikloak-audience, you must configure at least one required audience.
|
|
36
|
+
Run `rails g verikloak:audience:install` to generate the initializer, or add:
|
|
37
|
+
|
|
38
|
+
Verikloak::Audience.configure do |config|
|
|
39
|
+
config.required_aud = ['your-audience']
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
WARNING: Without this configuration, ALL requests will be rejected with 403.
|
|
43
|
+
MSG
|
|
44
|
+
|
|
45
|
+
def log_warning(message)
|
|
94
46
|
logger = (::Rails.logger if defined?(::Rails) && ::Rails.respond_to?(:logger))
|
|
47
|
+
logger ? logger.warn(message) : Kernel.warn(message)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def warn_core_not_configured = log_warning(CORE_NOT_CONFIGURED_WARNING)
|
|
51
|
+
def warn_missing_core_middleware = log_warning(WARNING_MESSAGE)
|
|
52
|
+
def warn_unconfigured = log_warning(UNCONFIGURED_WARNING)
|
|
53
|
+
end
|
|
95
54
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
55
|
+
# Helper methods for Railtie configuration sync.
|
|
56
|
+
module RailtieHelpers
|
|
57
|
+
include RailtieWarnings
|
|
58
|
+
|
|
59
|
+
def apply_verikloak_rails_configuration
|
|
60
|
+
rails_config = verikloak_rails_config
|
|
61
|
+
return unless rails_config
|
|
62
|
+
|
|
63
|
+
Verikloak::Audience.configure do |cfg|
|
|
64
|
+
sync_env_claims_key(cfg, rails_config)
|
|
65
|
+
sync_required_aud(cfg, rails_config)
|
|
66
|
+
sync_resource_client(cfg, rails_config)
|
|
67
|
+
sync_skip_paths(cfg, rails_config)
|
|
100
68
|
end
|
|
101
69
|
end
|
|
102
70
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
71
|
+
def discovery_url_configured?(value)
|
|
72
|
+
return false unless value
|
|
73
|
+
return !value.blank? if value.respond_to?(:blank?)
|
|
74
|
+
return !value.empty? if value.respond_to?(:empty?)
|
|
107
75
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
# temporarily skip validation to let the install task complete.
|
|
111
|
-
#
|
|
112
|
-
# @return [Boolean]
|
|
113
|
-
def self.skip_configuration_validation?
|
|
114
|
-
tokens = first_cli_tokens
|
|
115
|
-
return false if tokens.empty?
|
|
76
|
+
true
|
|
77
|
+
end
|
|
116
78
|
|
|
117
|
-
|
|
118
|
-
return true if
|
|
79
|
+
def middleware_not_found_error?(error)
|
|
80
|
+
return true if error.class.name.to_s.include?('MiddlewareNotFound')
|
|
119
81
|
|
|
120
|
-
|
|
82
|
+
message = error.message.to_s
|
|
83
|
+
message.include?('No such middleware') ||
|
|
84
|
+
message.include?('does not exist') ||
|
|
85
|
+
message.match?(/middleware.*not found/i)
|
|
121
86
|
end
|
|
122
87
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# relevant for generator detection, so we keep the return list short.
|
|
126
|
-
#
|
|
127
|
-
# @return [Array<String>] ordered CLI tokens that may signal a generator
|
|
128
|
-
def self.first_cli_tokens
|
|
129
|
-
tokens = []
|
|
88
|
+
def verikloak_install_generator?(command)
|
|
89
|
+
return false unless command.is_a?(String)
|
|
130
90
|
|
|
91
|
+
command.start_with?('verikloak:') && command.end_with?(':install')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def first_cli_tokens
|
|
95
|
+
tokens = []
|
|
131
96
|
ARGV.each do |arg|
|
|
132
|
-
next if arg.start_with?('-')
|
|
133
|
-
next if arg == 'rails'
|
|
97
|
+
next if arg.start_with?('-') || arg == 'rails'
|
|
134
98
|
|
|
135
99
|
tokens << arg
|
|
136
100
|
break if tokens.size >= 2
|
|
137
101
|
end
|
|
138
|
-
|
|
139
102
|
tokens
|
|
140
103
|
end
|
|
141
104
|
|
|
142
|
-
|
|
143
|
-
# generator (e.g. `verikloak:install`, `verikloak:pundit:install`).
|
|
144
|
-
#
|
|
145
|
-
# @param command [String, nil] first non-option argument from ARGV
|
|
146
|
-
# @return [Boolean]
|
|
147
|
-
def self.verikloak_install_generator?(command)
|
|
148
|
-
return false unless command.is_a?(String)
|
|
105
|
+
private
|
|
149
106
|
|
|
150
|
-
|
|
107
|
+
def verikloak_rails_config
|
|
108
|
+
return unless defined?(::Verikloak::Rails)
|
|
109
|
+
return unless ::Verikloak::Rails.respond_to?(:config)
|
|
110
|
+
|
|
111
|
+
::Verikloak::Rails.config
|
|
112
|
+
rescue StandardError
|
|
113
|
+
nil
|
|
151
114
|
end
|
|
152
115
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
# Aligns env_claims_key, required_aud, and resource_client defaults so
|
|
156
|
-
# that both gems operate on the same Rack env payload and audience list.
|
|
157
|
-
#
|
|
158
|
-
# @return [void]
|
|
159
|
-
def apply_verikloak_rails_configuration
|
|
160
|
-
rails_config = verikloak_rails_config
|
|
161
|
-
return unless rails_config
|
|
162
|
-
|
|
163
|
-
Verikloak::Audience.configure do |cfg|
|
|
164
|
-
sync_env_claims_key(cfg, rails_config)
|
|
165
|
-
sync_required_aud(cfg, rails_config)
|
|
166
|
-
sync_resource_client(cfg, rails_config)
|
|
167
|
-
end
|
|
168
|
-
end
|
|
116
|
+
def sync_env_claims_key(cfg, rails_config)
|
|
117
|
+
return unless rails_config.respond_to?(:user_env_key)
|
|
169
118
|
|
|
170
|
-
|
|
119
|
+
user_key = rails_config.user_env_key
|
|
120
|
+
return if blank?(user_key)
|
|
171
121
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
# @return [Verikloak::Rails::Configuration, nil]
|
|
175
|
-
def verikloak_rails_config
|
|
176
|
-
return unless defined?(::Verikloak::Rails)
|
|
177
|
-
return unless ::Verikloak::Rails.respond_to?(:config)
|
|
122
|
+
current = cfg.env_claims_key
|
|
123
|
+
return unless current.nil? || current == Verikloak::Audience::Configuration::DEFAULT_ENV_CLAIMS_KEY
|
|
178
124
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
nil
|
|
182
|
-
end
|
|
125
|
+
cfg.env_claims_key = user_key
|
|
126
|
+
end
|
|
183
127
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
# @param rails_config [Verikloak::Rails::Configuration]
|
|
188
|
-
# @return [void]
|
|
189
|
-
def sync_env_claims_key(cfg, rails_config)
|
|
190
|
-
return unless rails_config.respond_to?(:user_env_key)
|
|
128
|
+
def sync_required_aud(cfg, rails_config)
|
|
129
|
+
return unless cfg_required_aud_blank?(cfg)
|
|
130
|
+
return unless rails_config.respond_to?(:audience)
|
|
191
131
|
|
|
192
|
-
|
|
193
|
-
|
|
132
|
+
audiences = normalized_audiences(rails_config.audience)
|
|
133
|
+
return if audiences.empty?
|
|
194
134
|
|
|
195
|
-
|
|
196
|
-
|
|
135
|
+
cfg.required_aud = audiences.size == 1 ? audiences.first : audiences
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def sync_resource_client(cfg, rails_config)
|
|
139
|
+
return unless rails_config.respond_to?(:audience)
|
|
140
|
+
|
|
141
|
+
audiences = normalized_audiences(rails_config.audience)
|
|
142
|
+
return unless audiences.size == 1
|
|
197
143
|
|
|
198
|
-
|
|
144
|
+
current_client = cfg.resource_client
|
|
145
|
+
unless blank?(current_client) || current_client == Verikloak::Audience::Configuration::DEFAULT_RESOURCE_CLIENT
|
|
146
|
+
return
|
|
199
147
|
end
|
|
200
148
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
149
|
+
cfg.resource_client = audiences.first
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def sync_skip_paths(cfg, rails_config)
|
|
153
|
+
return unless rails_config.respond_to?(:skip_paths)
|
|
154
|
+
|
|
155
|
+
paths = rails_config.skip_paths
|
|
156
|
+
return if paths.nil?
|
|
157
|
+
return unless cfg.skip_paths.nil? || cfg.skip_paths.empty?
|
|
158
|
+
|
|
159
|
+
cfg.skip_paths = Array(paths).compact
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def cfg_required_aud_blank?(cfg) = value_blank?(cfg.required_aud)
|
|
163
|
+
|
|
164
|
+
def value_blank?(value)
|
|
165
|
+
return true if value.nil?
|
|
166
|
+
return true if value.respond_to?(:empty?) && value.empty?
|
|
167
|
+
|
|
168
|
+
value.to_s.empty?
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
alias blank? value_blank?
|
|
172
|
+
|
|
173
|
+
def normalized_audiences(source)
|
|
174
|
+
Array(source).compact.map(&:to_s).reject(&:empty?)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Rails integration for verikloak-audience.
|
|
179
|
+
#
|
|
180
|
+
# This Railtie automatically inserts {Verikloak::Audience::Middleware}
|
|
181
|
+
# into the Rails middleware stack directly after {::Verikloak::Middleware}
|
|
182
|
+
# when it is present.
|
|
183
|
+
#
|
|
184
|
+
# If the core Verikloak middleware is not available, nothing is inserted.
|
|
185
|
+
# You may still customize the placement in your application via
|
|
186
|
+
# `config.middleware`.
|
|
187
|
+
#
|
|
188
|
+
# @example Configure placement and options
|
|
189
|
+
# # config/application.rb
|
|
190
|
+
# config.middleware.insert_after Verikloak::Middleware,
|
|
191
|
+
# Verikloak::Audience::Middleware,
|
|
192
|
+
# profile: :allow_account,
|
|
193
|
+
# required_aud: ['rails-api'],
|
|
194
|
+
# resource_client: 'rails-api',
|
|
195
|
+
# env_claims_key: 'verikloak.user',
|
|
196
|
+
# suggest_in_logs: true
|
|
197
|
+
class Railtie < ::Rails::Railtie
|
|
198
|
+
class << self
|
|
199
|
+
attr_accessor :middleware_insertion_attempted, :unconfigured_warning_emitted
|
|
200
|
+
|
|
201
|
+
include RailtieHelpers
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
@middleware_insertion_attempted = false
|
|
205
|
+
@unconfigured_warning_emitted = false
|
|
206
|
+
|
|
207
|
+
initializer 'verikloak_audience.middleware',
|
|
208
|
+
after: 'verikloak.configure',
|
|
209
|
+
before: :build_middleware_stack do |app|
|
|
210
|
+
self.class.apply_verikloak_rails_configuration
|
|
211
|
+
self.class.insert_middleware(app)
|
|
212
|
+
end
|
|
209
213
|
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
initializer 'verikloak_audience.configuration' do
|
|
215
|
+
config.after_initialize do
|
|
216
|
+
next if Verikloak::Audience::Railtie.skip_configuration_validation?
|
|
217
|
+
next if Verikloak::Audience::Railtie.skip_unconfigured_validation?
|
|
212
218
|
|
|
213
|
-
|
|
219
|
+
Verikloak::Audience.config.validate!
|
|
214
220
|
end
|
|
221
|
+
end
|
|
215
222
|
|
|
216
|
-
|
|
217
|
-
#
|
|
218
|
-
# @param cfg [Verikloak::Audience::Configuration]
|
|
219
|
-
# @param rails_config [Verikloak::Rails::Configuration]
|
|
220
|
-
# @return [void]
|
|
221
|
-
def sync_resource_client(cfg, rails_config)
|
|
222
|
-
return unless rails_config.respond_to?(:audience)
|
|
223
|
+
COMMANDS_SKIPPING_VALIDATION = %w[generate g destroy d].freeze
|
|
223
224
|
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
def self.insert_middleware(app)
|
|
226
|
+
return unless defined?(::Verikloak::Middleware)
|
|
226
227
|
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
if defined?(::Verikloak::Rails) && ::Verikloak::Rails.respond_to?(:config)
|
|
229
|
+
discovery_url = ::Verikloak::Rails.config.discovery_url
|
|
230
|
+
unless discovery_url_configured?(discovery_url)
|
|
231
|
+
warn_core_not_configured
|
|
229
232
|
return
|
|
230
233
|
end
|
|
231
|
-
|
|
232
|
-
cfg.resource_client = audiences.first
|
|
233
234
|
end
|
|
234
235
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
236
|
+
return if middleware_insertion_attempted
|
|
237
|
+
|
|
238
|
+
middleware_stack = app.respond_to?(:config) ? app.config.middleware : app.middleware
|
|
239
|
+
|
|
240
|
+
if middleware_stack.respond_to?(:include?) && middleware_stack.include?(::Verikloak::Audience::Middleware)
|
|
241
|
+
return
|
|
241
242
|
end
|
|
242
243
|
|
|
243
|
-
|
|
244
|
-
#
|
|
245
|
-
# @param value [Object]
|
|
246
|
-
# @return [Boolean]
|
|
247
|
-
def value_blank?(value)
|
|
248
|
-
return true if value.nil?
|
|
249
|
-
return true if value.respond_to?(:empty?) && value.empty?
|
|
244
|
+
self.middleware_insertion_attempted = true
|
|
250
245
|
|
|
251
|
-
|
|
246
|
+
begin
|
|
247
|
+
middleware_stack.insert_after ::Verikloak::Middleware, ::Verikloak::Audience::Middleware
|
|
248
|
+
rescue StandardError => e
|
|
249
|
+
raise unless middleware_not_found_error?(e)
|
|
250
|
+
|
|
251
|
+
warn_missing_core_middleware
|
|
252
252
|
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def self.reset_middleware_insertion_flag!
|
|
256
|
+
self.middleware_insertion_attempted = false
|
|
257
|
+
end
|
|
253
258
|
|
|
254
|
-
|
|
259
|
+
def self.skip_configuration_validation?
|
|
260
|
+
tokens = first_cli_tokens
|
|
261
|
+
return false if tokens.empty?
|
|
255
262
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
263
|
+
return true if COMMANDS_SKIPPING_VALIDATION.include?(tokens.first)
|
|
264
|
+
|
|
265
|
+
tokens.any? { |token| verikloak_install_generator?(token) }
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def self.skip_unconfigured_validation?
|
|
269
|
+
audiences_unconfigured?
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def self.audiences_unconfigured?
|
|
273
|
+
audiences = Verikloak::Audience.config.required_aud_list
|
|
274
|
+
|
|
275
|
+
if audiences.empty?
|
|
276
|
+
warn_unconfigured_once
|
|
277
|
+
return true
|
|
262
278
|
end
|
|
279
|
+
|
|
280
|
+
false
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def self.warn_unconfigured_once
|
|
284
|
+
return if unconfigured_warning_emitted
|
|
285
|
+
|
|
286
|
+
self.unconfigured_warning_emitted = true
|
|
287
|
+
warn_unconfigured
|
|
263
288
|
end
|
|
264
289
|
end
|
|
265
290
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: verikloak-audience
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- taiyaky
|
|
@@ -76,7 +76,7 @@ metadata:
|
|
|
76
76
|
source_code_uri: https://github.com/taiyaky/verikloak-audience
|
|
77
77
|
changelog_uri: https://github.com/taiyaky/verikloak-audience/blob/main/CHANGELOG.md
|
|
78
78
|
bug_tracker_uri: https://github.com/taiyaky/verikloak-audience/issues
|
|
79
|
-
documentation_uri: https://rubydoc.info/gems/verikloak-audience/0.
|
|
79
|
+
documentation_uri: https://rubydoc.info/gems/verikloak-audience/0.3.1
|
|
80
80
|
rubygems_mfa_required: 'true'
|
|
81
81
|
rdoc_options: []
|
|
82
82
|
require_paths:
|