omniauth_openid_federation 1.3.2 → 2.0.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 +32 -1
- data/README.md +119 -13
- data/app/controllers/omniauth_openid_federation/federation_controller.rb +2 -2
- data/config/polyrun_coverage.yml +15 -0
- data/config/polyrun_spec_quality.yml +29 -0
- data/examples/README_INTEGRATION_TESTING.md +3 -0
- data/examples/integration_test_flow.rb +10 -8
- data/examples/jobs/federation_cache_refresh_job.rb.example +7 -7
- data/examples/jobs/federation_files_generation_job.rb.example +3 -2
- data/examples/mock_op_server.rb +4 -5
- data/examples/mock_rp_server.rb +3 -3
- data/examples/standalone_multiple_endpoints_example.rb +494 -0
- data/lib/omniauth_openid_federation/access_token.rb +91 -450
- data/lib/omniauth_openid_federation/cache.rb +16 -0
- data/lib/omniauth_openid_federation/cache_adapter.rb +6 -3
- data/lib/omniauth_openid_federation/constants.rb +3 -0
- data/lib/omniauth_openid_federation/federation/entity_statement.rb +0 -4
- data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +2 -4
- data/lib/omniauth_openid_federation/federation/signed_jwks.rb +0 -1
- data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +51 -6
- data/lib/omniauth_openid_federation/federation_endpoint.rb +1 -1
- data/lib/omniauth_openid_federation/http_client.rb +145 -32
- data/lib/omniauth_openid_federation/http_errors.rb +14 -0
- data/lib/omniauth_openid_federation/id_token.rb +19 -0
- data/lib/omniauth_openid_federation/jwe.rb +26 -0
- data/lib/omniauth_openid_federation/jwks/decode.rb +0 -13
- data/lib/omniauth_openid_federation/jwks/fetch.rb +16 -39
- data/lib/omniauth_openid_federation/jws.rb +2 -11
- data/lib/omniauth_openid_federation/jwt_response_decoder.rb +290 -0
- data/lib/omniauth_openid_federation/oidc_client.rb +101 -0
- data/lib/omniauth_openid_federation/rack.rb +2 -0
- data/lib/omniauth_openid_federation/rack_endpoint.rb +2 -2
- data/lib/omniauth_openid_federation/rate_limiter.rb +10 -12
- data/lib/omniauth_openid_federation/secure_compare.rb +15 -0
- data/lib/omniauth_openid_federation/strategy/authorization_request.rb +220 -0
- data/lib/omniauth_openid_federation/strategy/callback_handling.rb +135 -0
- data/lib/omniauth_openid_federation/strategy/client_construction.rb +101 -0
- data/lib/omniauth_openid_federation/strategy/client_entity_statement.rb +211 -0
- data/lib/omniauth_openid_federation/strategy/endpoint_resolution.rb +433 -0
- data/lib/omniauth_openid_federation/strategy/failure_handling.rb +75 -0
- data/lib/omniauth_openid_federation/strategy/id_token_decoding.rb +268 -0
- data/lib/omniauth_openid_federation/strategy/jwks_resolution.rb +268 -0
- data/lib/omniauth_openid_federation/strategy/provider_entity_statement.rb +134 -0
- data/lib/omniauth_openid_federation/strategy/userinfo_decoding.rb +61 -0
- data/lib/omniauth_openid_federation/strategy.rb +27 -1910
- data/lib/omniauth_openid_federation/tasks/authentication_flow_tester.rb +237 -0
- data/lib/omniauth_openid_federation/tasks/callback_processor.rb +235 -0
- data/lib/omniauth_openid_federation/tasks/client_keys_preparer.rb +96 -0
- data/lib/omniauth_openid_federation/tasks/local_endpoint_tester.rb +189 -0
- data/lib/omniauth_openid_federation/tasks/path_resolver.rb +20 -0
- data/lib/omniauth_openid_federation/tasks_helper.rb +28 -808
- data/lib/omniauth_openid_federation/time_helpers.rb +7 -0
- data/lib/omniauth_openid_federation/user_info.rb +15 -0
- data/lib/omniauth_openid_federation/utils.rb +10 -2
- data/lib/omniauth_openid_federation/validators.rb +43 -8
- data/lib/omniauth_openid_federation/version.rb +1 -1
- data/lib/omniauth_openid_federation.rb +9 -3
- data/lib/tasks/omniauth_openid_federation.rake +1 -2
- data/sig/omniauth_openid_federation.rbs +2 -1
- data/sig/strategy.rbs +6 -6
- metadata +181 -71
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed139d6baa665a3a7dce282bed397079492844cef7b45de0b5787edf3de9284e
|
|
4
|
+
data.tar.gz: c4d1f5e957c5aa2340d8cd223bfe1c591ed8b85f79417f775468868a54e4d465
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bb56e08a749c63510e1d1456766ed2375827cb41f4171e25fbc260a5e06563fd59559fed10c7cf95ccdf7893a70d1567a8372fbda13c454fa7be765ae775db0d
|
|
7
|
+
data.tar.gz: 576b586169d64ba1b4989db48badd0cb1da1f145336e0786dabd8b41770344b9a19b0c43b4a28859738f0d72c63fb985069372868d41fcb64665ab6821cfcf4b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 2.0.0 (2026-07-06)
|
|
4
|
+
|
|
5
|
+
- BREAKING: Replace `openid_connect` and `json-jwt` with `OmniauthOpenidFederation::OidcClient`, `AccessToken`, and `IdToken` built on `oauth2` and `jwt`
|
|
6
|
+
- BREAKING: Drop explicit `rack` runtime dependency; load Rack federation endpoints via `require "omniauth_openid_federation/rack"`
|
|
7
|
+
- BREAKING: Validate ID token `iss`, `aud`, and session `nonce` in `callback_phase`
|
|
8
|
+
- BREAKING: Fail closed on trust chain resolution errors instead of returning empty metadata
|
|
9
|
+
- BREAKING: Reject `alg: none` in access token JWT handling
|
|
10
|
+
- BREAKING: Remove broad JWKS decode retry on non-signature errors
|
|
11
|
+
- BREAKING: Reject unknown `crit` claims in entity statement validation
|
|
12
|
+
- BREAKING: Enforce minimum RSA key size (2048 bits) in `Validators.validate_private_key!`
|
|
13
|
+
- Use `jwe` gem for JWE encrypt/decrypt instead of `json-jwt`
|
|
14
|
+
- Refactor `OmniAuth::Strategies::OpenIDFederation` into concern modules under `lib/omniauth_openid_federation/strategy/`
|
|
15
|
+
- Add `OmniauthOpenidFederation::SecureCompare` for constant-time string comparison in the strategy
|
|
16
|
+
- Add `OmniauthOpenidFederation::JwtResponseDecoder` for encrypted/signed JWT userinfo and resource responses
|
|
17
|
+
- Add behavioral contract specs for token exchange, userinfo, and JWE parity with former `openid_connect` flow
|
|
18
|
+
- Add Telia Tunnistus PP entity statement fixture and contract spec (A128GCM and A128CBC-HS256)
|
|
19
|
+
- Add provider-agnostic strategy options: `default_request_object_claims`, `required_request_object_claims`, `allowed_acr_values`, `require_entity_statement_fingerprint`
|
|
20
|
+
- Use `Jwe.encrypted?` for JWE detection in `AccessToken#resource_request` and `TasksHelper`
|
|
21
|
+
- Deduplicate entity statement loading in `access_token.rb` via `load_entity_statement_content`
|
|
22
|
+
- Verify subordinate entity statement signatures in trust chain resolution
|
|
23
|
+
- Wire `verify_ssl` into `HttpClient` SSL options
|
|
24
|
+
- Use `CacheAdapter` in `RateLimiter` instead of `Rails.cache` directly
|
|
25
|
+
- Use issuer-scoped cache keys for federation JWKS and signed JWKS endpoints
|
|
26
|
+
- Fix OAuth callback failures in `callback_phase` to return the Rack response from `fail!` instead of `nil`; avoids `Rack::ETag` `NoMethodError` and HTTP 500 on auth failure
|
|
27
|
+
- Fix flaky `test_local_endpoint` spec when `SSL_CERT_FILE` is set in the environment
|
|
28
|
+
- Replace internal `decode_id_token` stubs in strategy specs with JWKS-backed setup
|
|
29
|
+
- Add `HttpClient.post` with form data, headers, and connect/read timeouts
|
|
30
|
+
- Retry GET requests on HTTP 429, 502, and 503; POST requests default to no retry
|
|
31
|
+
- Auto-configure SSL CA file when peer verification is enabled
|
|
32
|
+
- Route rake task HTTP through `HttpClient` instead of ad hoc `Net::HTTP`
|
|
33
|
+
|
|
3
34
|
## 1.3.2 (2025-12-09)
|
|
4
35
|
|
|
5
36
|
- Added `TimeHelpers` module for compatibility with non-Rails environments
|
|
@@ -38,7 +69,7 @@
|
|
|
38
69
|
## 1.1.0 (2025-11-26)
|
|
39
70
|
|
|
40
71
|
- Enhanced instrumentation: All blocking exceptions automatically reported through instrumentation system, including OmniAuth middleware errors (like AuthenticityTokenProtection)
|
|
41
|
-
- CSRF protection instrumentation: New authenticity_error event type for reporting OmniAuth CSRF
|
|
72
|
+
- CSRF protection instrumentation: New authenticity_error event type for reporting OmniAuth CSRF failures
|
|
42
73
|
- Comprehensive error reporting: Override fail! method in strategy to catch and instrument all authentication failures
|
|
43
74
|
- CSRF protection documentation: Added comprehensive Step 7 in README explaining CSRF protection configuration for both request and callback phases
|
|
44
75
|
- CSRF configuration examples: Added complete examples in examples/config/initializers/devise.rb.example and examples/app/controllers/users/omniauth_callbacks_controller.rb.example
|
data/README.md
CHANGED
|
@@ -4,12 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
OmniAuth strategy for OpenID Federation providers with comprehensive security features, supporting signed request objects, ID token encryption, and full OpenID Federation 1.0 compliance.
|
|
6
6
|
|
|
7
|
-
Sponsored by [Kisko Labs](https://www.kiskolabs.com).
|
|
8
|
-
|
|
9
|
-
<a href="https://www.kiskolabs.com">
|
|
10
|
-
<img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
|
|
11
|
-
</a>
|
|
12
|
-
|
|
13
7
|
## Installation
|
|
14
8
|
|
|
15
9
|
```ruby
|
|
@@ -279,7 +273,14 @@ Use `prepare_request_object_params` proc to modify parameters before they're add
|
|
|
279
273
|
|
|
280
274
|
```ruby
|
|
281
275
|
config.omniauth :openid_federation,
|
|
282
|
-
request_object_params: [:ftn_spname], # Allow-list for
|
|
276
|
+
request_object_params: [:ftn_spname], # Allow-list for provider-specific params (example: Finnish FTN)
|
|
277
|
+
default_request_object_claims: {
|
|
278
|
+
ui_locales: "fi",
|
|
279
|
+
acr_values: ENV["OPENID_ACR_VALUES"]
|
|
280
|
+
},
|
|
281
|
+
required_request_object_claims: %w[ftn_spname acr_values ui_locales],
|
|
282
|
+
allowed_acr_values: [ENV["OPENID_ACR_VALUES"]].compact,
|
|
283
|
+
require_entity_statement_fingerprint: true,
|
|
283
284
|
prepare_request_object_params: proc do |params|
|
|
284
285
|
# Combine config acr_values with form acr_values
|
|
285
286
|
form_acr_values = params["acr_values"]&.to_s&.strip
|
|
@@ -310,6 +311,96 @@ config.omniauth :openid_federation,
|
|
|
310
311
|
|
|
311
312
|
The proc will combine this with config values before adding to the signed JWT.
|
|
312
313
|
|
|
314
|
+
## Rodauth Integration
|
|
315
|
+
|
|
316
|
+
This strategy can be used with the Rodauth authentication framework via the
|
|
317
|
+
[`rodauth-omniauth` feature](https://github.com/janko/rodauth-omniauth).
|
|
318
|
+
|
|
319
|
+
### Minimal Roda/Rodauth setup
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
require "roda"
|
|
323
|
+
require "rodauth"
|
|
324
|
+
require "rodauth/omniauth"
|
|
325
|
+
require "omniauth_openid_federation"
|
|
326
|
+
|
|
327
|
+
DB = Sequel.sqlite # or your production database
|
|
328
|
+
|
|
329
|
+
DB.create_table? :accounts do
|
|
330
|
+
primary_key :id
|
|
331
|
+
String :email, null: false
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
DB.create_table? :account_identities do
|
|
335
|
+
primary_key :id
|
|
336
|
+
foreign_key :account_id, :accounts, null: false
|
|
337
|
+
String :provider, null: false
|
|
338
|
+
String :uid, null: false
|
|
339
|
+
index [:provider, :uid], unique: true
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
class App < Roda
|
|
343
|
+
plugin :sessions, secret: ENV.fetch("SESSION_SECRET") { SecureRandom.hex(32) }
|
|
344
|
+
plugin :json
|
|
345
|
+
|
|
346
|
+
plugin :rodauth, json: true do
|
|
347
|
+
db DB
|
|
348
|
+
enable :omniauth
|
|
349
|
+
|
|
350
|
+
# Mount OmniAuth under /auth, as recommended by rodauth-omniauth
|
|
351
|
+
omniauth_prefix "/auth"
|
|
352
|
+
|
|
353
|
+
omniauth_provider :openid_federation,
|
|
354
|
+
nil,
|
|
355
|
+
nil,
|
|
356
|
+
strategy_class: OmniAuth::Strategies::OpenIDFederation,
|
|
357
|
+
name: :openid_federation,
|
|
358
|
+
issuer: ENV.fetch("OPENID_ISSUER", "https://provider.example.com"),
|
|
359
|
+
audience: ENV.fetch("OPENID_AUDIENCE", "https://provider.example.com"),
|
|
360
|
+
client_options: {
|
|
361
|
+
identifier: ENV["OPENID_CLIENT_ID"],
|
|
362
|
+
redirect_uri: ENV["OPENID_REDIRECT_URI"] || "https://your-app.example.com/auth/openid_federation/callback",
|
|
363
|
+
host: URI.parse(ENV.fetch("OPENID_ISSUER", "https://provider.example.com")).host,
|
|
364
|
+
scheme: URI.parse(ENV.fetch("OPENID_ISSUER", "https://provider.example.com")).scheme,
|
|
365
|
+
authorization_endpoint: "/oauth2/authorize",
|
|
366
|
+
token_endpoint: "/oauth2/token",
|
|
367
|
+
userinfo_endpoint: "/oauth2/userinfo",
|
|
368
|
+
jwks_uri: "/.well-known/jwks.json",
|
|
369
|
+
private_key: OpenSSL::PKey::RSA.new(
|
|
370
|
+
Base64.decode64(ENV.fetch("OPENID_CLIENT_PRIVATE_KEY_BASE64"))
|
|
371
|
+
)
|
|
372
|
+
},
|
|
373
|
+
entity_statement_url: ENV["OPENID_ENTITY_STATEMENT_URL"],
|
|
374
|
+
entity_statement_fingerprint: ENV["OPENID_ENTITY_STATEMENT_FINGERPRINT"]
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
route do |r|
|
|
378
|
+
# Rodauth authentication + OmniAuth endpoints
|
|
379
|
+
# r.rodauth automatically handles omniauth routes based on omniauth_prefix setting
|
|
380
|
+
r.rodauth
|
|
381
|
+
|
|
382
|
+
r.root do
|
|
383
|
+
if rodauth.logged_in?
|
|
384
|
+
{logged_in: true, account_id: rodauth.session_value}
|
|
385
|
+
else
|
|
386
|
+
{logged_in: false}
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
With this configuration, the Rodauth `:omniauth` feature:
|
|
394
|
+
|
|
395
|
+
- uses `OmniAuth::Strategies::OpenIDFederation` as the strategy implementation,
|
|
396
|
+
- calls the strategy's request and callback phases under `/auth/openid_federation`,
|
|
397
|
+
- persists external identities into the `account_identities` table following the
|
|
398
|
+
[`rodauth-omniauth` schema](https://github.com/janko/rodauth-omniauth),
|
|
399
|
+
- and exposes helper methods like `rodauth.omniauth_auth`, `rodauth.omniauth_email`,
|
|
400
|
+
and `rodauth.omniauth_name` as documented in the
|
|
401
|
+
[Rodauth documentation](https://rodauth.jeremyevans.net/documentation.html)
|
|
402
|
+
and [`rodauth-omniauth` README](https://github.com/janko/rodauth-omniauth).
|
|
403
|
+
|
|
313
404
|
## Rake Tasks
|
|
314
405
|
|
|
315
406
|
### Prepare Client Keys
|
|
@@ -356,6 +447,10 @@ rake openid_federation:test_authentication_flow[
|
|
|
356
447
|
- `always_encrypt_request_object` - Force encryption of request objects (default: false)
|
|
357
448
|
- `request_object_params` - Array of parameter names to include in request object (allow-list)
|
|
358
449
|
- `prepare_request_object_params` - Proc to modify params before adding to signed request object: `proc { |params| modified_params }`
|
|
450
|
+
- `default_request_object_claims` - Hash of default claims merged before `prepare_request_object_params` (request params override defaults)
|
|
451
|
+
- `required_request_object_claims` - Array of claim names that must be present in the signed request object (raises before redirect)
|
|
452
|
+
- `allowed_acr_values` - When set, ID token `acr` must match one of the configured values
|
|
453
|
+
- `require_entity_statement_fingerprint` - When true, fetching provider entity statements from URL or issuer requires `entity_statement_fingerprint`
|
|
359
454
|
- `discovery` - Enable automatic endpoint discovery (default: true)
|
|
360
455
|
|
|
361
456
|
## Security
|
|
@@ -383,9 +478,10 @@ rake openid_federation:test_authentication_flow[
|
|
|
383
478
|
## Example Files
|
|
384
479
|
|
|
385
480
|
See `examples/` directory for complete configuration examples:
|
|
386
|
-
- `examples/
|
|
387
|
-
- `examples/config/initializers/
|
|
388
|
-
- `examples/config/
|
|
481
|
+
- `examples/standalone_multiple_endpoints_example.rb` - Standalone Sinatra app with multiple auth endpoints and entrance point-based redirects
|
|
482
|
+
- `examples/config/initializers/devise.rb.example` - Devise integration example (Rails)
|
|
483
|
+
- `examples/config/initializers/omniauth_openid_federation.rb.example` - Federation endpoint configuration
|
|
484
|
+
- `examples/config/open_id_connect_config.rb.example` - Configuration class example
|
|
389
485
|
|
|
390
486
|
## Development
|
|
391
487
|
|
|
@@ -393,9 +489,11 @@ See `examples/` directory for complete configuration examples:
|
|
|
393
489
|
git clone https://github.com/amkisko/omniauth_openid_federation.rb.git
|
|
394
490
|
cd omniauth_openid_federation.rb
|
|
395
491
|
bundle install
|
|
396
|
-
|
|
492
|
+
bundle exec polyrun parallel-rspec --workers 5 -c polyrun.yml
|
|
397
493
|
```
|
|
398
494
|
|
|
495
|
+
Same flow as CI; optional wrappers [`bin/rspec_parallel`](bin/rspec_parallel) and [`bin/appraisals`](bin/appraisals) (runs **`polyrun parallel-rspec`** per Appraisal gemfile). Details: **[POLYRUN.md](POLYRUN.md)**.
|
|
496
|
+
|
|
399
497
|
## Contributing
|
|
400
498
|
|
|
401
499
|
Contributions welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
@@ -413,8 +511,8 @@ Contributions welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidel
|
|
|
413
511
|
- [omniauth](https://github.com/omniauth/omniauth) - Authentication framework
|
|
414
512
|
- [devise](https://github.com/heartcombo/devise) - Rails authentication solution
|
|
415
513
|
- [jwt](https://github.com/jwt/ruby-jwt) - JSON Web Token implementation
|
|
416
|
-
- [jwe](https://
|
|
417
|
-
- [
|
|
514
|
+
- [jwe](https://rubygems.org/gems/jwe) - JSON Web Encryption (via `OmniauthOpenidFederation::Jwe`)
|
|
515
|
+
- [oauth2](https://github.com/oauth-xx/oauth2) - OAuth 2.0 client (via `OmniauthOpenidFederation::OidcClient`)
|
|
418
516
|
- [http](https://github.com/httprb/http) - HTTP client
|
|
419
517
|
- [anyway_config](https://github.com/palkan/anyway_config) - Configuration management
|
|
420
518
|
- [action_reporter](https://github.com/basecamp/action_reporter) - Error reporting
|
|
@@ -422,3 +520,11 @@ Contributions welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidel
|
|
|
422
520
|
## License
|
|
423
521
|
|
|
424
522
|
MIT License. See [LICENSE.md](LICENSE.md) for details.
|
|
523
|
+
|
|
524
|
+
## Sponsors
|
|
525
|
+
|
|
526
|
+
Sponsored by [Kisko Labs](https://www.kiskolabs.com).
|
|
527
|
+
|
|
528
|
+
<a href="https://www.kiskolabs.com">
|
|
529
|
+
<img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
|
|
530
|
+
</a>
|
|
@@ -105,7 +105,7 @@ module OmniauthOpenidFederation
|
|
|
105
105
|
jwks_to_serve = OmniauthOpenidFederation::FederationEndpoint.current_jwks
|
|
106
106
|
|
|
107
107
|
# Apply server-side caching if available
|
|
108
|
-
cache_key =
|
|
108
|
+
cache_key = OmniauthOpenidFederation::Cache.key_for_served_jwks(config.issuer)
|
|
109
109
|
cache_ttl = config.jwks_cache_ttl || 3600
|
|
110
110
|
|
|
111
111
|
jwks_json = if OmniauthOpenidFederation::CacheAdapter.available?
|
|
@@ -144,7 +144,7 @@ module OmniauthOpenidFederation
|
|
|
144
144
|
signed_jwks_jwt = OmniauthOpenidFederation::FederationEndpoint.generate_signed_jwks
|
|
145
145
|
|
|
146
146
|
# Apply server-side caching if available
|
|
147
|
-
cache_key =
|
|
147
|
+
cache_key = OmniauthOpenidFederation::Cache.key_for_served_signed_jwks(config.issuer)
|
|
148
148
|
cache_ttl = config.jwks_cache_ttl || 3600
|
|
149
149
|
|
|
150
150
|
cached_jwt = if OmniauthOpenidFederation::CacheAdapter.available?
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Polyrun::Coverage::Rails — loaded from spec/spec_helper via Polyrun::Coverage::Rails.start!
|
|
2
|
+
# (must run before application code; keep coverage config here, not in initializers)
|
|
3
|
+
#
|
|
4
|
+
# Do not set track_files here: with track_files, Polyrun merges the full Coverage.result blob
|
|
5
|
+
# (every loaded gem / dependency) before filtering, which dilutes the line % and breaks the gate.
|
|
6
|
+
# Default track_under: ["lib"] keeps only this gem under the project root.
|
|
7
|
+
|
|
8
|
+
track_under:
|
|
9
|
+
- lib
|
|
10
|
+
reject_patterns:
|
|
11
|
+
- "/lib/tasks/"
|
|
12
|
+
- "/lib/omniauth_openid_federation/version.rb"
|
|
13
|
+
- "/spec/"
|
|
14
|
+
minimum_line_percent: 80
|
|
15
|
+
strict: true
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Per-example spec quality (coverage deltas, resource use). See polyrun report-spec-quality.
|
|
2
|
+
track_under:
|
|
3
|
+
- lib
|
|
4
|
+
- app
|
|
5
|
+
ignore_paths:
|
|
6
|
+
- /lib/omniauth_openid_federation/version.rb
|
|
7
|
+
- /lib/tasks/
|
|
8
|
+
min_line_churn: 40
|
|
9
|
+
min_query_count: 15
|
|
10
|
+
hot_line_example_overlap: 8
|
|
11
|
+
strict: false
|
|
12
|
+
sample: 1.0
|
|
13
|
+
ignore_examples:
|
|
14
|
+
- version_spec.rb
|
|
15
|
+
- omniauth_openid_federation_spec.rb:2
|
|
16
|
+
- /handles missing URL/
|
|
17
|
+
- /handles configuration errors/
|
|
18
|
+
- /handles validation errors/
|
|
19
|
+
- /handles unexpected errors/
|
|
20
|
+
- /handles fetch errors/
|
|
21
|
+
ignore_query_patterns: []
|
|
22
|
+
profile:
|
|
23
|
+
- cpu
|
|
24
|
+
- mem
|
|
25
|
+
sql_counter: true
|
|
26
|
+
# CI gates (POLYRUN_SPEC_QUALITY_STRICT=1 or strict: true):
|
|
27
|
+
# minimum_unique_lines_per_example: 1
|
|
28
|
+
# max_zero_hit_examples: 0
|
|
29
|
+
# max_hot_line_overlap: 100
|
|
@@ -145,6 +145,9 @@ This will:
|
|
|
145
145
|
5. Run all integration tests
|
|
146
146
|
6. Clean up (kill servers, remove tmp dirs) on exit
|
|
147
147
|
|
|
148
|
+
CI runs the same flow on every push to `main` (see `.github/workflows/test.yml`, job `integration`).
|
|
149
|
+
Locally: `make integration-test` or `bundle exec ruby examples/integration_test_flow.rb` (exits non-zero on failure).
|
|
150
|
+
|
|
148
151
|
### Manual Start (For Debugging)
|
|
149
152
|
|
|
150
153
|
If you want to start servers manually for debugging:
|
|
@@ -42,7 +42,7 @@ require "securerandom"
|
|
|
42
42
|
require "fileutils"
|
|
43
43
|
require "timeout"
|
|
44
44
|
require "open3"
|
|
45
|
-
|
|
45
|
+
require_relative "../lib/omniauth_openid_federation"
|
|
46
46
|
require "jwt"
|
|
47
47
|
|
|
48
48
|
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
@@ -272,7 +272,7 @@ class IntegrationTestFlow
|
|
|
272
272
|
script_path = File.expand_path(script, __dir__)
|
|
273
273
|
project_root = File.expand_path("..", __dir__)
|
|
274
274
|
|
|
275
|
-
# Use bundle exec to ensure all dependencies (jwt, jwe, etc.) are available
|
|
275
|
+
# Use bundle exec to ensure all dependencies (jwt, jwe, oauth2, etc.) are available
|
|
276
276
|
pid = Process.spawn(
|
|
277
277
|
{
|
|
278
278
|
"RUBYOPT" => "-W0", # Suppress warnings
|
|
@@ -319,7 +319,7 @@ class IntegrationTestFlow
|
|
|
319
319
|
attempt = 0
|
|
320
320
|
ready = false
|
|
321
321
|
server_name = url.include?("9292") ? "OP" : "RP"
|
|
322
|
-
start_time =
|
|
322
|
+
start_time = OmniauthOpenidFederation::TimeHelpers.now
|
|
323
323
|
|
|
324
324
|
while attempt < max_attempts && !ready
|
|
325
325
|
begin
|
|
@@ -330,7 +330,7 @@ class IntegrationTestFlow
|
|
|
330
330
|
response = http.get(uri.path)
|
|
331
331
|
|
|
332
332
|
if response.code == "200"
|
|
333
|
-
elapsed = (
|
|
333
|
+
elapsed = (OmniauthOpenidFederation::TimeHelpers.now - start_time).round(1)
|
|
334
334
|
puts " ✓ #{url} is ready (#{elapsed}s)"
|
|
335
335
|
ready = true
|
|
336
336
|
end
|
|
@@ -342,7 +342,7 @@ class IntegrationTestFlow
|
|
|
342
342
|
attempt += 1
|
|
343
343
|
# Show progress every 5 attempts (1 second)
|
|
344
344
|
if attempt % 5 == 0
|
|
345
|
-
elapsed = (
|
|
345
|
+
elapsed = (OmniauthOpenidFederation::TimeHelpers.now - start_time).round(1)
|
|
346
346
|
print "."
|
|
347
347
|
end
|
|
348
348
|
sleep check_interval
|
|
@@ -350,7 +350,7 @@ class IntegrationTestFlow
|
|
|
350
350
|
end
|
|
351
351
|
|
|
352
352
|
unless ready
|
|
353
|
-
elapsed = (
|
|
353
|
+
elapsed = (OmniauthOpenidFederation::TimeHelpers.now - start_time).round(1)
|
|
354
354
|
puts "\n ✗ #{server_name} server at #{url} did not become ready in time (#{elapsed}s)"
|
|
355
355
|
err_log = File.join(@tmp_dir, "#{server_name.downcase}_server_error.log")
|
|
356
356
|
log_file = File.join(@tmp_dir, "#{server_name.downcase}_server.log")
|
|
@@ -679,7 +679,7 @@ class IntegrationTestFlow
|
|
|
679
679
|
|
|
680
680
|
# Encrypt with wrong key (provider won't be able to decrypt with its key)
|
|
681
681
|
signed_jwt = jws.sign
|
|
682
|
-
encrypted_request =
|
|
682
|
+
encrypted_request = OmniauthOpenidFederation::Jwe.encrypt(signed_jwt, wrong_key, alg: "RSA-OAEP", enc: "A128CBC-HS256")
|
|
683
683
|
|
|
684
684
|
# Verify it's encrypted (JWE has 5 parts)
|
|
685
685
|
parts = encrypted_request.split(".")
|
|
@@ -1261,6 +1261,8 @@ class IntegrationTestFlow
|
|
|
1261
1261
|
|
|
1262
1262
|
puts ""
|
|
1263
1263
|
puts (passed == @test_results.length) ? "✅ All tests passed!" : "❌ Some tests failed."
|
|
1264
|
+
|
|
1265
|
+
passed == @test_results.length
|
|
1264
1266
|
end
|
|
1265
1267
|
|
|
1266
1268
|
def cleanup
|
|
@@ -1330,5 +1332,5 @@ if __FILE__ == $0
|
|
|
1330
1332
|
puts ""
|
|
1331
1333
|
|
|
1332
1334
|
test_flow = IntegrationTestFlow.new
|
|
1333
|
-
test_flow.run
|
|
1335
|
+
exit(test_flow.run ? 0 : 1)
|
|
1334
1336
|
end
|
|
@@ -32,14 +32,17 @@ class FederationCacheRefreshJob < ApplicationJob
|
|
|
32
32
|
|
|
33
33
|
Rails.logger.info "[FederationCacheRefreshJob] Starting cache refresh..."
|
|
34
34
|
|
|
35
|
+
config = OmniauthOpenidFederation::FederationEndpoint.configuration
|
|
36
|
+
|
|
35
37
|
# Clear all federation caches
|
|
36
38
|
if Rails.cache.respond_to?(:delete_matched)
|
|
37
39
|
Rails.cache.delete_matched("federation:*")
|
|
38
40
|
Rails.logger.info "[FederationCacheRefreshJob] Cleared federation caches"
|
|
39
41
|
else
|
|
40
42
|
# Fallback: clear known cache keys
|
|
41
|
-
|
|
42
|
-
Rails.cache.delete(
|
|
43
|
+
issuer = config.issuer
|
|
44
|
+
Rails.cache.delete(OmniauthOpenidFederation::Cache.key_for_served_jwks(issuer))
|
|
45
|
+
Rails.cache.delete(OmniauthOpenidFederation::Cache.key_for_served_signed_jwks(issuer))
|
|
43
46
|
Rails.cache.delete("federation:entity_statement")
|
|
44
47
|
Rails.logger.info "[FederationCacheRefreshJob] Cleared federation caches (fallback)"
|
|
45
48
|
end
|
|
@@ -47,20 +50,17 @@ class FederationCacheRefreshJob < ApplicationJob
|
|
|
47
50
|
# Optionally: Pre-warm caches by generating data
|
|
48
51
|
# This ensures data is ready immediately after cache clear
|
|
49
52
|
begin
|
|
50
|
-
config = OmniauthOpenidFederation::FederationEndpoint.configuration
|
|
51
|
-
|
|
52
53
|
# Pre-warm JWKS cache
|
|
53
54
|
if config.current_jwks || config.current_jwks_proc
|
|
54
55
|
jwks = OmniauthOpenidFederation::FederationEndpoint.current_jwks
|
|
55
|
-
cache_key =
|
|
56
|
+
cache_key = OmniauthOpenidFederation::Cache.key_for_served_jwks(config.issuer)
|
|
56
57
|
Rails.cache.write(cache_key, jwks.to_json, expires_in: config.jwks_cache_ttl || 3600)
|
|
57
58
|
Rails.logger.info "[FederationCacheRefreshJob] Pre-warmed JWKS cache"
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
# Pre-warm signed JWKS cache
|
|
61
62
|
signed_jwks = OmniauthOpenidFederation::FederationEndpoint.generate_signed_jwks
|
|
62
|
-
cache_key =
|
|
63
|
-
config = OmniauthOpenidFederation::FederationEndpoint.configuration
|
|
63
|
+
cache_key = OmniauthOpenidFederation::Cache.key_for_served_signed_jwks(config.issuer)
|
|
64
64
|
Rails.cache.write(cache_key, signed_jwks, expires_in: config.jwks_cache_ttl || 3600)
|
|
65
65
|
Rails.logger.info "[FederationCacheRefreshJob] Pre-warmed signed JWKS cache"
|
|
66
66
|
rescue => e
|
|
@@ -72,8 +72,9 @@ class FederationFilesGenerationJob < ApplicationJob
|
|
|
72
72
|
Rails.logger.info "[FederationFilesGenerationJob] Cleared federation caches"
|
|
73
73
|
else
|
|
74
74
|
# Fallback: clear known cache keys
|
|
75
|
-
|
|
76
|
-
Rails.cache.delete(
|
|
75
|
+
issuer = OmniauthOpenidFederation::FederationEndpoint.configuration.issuer
|
|
76
|
+
Rails.cache.delete(OmniauthOpenidFederation::Cache.key_for_served_jwks(issuer))
|
|
77
|
+
Rails.cache.delete(OmniauthOpenidFederation::Cache.key_for_served_signed_jwks(issuer))
|
|
77
78
|
Rails.logger.info "[FederationFilesGenerationJob] Cleared federation caches (fallback)"
|
|
78
79
|
end
|
|
79
80
|
|
data/examples/mock_op_server.rb
CHANGED
|
@@ -37,7 +37,6 @@ require "webrick"
|
|
|
37
37
|
require "yaml"
|
|
38
38
|
require "json"
|
|
39
39
|
require "jwt"
|
|
40
|
-
require "jwe"
|
|
41
40
|
require "openssl"
|
|
42
41
|
require "base64"
|
|
43
42
|
require "uri"
|
|
@@ -111,7 +110,7 @@ class MockOPServer
|
|
|
111
110
|
end
|
|
112
111
|
|
|
113
112
|
def self.load_signing_key(key_data)
|
|
114
|
-
if
|
|
113
|
+
if OmniauthOpenidFederation::StringHelpers.blank?(key_data)
|
|
115
114
|
# Generate a new key for testing
|
|
116
115
|
OpenSSL::PKey::RSA.new(2048)
|
|
117
116
|
elsif key_data.is_a?(String)
|
|
@@ -126,7 +125,7 @@ class MockOPServer
|
|
|
126
125
|
end
|
|
127
126
|
|
|
128
127
|
def self.load_encryption_key(key_data)
|
|
129
|
-
return nil if
|
|
128
|
+
return nil if OmniauthOpenidFederation::StringHelpers.blank?(key_data)
|
|
130
129
|
load_signing_key(key_data)
|
|
131
130
|
end
|
|
132
131
|
|
|
@@ -478,7 +477,7 @@ class MockOPServer
|
|
|
478
477
|
redirect_uri: redirect_uri,
|
|
479
478
|
state: state,
|
|
480
479
|
nonce: nonce,
|
|
481
|
-
created_at:
|
|
480
|
+
created_at: OmniauthOpenidFederation::TimeHelpers.now
|
|
482
481
|
}
|
|
483
482
|
|
|
484
483
|
# Redirect back to RP with authorization code
|
|
@@ -566,7 +565,7 @@ class MockOPServer
|
|
|
566
565
|
|
|
567
566
|
# Try to decrypt - will fail if wrong key was used
|
|
568
567
|
begin
|
|
569
|
-
request_jwt =
|
|
568
|
+
request_jwt = OmniauthOpenidFederation::Jwe.decrypt(request_jwt, MockOPServer::ENCRYPTION_KEY)
|
|
570
569
|
rescue => e
|
|
571
570
|
raise OmniauthOpenidFederation::DecryptionError, "Failed to decrypt request object: #{e.message}"
|
|
572
571
|
end
|
data/examples/mock_rp_server.rb
CHANGED
|
@@ -66,7 +66,7 @@ class MockRPServer
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def self.load_signing_key(key_data)
|
|
69
|
-
if
|
|
69
|
+
if OmniauthOpenidFederation::StringHelpers.blank?(key_data)
|
|
70
70
|
OpenSSL::PKey::RSA.new(2048)
|
|
71
71
|
elsif key_data.is_a?(String)
|
|
72
72
|
if key_data.include?("BEGIN")
|
|
@@ -80,7 +80,7 @@ class MockRPServer
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def self.load_encryption_key(key_data)
|
|
83
|
-
return nil if
|
|
83
|
+
return nil if OmniauthOpenidFederation::StringHelpers.blank?(key_data)
|
|
84
84
|
load_signing_key(key_data)
|
|
85
85
|
end
|
|
86
86
|
|
|
@@ -297,7 +297,7 @@ class MockRPServer
|
|
|
297
297
|
provider_entity_id: provider_entity_id,
|
|
298
298
|
redirect_uri: redirect_uri,
|
|
299
299
|
nonce: nonce,
|
|
300
|
-
created_at:
|
|
300
|
+
created_at: OmniauthOpenidFederation::TimeHelpers.now
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
# Step 5: Redirect to provider
|