omniauth_openid_federation 1.3.0 → 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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -1
  3. data/README.md +120 -14
  4. data/app/controllers/omniauth_openid_federation/federation_controller.rb +3 -3
  5. data/config/polyrun_coverage.yml +15 -0
  6. data/config/polyrun_spec_quality.yml +29 -0
  7. data/config/routes.rb +20 -10
  8. data/examples/README_INTEGRATION_TESTING.md +3 -0
  9. data/examples/integration_test_flow.rb +10 -8
  10. data/examples/jobs/federation_cache_refresh_job.rb.example +7 -7
  11. data/examples/jobs/federation_files_generation_job.rb.example +3 -2
  12. data/examples/mock_op_server.rb +4 -5
  13. data/examples/mock_rp_server.rb +3 -3
  14. data/examples/standalone_multiple_endpoints_example.rb +494 -0
  15. data/lib/omniauth_openid_federation/access_token.rb +91 -450
  16. data/lib/omniauth_openid_federation/cache.rb +16 -0
  17. data/lib/omniauth_openid_federation/cache_adapter.rb +6 -3
  18. data/lib/omniauth_openid_federation/constants.rb +3 -0
  19. data/lib/omniauth_openid_federation/entity_statement_reader.rb +39 -14
  20. data/lib/omniauth_openid_federation/federation/entity_statement.rb +0 -4
  21. data/lib/omniauth_openid_federation/federation/entity_statement_builder.rb +7 -14
  22. data/lib/omniauth_openid_federation/federation/entity_statement_helper.rb +40 -11
  23. data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +8 -91
  24. data/lib/omniauth_openid_federation/federation/signed_jwks.rb +0 -1
  25. data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +54 -21
  26. data/lib/omniauth_openid_federation/federation_endpoint.rb +40 -172
  27. data/lib/omniauth_openid_federation/http_client.rb +145 -32
  28. data/lib/omniauth_openid_federation/http_errors.rb +14 -0
  29. data/lib/omniauth_openid_federation/id_token.rb +19 -0
  30. data/lib/omniauth_openid_federation/jwe.rb +26 -0
  31. data/lib/omniauth_openid_federation/jwks/decode.rb +0 -13
  32. data/lib/omniauth_openid_federation/jwks/fetch.rb +16 -39
  33. data/lib/omniauth_openid_federation/jwks/rotate.rb +45 -20
  34. data/lib/omniauth_openid_federation/jws.rb +3 -11
  35. data/lib/omniauth_openid_federation/jwt_response_decoder.rb +290 -0
  36. data/lib/omniauth_openid_federation/oidc_client.rb +101 -0
  37. data/lib/omniauth_openid_federation/rack.rb +2 -0
  38. data/lib/omniauth_openid_federation/rack_endpoint.rb +21 -9
  39. data/lib/omniauth_openid_federation/rate_limiter.rb +10 -12
  40. data/lib/omniauth_openid_federation/secure_compare.rb +15 -0
  41. data/lib/omniauth_openid_federation/strategy/authorization_request.rb +220 -0
  42. data/lib/omniauth_openid_federation/strategy/callback_handling.rb +135 -0
  43. data/lib/omniauth_openid_federation/strategy/client_construction.rb +101 -0
  44. data/lib/omniauth_openid_federation/strategy/client_entity_statement.rb +211 -0
  45. data/lib/omniauth_openid_federation/strategy/endpoint_resolution.rb +433 -0
  46. data/lib/omniauth_openid_federation/strategy/failure_handling.rb +75 -0
  47. data/lib/omniauth_openid_federation/strategy/id_token_decoding.rb +268 -0
  48. data/lib/omniauth_openid_federation/strategy/jwks_resolution.rb +268 -0
  49. data/lib/omniauth_openid_federation/strategy/provider_entity_statement.rb +134 -0
  50. data/lib/omniauth_openid_federation/strategy/userinfo_decoding.rb +61 -0
  51. data/lib/omniauth_openid_federation/strategy.rb +27 -1910
  52. data/lib/omniauth_openid_federation/tasks/authentication_flow_tester.rb +237 -0
  53. data/lib/omniauth_openid_federation/tasks/callback_processor.rb +235 -0
  54. data/lib/omniauth_openid_federation/tasks/client_keys_preparer.rb +96 -0
  55. data/lib/omniauth_openid_federation/tasks/local_endpoint_tester.rb +189 -0
  56. data/lib/omniauth_openid_federation/tasks/path_resolver.rb +20 -0
  57. data/lib/omniauth_openid_federation/tasks_helper.rb +28 -790
  58. data/lib/omniauth_openid_federation/time_helpers.rb +67 -0
  59. data/lib/omniauth_openid_federation/user_info.rb +15 -0
  60. data/lib/omniauth_openid_federation/utils.rb +14 -9
  61. data/lib/omniauth_openid_federation/validators.rb +55 -44
  62. data/lib/omniauth_openid_federation/version.rb +1 -1
  63. data/lib/omniauth_openid_federation.rb +10 -3
  64. data/lib/tasks/omniauth_openid_federation.rake +5 -5
  65. data/sig/omniauth_openid_federation.rbs +8 -1
  66. data/sig/strategy.rbs +6 -6
  67. metadata +252 -43
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bdb5dbe8832e2837c2970556b8498492c8e32489e8efad28eff176be94af1a1d
4
- data.tar.gz: 7addf88091e26b0c603a5bd50fe8c701d8d8cb2c5a5854549a59795b7782a792
3
+ metadata.gz: ed139d6baa665a3a7dce282bed397079492844cef7b45de0b5787edf3de9284e
4
+ data.tar.gz: c4d1f5e957c5aa2340d8cd223bfe1c591ed8b85f79417f775468868a54e4d465
5
5
  SHA512:
