verikloak-audience 1.0.0 → 1.1.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 +4 -4
- data/CHANGELOG.md +27 -0
- data/ERRORS.md +48 -0
- data/MAINTAINERS.md +42 -0
- data/README.md +1 -1
- data/lib/verikloak/audience/checker.rb +41 -21
- data/lib/verikloak/audience/configuration.rb +38 -4
- data/lib/verikloak/audience/middleware.rb +59 -22
- data/lib/verikloak/audience/railtie.rb +27 -15
- data/lib/verikloak/audience/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 14ece6bb132264abcd755a0904c92d4f8b4f63b3615e25dd76c9101b2af04a96
|
|
4
|
+
data.tar.gz: f546f6e3510e3453a62726cf6b2304653f78d9939f500a0ee2fff076332dd327
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 27062866e3d84df87c309ca11d29cdcd39b6b85909a572498c12ba88cce3d7ff135963de5fb0b560a8380340872dda10c1a81376962b86bcb3394398aab8567a
|
|
7
|
+
data.tar.gz: '08c8ea868d6db70f9f738a6967d49276bd9b72473c528a0302ea4f4aeadbe6c383ae7cdd153c25a7dcb2857ca35be26f5abc87eac2764fd12b859204536c0390'
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.1.0] - 2026-07-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Boot-time profile validation**: `Configuration#validate!` now rejects unknown `profile` values, so misspellings (e.g. `:strict_signle` instead of `:strict_single`) fail fast at startup instead of raising on the first request
|
|
14
|
+
- **`Configuration#normalized_profile` / `#validated_profile`**: Shared profile coercion and validation helpers (Symbol coercion, default fallback, unknown-profile check) used by both `validate!` and `Checker`
|
|
15
|
+
- **JSON 500 for request-time configuration errors**: When a `ConfigurationError` surfaces during a request (normally prevented by boot-time validation), the middleware now renders a JSON error response (`audience_configuration_error`, status 500) instead of leaking the exception through the Rack stack; the response body stays generic and the failure detail is written to the request logger
|
|
16
|
+
- **`Checker.observed_audiences`**: Public helper returning the normalized audience values seen in a claims payload, used by the middleware so rejection logs show the same audiences the checker evaluated (including duck-typed `#to_hash` claims)
|
|
17
|
+
- **CI Ruby matrix**: RSpec now runs against Ruby 3.1 / 3.2 / 3.3 / 3.4 (all series supported by the gemspec) — the Docker-based job covers the development Ruby (3.4) and a non-Docker `ruby/setup-ruby` matrix covers 3.1–3.3, mirroring verikloak core's CI layout
|
|
18
|
+
- **Coverage floor**: SimpleCov now enforces a minimum line coverage of 90% in CI (not enforced for focused local runs)
|
|
19
|
+
- **MAINTAINERS.md**: Release instructions referenced from the README now exist
|
|
20
|
+
- Tests for the `Forbidden`/`Error`/`ConfigurationError` classes, the Railtie `discovery_url` guard, the `REQUEST_PATH` fallback, and Symbol inputs to the public checker predicates
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- **`Checker.suggest` accepts a `fallback:` keyword** (default `:strict_single`, preserving the 1.0 always-Symbol contract): the middleware passes `fallback: nil`, so rejection logs read `no profile matches the observed aud` instead of naming a misleading suggestion
|
|
24
|
+
- **Middleware boot-time validation now evaluates the middleware's own (post-override) configuration**, so options passed via `config.middleware.use` are validated at startup even when the global configuration is untouched
|
|
25
|
+
- Values synced from verikloak-rails now follow its own resolution rules: `user_env_key` is whitespace-stripped and blank values are treated as unset (both middlewares then share the `verikloak.user` default, mirroring `effective_user_env_key` in verikloak-rails >= 1.2), and blank `required_aud`/`resource_client` are treated as unset
|
|
26
|
+
- Runtime dependency raised to `verikloak ~> 1.1` (was `~> 1.0`); lockfile updated to verikloak 1.1.0
|
|
27
|
+
- `Checker::VALID_PROFILES` is now an alias of `Configuration::VALID_PROFILES` (same values; the canonical definition moved)
|
|
28
|
+
- All public checker predicates (`strict_single?`, `allow_account?`, `any_match?`) now coerce `required` inputs consistently, accepting Symbols and single values
|
|
29
|
+
- `Railtie.skip_validation?` combines the generator-mode and unconfigured checks and takes the configuration under validation as an argument; the middleware and the after-initialize hook now share it
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- **CI coverage was never measured**: The `SIMPLECOV` environment variable set in GitHub Actions never reached the Docker container; `compose.yml` now passes it through, so coverage reports are actually generated in CI
|
|
33
|
+
- Removed dead code paths (`env_key&.to_sym` safe navigation, unreachable `nil` check in `sync_env_claims_key`) and redundant `module_function` declarations in `Checker`
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
10
37
|
## [1.0.0] - 2026-02-15
|
|
11
38
|
|
|
12
39
|
### Fixed
|
data/ERRORS.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Verikloak Audience Error Reference
|
|
2
|
+
|
|
3
|
+
This document captures the *current* error behaviour implemented by the `verikloak-audience` gem. Use it as a developer aid when wiring the middleware or surfacing audience failures to callers.
|
|
4
|
+
|
|
5
|
+
## Response Shape
|
|
6
|
+
- Status: `403 Forbidden` (or `500 Internal Server Error` for configuration failures, see below)
|
|
7
|
+
- Headers: `Content-Type: application/json`
|
|
8
|
+
- Body: `{ "error": <code>, "message": <text> }`
|
|
9
|
+
- Authentication headers: none added by this gem (the upstream Verikloak middleware already emits `WWW-Authenticate`).
|
|
10
|
+
|
|
11
|
+
Example 403 response:
|
|
12
|
+
```http
|
|
13
|
+
HTTP/1.1 403 Forbidden
|
|
14
|
+
Content-Type: application/json
|
|
15
|
+
|
|
16
|
+
{"error":"insufficient_audience","message":"Audience not acceptable for profile strict_single"}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Error Catalog
|
|
20
|
+
|
|
21
|
+
| Code | HTTP | Message template | Trigger |
|
|
22
|
+
|--------------------------------|------|----------------------------------------------------|---------|
|
|
23
|
+
| `insufficient_audience` | 403 | `Audience not acceptable for profile <profile>` | Returned whenever the configured audience profile does not match the claims loaded from `env[config.env_claims_key]`. |
|
|
24
|
+
| `audience_configuration_error` | 500 | `audience configuration error` | Returned when a configuration problem (e.g. an unknown `profile`) is detected while evaluating a request. The body stays generic; the failure detail (e.g. the invalid profile value) is written to the request logger only. Boot-time validation (`Configuration#validate!`) normally catches this before any request is served; this response guards deployments where validation was skipped (e.g. an unconfigured Rails boot). |
|
|
25
|
+
|
|
26
|
+
### Trigger details
|
|
27
|
+
- Claims are read from `env[config.env_claims_key]` (default: `"verikloak.user"`). Missing claims are treated as `{}`.
|
|
28
|
+
- Validation is delegated to `Verikloak::Audience::Checker.ok?`, which supports profiles `:strict_single`, `:allow_account`, `:any_match`, and `:resource_or_aud`.
|
|
29
|
+
- When validation fails and `config.suggest_in_logs` is true (default), the middleware emits a `warn` message to STDERR such as:
|
|
30
|
+
```
|
|
31
|
+
[verikloak-audience] insufficient_audience; suggestion profile=:allow_account aud=["my-api","account"]
|
|
32
|
+
```
|
|
33
|
+
When no profile would accept the observed claims, the log reads `no profile matches the observed aud` instead of naming a profile. This logging side-effect has no impact on the HTTP response.
|
|
34
|
+
|
|
35
|
+
## Exception Classes
|
|
36
|
+
|
|
37
|
+
| Class | Default code | Default status | Notes |
|
|
38
|
+
|--------------------------------------|----------------------------|----------------|-------|
|
|
39
|
+
| `Verikloak::Audience::Error` | `audience_error` | 403 | Base class exposing `#code` and `#http_status`; not raised automatically by the middleware but available for host apps. |
|
|
40
|
+
| `Verikloak::Audience::Forbidden` | `insufficient_audience` | 403 | Thin wrapper around `Error`. Useful if your application raises exceptions instead of returning JSON responses directly. |
|
|
41
|
+
| `Verikloak::Audience::ConfigurationError` | `audience_configuration_error` | 500 | Raised by `Configuration#validate!` and configuration writers when settings are missing or inconsistent; rendered as the 500 JSON response if it surfaces during a request. |
|
|
42
|
+
|
|
43
|
+
These classes exist for integration purposes (e.g., controller helpers) and mirror the JSON shape returned by the Rack middleware. If you rescue them inside a Rails app, you can map them to the same 403 payload shown above.
|
|
44
|
+
|
|
45
|
+
## Operational Notes
|
|
46
|
+
- Audience mismatches always resolve to the single 403 response. The only other status this gem emits is the 500 `audience_configuration_error` response described above, which signals a misconfiguration rather than a client failure.
|
|
47
|
+
- Upstream Verikloak middleware remains responsible for token verification errors (401/503/etc.). Place `Verikloak::Audience::Middleware` after it so that verified claims are available for audience checks.
|
|
48
|
+
- Adjust `config.required_aud`, `config.profile`, and `config.resource_client` to control what qualifies as a passing audience.
|
data/MAINTAINERS.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Maintainers Guide
|
|
2
|
+
|
|
3
|
+
Release instructions for `verikloak-audience`. For contribution guidelines, see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Push access to this repository and ownership of the gem on [RubyGems.org](https://rubygems.org/gems/verikloak-audience)
|
|
8
|
+
- MFA enabled on your RubyGems account (`rubygems_mfa_required` is set in the gemspec, so pushes without MFA are rejected)
|
|
9
|
+
|
|
10
|
+
## Release checklist
|
|
11
|
+
|
|
12
|
+
1. Ensure `main` is green (RSpec across all supported Rubies, RuboCop, bundler-audit).
|
|
13
|
+
2. Bump the version in `lib/verikloak/audience/version.rb` following [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
14
|
+
3. Add a dated entry to `CHANGELOG.md` describing the changes (Keep a Changelog format).
|
|
15
|
+
4. Update `Gemfile.lock` so the lockfile records the new version:
|
|
16
|
+
```bash
|
|
17
|
+
docker compose run --rm dev bundle install
|
|
18
|
+
```
|
|
19
|
+
5. Run the full test suite one more time:
|
|
20
|
+
```bash
|
|
21
|
+
docker compose run --rm dev rspec
|
|
22
|
+
docker compose run --rm dev rubocop
|
|
23
|
+
```
|
|
24
|
+
6. Commit the release changes and open a pull request. Merge once CI passes.
|
|
25
|
+
7. Tag the merge commit on `main` and push the tag:
|
|
26
|
+
```bash
|
|
27
|
+
git tag v<VERSION>
|
|
28
|
+
git push origin v<VERSION>
|
|
29
|
+
```
|
|
30
|
+
8. Build and publish the gem:
|
|
31
|
+
```bash
|
|
32
|
+
gem build verikloak-audience.gemspec
|
|
33
|
+
gem push verikloak-audience-<VERSION>.gem
|
|
34
|
+
```
|
|
35
|
+
9. Verify the release on [RubyGems.org](https://rubygems.org/gems/verikloak-audience) and check that
|
|
36
|
+
[rubydoc.info](https://rubydoc.info/gems/verikloak-audience) picks up the new version.
|
|
37
|
+
|
|
38
|
+
## Compatibility policy
|
|
39
|
+
|
|
40
|
+
- Keep the runtime dependency pinned to the current minor floor of the core gem (currently `verikloak ~> 1.1`).
|
|
41
|
+
- Supported Ruby versions follow the gemspec (`>= 3.1`); the CI matrix must cover every supported series.
|
|
42
|
+
- Coordinate breaking changes with the other gems in the Verikloak family (verikloak, verikloak-rails, verikloak-bff, verikloak-pundit) so error hierarchies and Rack env keys stay aligned.
|
data/README.md
CHANGED
|
@@ -69,7 +69,7 @@ See [`examples/rack.ru`](examples/rack.ru) for a full Rack sample. In Rails, alw
|
|
|
69
69
|
`env_claims_key` assumes the preceding `Verikloak::Middleware` populates the Rack env. If the middleware order changes, claims will be missing and the audience check will always reject.
|
|
70
70
|
|
|
71
71
|
### Operational safeguards
|
|
72
|
-
- Middleware initialisation now fails fast when `required_aud` is empty. When Rails loads via the supplied Railtie, `Verikloak::Audience.config.validate!` runs after boot so configuration mistakes surface during startup instead of returning 403 for every request.
|
|
72
|
+
- Middleware initialisation now fails fast when `required_aud` is empty or `profile` is not one of the supported profiles. When Rails loads via the supplied Railtie, `Verikloak::Audience.config.validate!` runs after boot so configuration mistakes surface during startup instead of returning 403 for every request. If a configuration problem still slips through to request time (e.g. validation was skipped during an unconfigured boot), the middleware responds with a generic JSON 500 `audience_configuration_error` and logs the failure detail server-side instead of leaking an exception.
|
|
73
73
|
- When audience validation fails, the middleware consults `env['verikloak.logger']`, `env['rack.logger']`, and `env['action_dispatch.logger']` (in that order) before falling back to Ruby's `Kernel#warn`, keeping failure logs consistent with Rails and Verikloak observers.
|
|
74
74
|
- For the `:resource_or_aud` profile, `resource_client` must match one of the values in `required_aud`. A single-element `required_aud` automatically infers the client id, ensuring the same client identifier is shared with downstream BFF/Pundit integrations.
|
|
75
75
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'verikloak/audience/configuration'
|
|
4
|
+
|
|
3
5
|
module Verikloak
|
|
4
6
|
module Audience
|
|
5
7
|
# Audience profile checker functions.
|
|
@@ -7,7 +9,9 @@ module Verikloak
|
|
|
7
9
|
# This module provides predicate helpers used by the middleware to decide
|
|
8
10
|
# whether a given set of claims satisfies the configured profile.
|
|
9
11
|
module Checker
|
|
10
|
-
|
|
12
|
+
# Kept for backward compatibility; the canonical definition lives in
|
|
13
|
+
# {Verikloak::Audience::Configuration::VALID_PROFILES}.
|
|
14
|
+
VALID_PROFILES = Verikloak::Audience::Configuration::VALID_PROFILES
|
|
11
15
|
|
|
12
16
|
module_function
|
|
13
17
|
|
|
@@ -15,18 +19,11 @@ module Verikloak
|
|
|
15
19
|
#
|
|
16
20
|
# @param claims [Hash] OIDC claims (expects keys like "aud", "resource_access")
|
|
17
21
|
# @param cfg [Verikloak::Audience::Configuration]
|
|
22
|
+
# @raise [ConfigurationError] when the configured profile is unknown
|
|
18
23
|
# @return [Boolean]
|
|
19
24
|
def ok?(claims, cfg)
|
|
20
25
|
claims = normalize_claims(claims)
|
|
21
|
-
|
|
22
|
-
profile = cfg.profile
|
|
23
|
-
profile = profile.to_sym if profile.respond_to?(:to_sym)
|
|
24
|
-
profile = :strict_single if profile.nil?
|
|
25
|
-
|
|
26
|
-
unless VALID_PROFILES.include?(profile)
|
|
27
|
-
raise Verikloak::Audience::ConfigurationError,
|
|
28
|
-
"unknown audience profile #{cfg.profile.inspect}"
|
|
29
|
-
end
|
|
26
|
+
profile = cfg.validated_profile
|
|
30
27
|
|
|
31
28
|
case profile
|
|
32
29
|
when :strict_single
|
|
@@ -43,23 +40,25 @@ module Verikloak
|
|
|
43
40
|
# Validate that aud matches required exactly (order-insensitive).
|
|
44
41
|
#
|
|
45
42
|
# @param claims [Hash]
|
|
46
|
-
# @param required [Array<String
|
|
43
|
+
# @param required [Array<String,Symbol>, String, Symbol]
|
|
47
44
|
# @return [Boolean]
|
|
48
45
|
def strict_single?(claims, required)
|
|
49
46
|
aud = normalized_audiences(claims)
|
|
47
|
+
required = normalized_required(required)
|
|
50
48
|
return false if required.empty?
|
|
51
49
|
|
|
52
50
|
# Must contain all required and have no unexpected extra (order-insensitive)
|
|
53
|
-
|
|
51
|
+
aud.sort == required.sort
|
|
54
52
|
end
|
|
55
53
|
|
|
56
54
|
# Validate aud allowing "account" as an extra value.
|
|
57
55
|
#
|
|
58
56
|
# @param claims [Hash]
|
|
59
|
-
# @param required [Array<String
|
|
57
|
+
# @param required [Array<String,Symbol>, String, Symbol]
|
|
60
58
|
# @return [Boolean]
|
|
61
59
|
def allow_account?(claims, required)
|
|
62
60
|
aud = normalized_audiences(claims)
|
|
61
|
+
required = normalized_required(required)
|
|
63
62
|
return false if required.empty?
|
|
64
63
|
|
|
65
64
|
# Permit 'account' extra
|
|
@@ -72,14 +71,15 @@ module Verikloak
|
|
|
72
71
|
# More permissive than :strict_single; useful when multiple clients share audiences.
|
|
73
72
|
#
|
|
74
73
|
# @param claims [Hash]
|
|
75
|
-
# @param required [Array<String
|
|
74
|
+
# @param required [Array<String,Symbol>, String, Symbol]
|
|
76
75
|
# @return [Boolean]
|
|
77
76
|
def any_match?(claims, required)
|
|
78
77
|
aud = normalized_audiences(claims)
|
|
78
|
+
required = normalized_required(required)
|
|
79
79
|
return false if required.empty?
|
|
80
80
|
|
|
81
81
|
# At least one of the required audiences must be present
|
|
82
|
-
aud.intersect?(required
|
|
82
|
+
aud.intersect?(required)
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
# Permit when resource roles exist for the client; otherwise fallback to
|
|
@@ -87,7 +87,7 @@ module Verikloak
|
|
|
87
87
|
#
|
|
88
88
|
# @param claims [Hash]
|
|
89
89
|
# @param client [String]
|
|
90
|
-
# @param required [Array<String
|
|
90
|
+
# @param required [Array<String,Symbol>, String, Symbol]
|
|
91
91
|
# @return [Boolean]
|
|
92
92
|
def resource_or_aud?(claims, client, required)
|
|
93
93
|
roles = Array(claims.dig('resource_access', client, 'roles')).compact.reject { |r| r.to_s.empty? }
|
|
@@ -101,8 +101,11 @@ module Verikloak
|
|
|
101
101
|
#
|
|
102
102
|
# @param claims [Hash]
|
|
103
103
|
# @param cfg [Verikloak::Audience::Configuration]
|
|
104
|
-
# @
|
|
105
|
-
|
|
104
|
+
# @param fallback [Symbol, nil] value returned when no profile accepts
|
|
105
|
+
# the claims (defaults to :strict_single, preserving the 1.0 contract)
|
|
106
|
+
# @return [:strict_single, :allow_account, :any_match, :resource_or_aud, nil]
|
|
107
|
+
# the most fitting profile, or the fallback when none accepts
|
|
108
|
+
def suggest(claims, cfg, fallback: :strict_single)
|
|
106
109
|
claims = normalize_claims(claims)
|
|
107
110
|
|
|
108
111
|
required = cfg.required_aud_list
|
|
@@ -111,7 +114,16 @@ module Verikloak
|
|
|
111
114
|
return :any_match if any_match?(claims, required)
|
|
112
115
|
return :resource_or_aud if resource_or_aud?(claims, cfg.resource_client.to_s, required)
|
|
113
116
|
|
|
114
|
-
|
|
117
|
+
fallback
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Audience values observed in the claims, normalized exactly like the
|
|
121
|
+
# profile checks normalize them (useful for consistent logging).
|
|
122
|
+
#
|
|
123
|
+
# @param claims [Object] raw claims value (Hash or #to_hash)
|
|
124
|
+
# @return [Array<String>]
|
|
125
|
+
def observed_audiences(claims)
|
|
126
|
+
normalized_audiences(normalize_claims(claims))
|
|
115
127
|
end
|
|
116
128
|
|
|
117
129
|
# Normalize incoming claims to a Hash to guard against unexpected
|
|
@@ -133,7 +145,6 @@ module Verikloak
|
|
|
133
145
|
warn "[Verikloak::Audience] normalize_claims failed: #{e.class}: #{e.message}" if $DEBUG
|
|
134
146
|
{}
|
|
135
147
|
end
|
|
136
|
-
module_function :normalize_claims
|
|
137
148
|
private_class_method :normalize_claims
|
|
138
149
|
|
|
139
150
|
# Normalize audience claims into a predictable array of strings.
|
|
@@ -143,8 +154,17 @@ module Verikloak
|
|
|
143
154
|
def normalized_audiences(claims)
|
|
144
155
|
Array(claims['aud']).map(&:to_s)
|
|
145
156
|
end
|
|
146
|
-
module_function :normalized_audiences
|
|
147
157
|
private_class_method :normalized_audiences
|
|
158
|
+
|
|
159
|
+
# Coerce a required-audience input into an array of strings so that the
|
|
160
|
+
# public predicates accept Symbols and single values consistently.
|
|
161
|
+
#
|
|
162
|
+
# @param required [Array<String,Symbol>, String, Symbol, nil]
|
|
163
|
+
# @return [Array<String>]
|
|
164
|
+
def normalized_required(required)
|
|
165
|
+
Array(required).map(&:to_s)
|
|
166
|
+
end
|
|
167
|
+
private_class_method :normalized_required
|
|
148
168
|
end
|
|
149
169
|
end
|
|
150
170
|
end
|
|
@@ -28,6 +28,10 @@ module Verikloak
|
|
|
28
28
|
class Configuration
|
|
29
29
|
DEFAULT_RESOURCE_CLIENT = 'rails-api'
|
|
30
30
|
DEFAULT_ENV_CLAIMS_KEY = 'verikloak.user'
|
|
31
|
+
DEFAULT_PROFILE = :strict_single
|
|
32
|
+
|
|
33
|
+
# Enforcement profiles supported by {Verikloak::Audience::Checker}.
|
|
34
|
+
VALID_PROFILES = %i[strict_single allow_account any_match resource_or_aud].freeze
|
|
31
35
|
|
|
32
36
|
attr_accessor :profile, :required_aud, :resource_client,
|
|
33
37
|
:suggest_in_logs, :skip_paths
|
|
@@ -37,7 +41,7 @@ module Verikloak
|
|
|
37
41
|
#
|
|
38
42
|
# @return [void]
|
|
39
43
|
def initialize
|
|
40
|
-
@profile =
|
|
44
|
+
@profile = DEFAULT_PROFILE
|
|
41
45
|
@required_aud = []
|
|
42
46
|
@resource_client = DEFAULT_RESOURCE_CLIENT
|
|
43
47
|
self.env_claims_key = DEFAULT_ENV_CLAIMS_KEY
|
|
@@ -66,6 +70,35 @@ module Verikloak
|
|
|
66
70
|
Array(required_aud).map(&:to_s)
|
|
67
71
|
end
|
|
68
72
|
|
|
73
|
+
# Coerce `profile` into a Symbol, falling back to the default when unset.
|
|
74
|
+
# Values that cannot be symbolized (e.g. an Integer) are returned as-is
|
|
75
|
+
# so {#validated_profile} can reject them with a precise error. The
|
|
76
|
+
# result is not guaranteed to be a member of {VALID_PROFILES}; use
|
|
77
|
+
# {#validated_profile} to enforce that.
|
|
78
|
+
#
|
|
79
|
+
# @return [Symbol, Object]
|
|
80
|
+
def normalized_profile
|
|
81
|
+
value = profile
|
|
82
|
+
value = value.to_sym if value.respond_to?(:to_sym)
|
|
83
|
+
value.nil? ? DEFAULT_PROFILE : value
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Coerce `profile` into a Symbol and ensure it is one of
|
|
87
|
+
# {VALID_PROFILES}. Single source of the unknown-profile check shared
|
|
88
|
+
# by {#validate!} and {Verikloak::Audience::Checker.ok?}.
|
|
89
|
+
#
|
|
90
|
+
# @raise [ConfigurationError] when the profile is unknown
|
|
91
|
+
# @return [Symbol]
|
|
92
|
+
def validated_profile
|
|
93
|
+
profile_name = normalized_profile
|
|
94
|
+
unless VALID_PROFILES.include?(profile_name)
|
|
95
|
+
raise Verikloak::Audience::ConfigurationError,
|
|
96
|
+
"unknown audience profile #{profile.inspect}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
profile_name
|
|
100
|
+
end
|
|
101
|
+
|
|
69
102
|
# @param value [#to_s, nil]
|
|
70
103
|
# @raise [ConfigurationError] when value is nil or blank
|
|
71
104
|
# @return [void]
|
|
@@ -82,6 +115,9 @@ module Verikloak
|
|
|
82
115
|
|
|
83
116
|
# Validate the configuration to ensure required values are present.
|
|
84
117
|
#
|
|
118
|
+
# @raise [ConfigurationError] when `required_aud` is empty, `profile` is
|
|
119
|
+
# not one of {VALID_PROFILES}, or `resource_client` conflicts with
|
|
120
|
+
# `required_aud` under the `:resource_or_aud` profile
|
|
85
121
|
# @return [Configuration] the validated configuration
|
|
86
122
|
def validate!
|
|
87
123
|
audiences = required_aud_list
|
|
@@ -90,9 +126,7 @@ module Verikloak
|
|
|
90
126
|
'required_aud must include at least one audience'
|
|
91
127
|
end
|
|
92
128
|
|
|
93
|
-
profile_name =
|
|
94
|
-
profile_name = profile_name.to_sym if profile_name.respond_to?(:to_sym)
|
|
95
|
-
profile_name ||= :strict_single
|
|
129
|
+
profile_name = validated_profile
|
|
96
130
|
|
|
97
131
|
ensure_resource_client!(audiences) if profile_name == :resource_or_aud
|
|
98
132
|
|
|
@@ -42,16 +42,15 @@ module Verikloak
|
|
|
42
42
|
path = env['PATH_INFO'] || env['REQUEST_PATH'] || ''
|
|
43
43
|
return @app.call(env) if skip?(path)
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
suggestion = Checker.suggest(claims, @config)
|
|
51
|
-
aud_view = Array(claims['aud']).inspect
|
|
52
|
-
log_warning(env,
|
|
53
|
-
"[verikloak-audience] insufficient_audience; suggestion profile=:#{suggestion} aud=#{aud_view}")
|
|
45
|
+
claims = read_claims(env)
|
|
46
|
+
begin
|
|
47
|
+
authorized = Checker.ok?(claims, @config)
|
|
48
|
+
rescue Verikloak::Audience::ConfigurationError => e
|
|
49
|
+
return configuration_error_response(env, e)
|
|
54
50
|
end
|
|
51
|
+
return @app.call(env) if authorized
|
|
52
|
+
|
|
53
|
+
log_rejection(env, claims)
|
|
55
54
|
|
|
56
55
|
Verikloak::ErrorResponse.build(
|
|
57
56
|
code: 'insufficient_audience',
|
|
@@ -62,6 +61,50 @@ module Verikloak
|
|
|
62
61
|
|
|
63
62
|
private
|
|
64
63
|
|
|
64
|
+
# Read verified claims from the Rack env under the configured key.
|
|
65
|
+
#
|
|
66
|
+
# @param env [Hash] Rack environment
|
|
67
|
+
# @return [Object] raw claims value (normalized later by {Checker})
|
|
68
|
+
def read_claims(env)
|
|
69
|
+
env_key = @config.env_claims_key
|
|
70
|
+
env[env_key] || env[env_key.to_sym] || {}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Log a configuration failure and render it as a generic JSON error
|
|
74
|
+
# response instead of leaking a raw exception through the Rack stack.
|
|
75
|
+
# The failure detail goes to the server log only, never to the client.
|
|
76
|
+
# Boot-time validation normally prevents this path; it guards
|
|
77
|
+
# deployments where validation was skipped (e.g. unconfigured Rails
|
|
78
|
+
# boot).
|
|
79
|
+
#
|
|
80
|
+
# @param env [Hash] Rack environment
|
|
81
|
+
# @param error [Verikloak::Audience::ConfigurationError]
|
|
82
|
+
# @return [Array(Integer, Hash, #each)] Rack response triple
|
|
83
|
+
def configuration_error_response(env, error)
|
|
84
|
+
log_warning(env, "[verikloak-audience] configuration error: #{error.message}")
|
|
85
|
+
|
|
86
|
+
Verikloak::ErrorResponse.build(
|
|
87
|
+
code: error.code,
|
|
88
|
+
message: 'audience configuration error',
|
|
89
|
+
status: error.http_status
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Emit the failure log (with a profile suggestion when one fits) if
|
|
94
|
+
# `suggest_in_logs` is enabled.
|
|
95
|
+
#
|
|
96
|
+
# @param env [Hash] Rack environment
|
|
97
|
+
# @param claims [Object] raw claims value read from the env
|
|
98
|
+
# @return [void]
|
|
99
|
+
def log_rejection(env, claims)
|
|
100
|
+
return unless @config.suggest_in_logs
|
|
101
|
+
|
|
102
|
+
suggestion = Checker.suggest(claims, @config, fallback: nil)
|
|
103
|
+
detail = suggestion ? "suggestion profile=:#{suggestion}" : 'no profile matches the observed aud'
|
|
104
|
+
aud_view = Checker.observed_audiences(claims).inspect
|
|
105
|
+
log_warning(env, "[verikloak-audience] insufficient_audience; #{detail} aud=#{aud_view}")
|
|
106
|
+
end
|
|
107
|
+
|
|
65
108
|
# Apply provided options to the configuration instance.
|
|
66
109
|
#
|
|
67
110
|
# @param opts [Hash] raw overrides provided to the middleware
|
|
@@ -82,23 +125,17 @@ module Verikloak
|
|
|
82
125
|
# Determine whether configuration validation should run. This allows
|
|
83
126
|
# Rails generators to boot without a fully-populated configuration since
|
|
84
127
|
# the install task is responsible for creating it. Also skips validation
|
|
85
|
-
# when configuration is not explicitly set up.
|
|
128
|
+
# when configuration is not explicitly set up. The middleware's own
|
|
129
|
+
# (post-override) configuration decides the unconfigured case, so
|
|
130
|
+
# options passed to `use` are validated even when the global
|
|
131
|
+
# configuration is untouched.
|
|
86
132
|
#
|
|
87
133
|
# @return [Boolean]
|
|
88
134
|
def skip_validation?
|
|
89
|
-
|
|
90
|
-
if defined?(::Verikloak::Audience::Railtie)
|
|
91
|
-
if ::Verikloak::Audience::Railtie.respond_to?(:skip_configuration_validation?) && ::Verikloak::Audience::Railtie.skip_configuration_validation?
|
|
92
|
-
return true
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Skip if configuration is incomplete (no audiences configured)
|
|
96
|
-
if ::Verikloak::Audience::Railtie.respond_to?(:skip_unconfigured_validation?) && ::Verikloak::Audience::Railtie.skip_unconfigured_validation?
|
|
97
|
-
return true
|
|
98
|
-
end
|
|
99
|
-
end
|
|
135
|
+
return false unless defined?(::Verikloak::Audience::Railtie)
|
|
100
136
|
|
|
101
|
-
|
|
137
|
+
railtie = ::Verikloak::Audience::Railtie
|
|
138
|
+
railtie.respond_to?(:skip_validation?) && railtie.skip_validation?(@config)
|
|
102
139
|
end
|
|
103
140
|
|
|
104
141
|
# Emit a warning for failed audience checks using request-scoped loggers
|
|
@@ -69,11 +69,7 @@ module Verikloak
|
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def discovery_url_configured?(value)
|
|
72
|
-
|
|
73
|
-
return !value.blank? if value.respond_to?(:blank?)
|
|
74
|
-
return !value.empty? if value.respond_to?(:empty?)
|
|
75
|
-
|
|
76
|
-
true
|
|
72
|
+
!value_blank?(value)
|
|
77
73
|
end
|
|
78
74
|
|
|
79
75
|
def middleware_not_found_error?(error)
|
|
@@ -116,11 +112,16 @@ module Verikloak
|
|
|
116
112
|
def sync_env_claims_key(cfg, rails_config)
|
|
117
113
|
return unless rails_config.respond_to?(:user_env_key)
|
|
118
114
|
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
# Resolve the key the same way verikloak-rails' #effective_user_env_key
|
|
116
|
+
# does (>= 1.2): strip whitespace and treat blank as unset. The core
|
|
117
|
+
# middleware then falls back to its default env key, which matches our
|
|
118
|
+
# default, so both middlewares stay aligned on the same key.
|
|
119
|
+
user_key = rails_config.user_env_key.to_s.strip
|
|
120
|
+
return if user_key.empty?
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
# env_claims_key is never nil (its writer rejects nil), so only the
|
|
123
|
+
# untouched default is eligible for syncing.
|
|
124
|
+
return unless cfg.env_claims_key == Verikloak::Audience::Configuration::DEFAULT_ENV_CLAIMS_KEY
|
|
124
125
|
|
|
125
126
|
cfg.env_claims_key = user_key
|
|
126
127
|
end
|
|
@@ -163,6 +164,7 @@ module Verikloak
|
|
|
163
164
|
|
|
164
165
|
def value_blank?(value)
|
|
165
166
|
return true if value.nil?
|
|
167
|
+
return value.blank? if value.respond_to?(:blank?)
|
|
166
168
|
return true if value.respond_to?(:empty?) && value.empty?
|
|
167
169
|
|
|
168
170
|
value.to_s.empty?
|
|
@@ -213,8 +215,7 @@ module Verikloak
|
|
|
213
215
|
|
|
214
216
|
initializer 'verikloak_audience.configuration' do
|
|
215
217
|
config.after_initialize do
|
|
216
|
-
next if Verikloak::Audience::Railtie.
|
|
217
|
-
next if Verikloak::Audience::Railtie.skip_unconfigured_validation?
|
|
218
|
+
next if Verikloak::Audience::Railtie.skip_validation?
|
|
218
219
|
|
|
219
220
|
Verikloak::Audience.config.validate!
|
|
220
221
|
end
|
|
@@ -256,6 +257,17 @@ module Verikloak
|
|
|
256
257
|
self.middleware_insertion_attempted = false
|
|
257
258
|
end
|
|
258
259
|
|
|
260
|
+
# Combined predicate used by the after-initialize hook and the
|
|
261
|
+
# middleware to decide whether configuration validation should run.
|
|
262
|
+
#
|
|
263
|
+
# @param config [Verikloak::Audience::Configuration] configuration whose
|
|
264
|
+
# audiences decide the unconfigured-boot skip; the middleware passes
|
|
265
|
+
# its own (post-override) configuration, the hook uses the global one
|
|
266
|
+
# @return [Boolean]
|
|
267
|
+
def self.skip_validation?(config = Verikloak::Audience.config)
|
|
268
|
+
skip_configuration_validation? || skip_unconfigured_validation?(config)
|
|
269
|
+
end
|
|
270
|
+
|
|
259
271
|
def self.skip_configuration_validation?
|
|
260
272
|
tokens = first_cli_tokens
|
|
261
273
|
return false if tokens.empty?
|
|
@@ -265,12 +277,12 @@ module Verikloak
|
|
|
265
277
|
tokens.any? { |token| verikloak_install_generator?(token) }
|
|
266
278
|
end
|
|
267
279
|
|
|
268
|
-
def self.skip_unconfigured_validation?
|
|
269
|
-
audiences_unconfigured?
|
|
280
|
+
def self.skip_unconfigured_validation?(config = Verikloak::Audience.config)
|
|
281
|
+
audiences_unconfigured?(config)
|
|
270
282
|
end
|
|
271
283
|
|
|
272
|
-
def self.audiences_unconfigured?
|
|
273
|
-
audiences =
|
|
284
|
+
def self.audiences_unconfigured?(config = Verikloak::Audience.config)
|
|
285
|
+
audiences = config.required_aud_list
|
|
274
286
|
|
|
275
287
|
if audiences.empty?
|
|
276
288
|
warn_unconfigured_once
|
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: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- taiyaky
|
|
@@ -35,14 +35,14 @@ dependencies:
|
|
|
35
35
|
requirements:
|
|
36
36
|
- - "~>"
|
|
37
37
|
- !ruby/object:Gem::Version
|
|
38
|
-
version: '1.
|
|
38
|
+
version: '1.1'
|
|
39
39
|
type: :runtime
|
|
40
40
|
prerelease: false
|
|
41
41
|
version_requirements: !ruby/object:Gem::Requirement
|
|
42
42
|
requirements:
|
|
43
43
|
- - "~>"
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
|
-
version: '1.
|
|
45
|
+
version: '1.1'
|
|
46
46
|
description: |
|
|
47
47
|
Rack middleware that enforces audience checks with deployable profiles,
|
|
48
48
|
layering on top of Verikloak token verification.
|
|
@@ -51,7 +51,9 @@ extensions: []
|
|
|
51
51
|
extra_rdoc_files: []
|
|
52
52
|
files:
|
|
53
53
|
- CHANGELOG.md
|
|
54
|
+
- ERRORS.md
|
|
54
55
|
- LICENSE
|
|
56
|
+
- MAINTAINERS.md
|
|
55
57
|
- README.md
|
|
56
58
|
- lib/generators/verikloak/audience/install/install_generator.rb
|
|
57
59
|
- lib/generators/verikloak/audience/install/templates/initializer.rb.erb
|
|
@@ -70,7 +72,7 @@ metadata:
|
|
|
70
72
|
source_code_uri: https://github.com/taiyaky/verikloak-audience
|
|
71
73
|
changelog_uri: https://github.com/taiyaky/verikloak-audience/blob/main/CHANGELOG.md
|
|
72
74
|
bug_tracker_uri: https://github.com/taiyaky/verikloak-audience/issues
|
|
73
|
-
documentation_uri: https://rubydoc.info/gems/verikloak-audience/1.
|
|
75
|
+
documentation_uri: https://rubydoc.info/gems/verikloak-audience/1.1.0
|
|
74
76
|
rubygems_mfa_required: 'true'
|
|
75
77
|
rdoc_options: []
|
|
76
78
|
require_paths:
|