6
- metadata.gz: 31a2b0f6042da80bcb445c0d92e137b06da8d017e58e05835fb0983e35df4215797ccabef024378cc5faa78006037f0f338cb074bc8784c72eea02b349fbc248
7
- data.tar.gz: cd47a106c42a20a80caa3db3212dbe4c1748af65a17f88b4a57273f35a01cbc79e814b998f793691b50b368592f5d31ac31ff87c1831880497047b02178569d1
6
+ metadata.gz: bb56e08a749c63510e1d1456766ed2375827cb41f4171e25fbc260a5e06563fd59559fed10c7cf95ccdf7893a70d1567a8372fbda13c454fa7be765ae775db0d
7
+ data.tar.gz: 576b586169d64ba1b4989db48badd0cb1da1f145336e0786dabd8b41770344b9a19b0c43b4a28859738f0d72c63fb985069372868d41fcb64665ab6821cfcf4b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
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
+
34
+ ## 1.3.2 (2025-12-09)
35
+
36
+ - Added `TimeHelpers` module for compatibility with non-Rails environments
37
+ - Replaced `Time.zone` usage with `TimeHelpers` to work with or without ActiveSupport
38
+
39
+ ## 1.3.1 (2025-12-09)
40
+
41
+ - Enhanced SSL configuration for HTTPS requests in tasks_helper.rb
42
+ - Updated federation controller to use ApplicationController
43
+ - Updated routes to have semaphore if it is already loaded
44
+ - Updated gemfiles and workflows for Rails 8 compatibility
45
+ - Improved time handling in integration and mock server classes using Time.zone.now
46
+
3
47
  ## 1.3.0 (2025-11-28)
4
48
 
5
49
  - Added `prepare_request_object_params` proc option to customize request parameters before signing
@@ -25,7 +69,7 @@
25
69
  ## 1.1.0 (2025-11-26)
26
70
 
27
71
  - Enhanced instrumentation: All blocking exceptions automatically reported through instrumentation system, including OmniAuth middleware errors (like AuthenticityTokenProtection)
28
- - CSRF protection instrumentation: New authenticity_error event type for reporting OmniAuth CSRF protection failures
72
+ - CSRF protection instrumentation: New authenticity_error event type for reporting OmniAuth CSRF failures
29
73
  - Comprehensive error reporting: Override fail! method in strategy to catch and instrument all authentication failures
30
74
  - CSRF protection documentation: Added comprehensive Step 7 in README explaining CSRF protection configuration for both request and callback phases
31
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
@@ -1,15 +1,9 @@
1
1
  # omniauth_openid_federation
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/omniauth_openid_federation.svg?v=1.1.0)](https://badge.fury.io/rb/omniauth_openid_federation) [![Test Status](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/omniauth_openid_federation.rb/graph/badge.svg?token=CX3O9M1GIT)](https://codecov.io/gh/amkisko/omniauth_openid_federation.rb/graph/badge.svg)
3
+ [![Gem Version](https://badge.fury.io/rb/omniauth_openid_federation.svg?v=1.1.0)](https://badge.fury.io/rb/omniauth_openid_federation) [![Test Status](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/omniauth_openid_federation.rb/graph/badge.svg?token=CX3O9M1GIT)](https://codecov.io/gh/amkisko/omniauth_openid_federation.rb) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=amkisko_omniauth_openid_federation.rb&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=amkisko_omniauth_openid_federation.rb)
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 custom params
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/config/initializers/devise.rb.example`
387
- - `examples/config/initializers/omniauth_openid_federation.rb.example`
388
- - `examples/config/open_id_connect_config.rb.example`
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
- bin/rspec
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://github.com/nov/jwe) - JSON Web Encryption
417
- - [openid_connect](https://github.com/nov/openid_connect) - OpenID Connect client
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>
@@ -12,7 +12,7 @@
12
12
  require "omniauth_openid_federation/cache_adapter"
13
13
 
14
14
  module OmniauthOpenidFederation
15
- class FederationController < ActionController::Base
15
+ class FederationController < ::ApplicationController
16
16
  # Serve the entity statement
17
17
  #
18
18
  # GET /.well-known/openid-federation
@@ -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 = "federation:jwks:#{Digest::SHA256.hexdigest(jwks_to_serve.to_json)}"
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 = "federation:signed_jwks:#{Digest::SHA256.hexdigest(signed_jwks_jwt)}"
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
data/config/routes.rb CHANGED
@@ -1,17 +1,27 @@
1
1
  # Routes for OpenID Federation well-known endpoints
2
2
  # These routes are mounted at the root level (not namespaced) because
3
3
  # OpenID Federation spec requires specific well-known paths
4
- OmniauthOpenidFederation::Engine.routes.draw do
5
- # OpenID Federation 1.0 Section 9: Entity Configuration endpoint
6
- # MUST be at /.well-known/openid-federation
7
- get "/.well-known/openid-federation", to: "omniauth_openid_federation/federation#show", as: :openid_federation
4
+ # Guard to prevent double-loading routes (important for test isolation)
5
+ # Use a file-level instance variable that doesn't trigger class loading
6
+ @_omniauth_openid_federation_routes_loaded ||= false
7
+ unless @_omniauth_openid_federation_routes_loaded
8
+ @_omniauth_openid_federation_routes_loaded = true
9
+ begin
10
+ OmniauthOpenidFederation::Engine.routes.draw do
11
+ # OpenID Federation 1.0 Section 9: Entity Configuration endpoint
12
+ # MUST be at /.well-known/openid-federation
13
+ get "/.well-known/openid-federation", to: "omniauth_openid_federation/federation#show", as: :openid_federation
8
14
 
9
- # Fetch endpoint for Subordinate Statements (Section 6.1)
10
- get "/.well-known/openid-federation/fetch", to: "omniauth_openid_federation/federation#fetch", as: :openid_federation_fetch
15
+ # Fetch endpoint for Subordinate Statements (Section 6.1)
16
+ get "/.well-known/openid-federation/fetch", to: "omniauth_openid_federation/federation#fetch", as: :openid_federation_fetch
11
17
 
12
- # Standard JWKS endpoint
13
- get "/.well-known/jwks.json", to: "omniauth_openid_federation/federation#jwks", as: :openid_federation_jwks
18
+ # Standard JWKS endpoint
19
+ get "/.well-known/jwks.json", to: "omniauth_openid_federation/federation#jwks", as: :openid_federation_jwks
14
20
 
15
- # Signed JWKS endpoint (OpenID Federation requirement)
16
- get "/.well-known/signed-jwks.json", to: "omniauth_openid_federation/federation#signed_jwks", as: :openid_federation_signed_jwks
21
+ # Signed JWKS endpoint (OpenID Federation requirement)
22
+ get "/.well-known/signed-jwks.json", to: "omniauth_openid_federation/federation#signed_jwks", as: :openid_federation_signed_jwks
23
+ end
24
+ rescue NameError, LoadError
25
+ # Rails not available or not fully initialized - routes will be loaded when Engine initializes
26
+ end
17
27
  end
@@ -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
- require "jwe"
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 = Time.now
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 = (Time.now - start_time).round(1)
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 = (Time.now - start_time).round(1)
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 = (Time.now - start_time).round(1)
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 = JWE.encrypt(signed_jwt, wrong_key)
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
- Rails.cache.delete("federation:jwks")
42
- Rails.cache.delete("federation:signed_jwks")
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 = "federation:jwks:#{Digest::SHA256.hexdigest(jwks.to_json)}"
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 = "federation:signed_jwks:#{Digest::SHA256.hexdigest(signed_jwks)}"
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
- Rails.cache.delete("federation:jwks")
76
- Rails.cache.delete("federation:signed_jwks")
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
 
@@ -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 key_data.nil? || key_data.empty?
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 key_data.nil? || key_data.empty?
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: Time.now
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 = JWE.decrypt(request_jwt, MockOPServer::ENCRYPTION_KEY)
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
@@ -66,7 +66,7 @@ class MockRPServer
66
66
  end
67
67
 
68
68
  def self.load_signing_key(key_data)
69
- if key_data.nil? || key_data.empty?
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 key_data.nil? || key_data.empty?
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: Time.now
300
+ created_at: OmniauthOpenidFederation::TimeHelpers.now
301
301
  }
302
302
 
303
303
  # Step 5: Redirect to provider