omniauth_openid_federation 1.3.2 → 2.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -1
  3. data/README.md +136 -15
  4. data/SECURITY.md +11 -14
  5. data/app/controllers/omniauth_openid_federation/federation_controller.rb +2 -2
  6. data/config/polyrun_coverage.yml +15 -0
  7. data/config/polyrun_spec_quality.yml +29 -0
  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 +42 -4
  17. data/lib/omniauth_openid_federation/cache_adapter.rb +6 -3
  18. data/lib/omniauth_openid_federation/constants.rb +6 -0
  19. data/lib/omniauth_openid_federation/federation/entity_statement.rb +0 -4
  20. data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +33 -21
  21. data/lib/omniauth_openid_federation/federation/signed_jwks.rb +4 -2
  22. data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +85 -20
  23. data/lib/omniauth_openid_federation/federation/trust_chain_signature.rb +62 -0
  24. data/lib/omniauth_openid_federation/federation_endpoint.rb +1 -1
  25. data/lib/omniauth_openid_federation/http_client.rb +182 -32
  26. data/lib/omniauth_openid_federation/http_errors.rb +14 -0
  27. data/lib/omniauth_openid_federation/id_token.rb +19 -0
  28. data/lib/omniauth_openid_federation/jwe.rb +26 -0
  29. data/lib/omniauth_openid_federation/jwks/decode.rb +0 -13
  30. data/lib/omniauth_openid_federation/jwks/fetch.rb +16 -39
  31. data/lib/omniauth_openid_federation/jws.rb +2 -11
  32. data/lib/omniauth_openid_federation/jwt_response_decoder.rb +288 -0
  33. data/lib/omniauth_openid_federation/oidc_client.rb +101 -0
  34. data/lib/omniauth_openid_federation/rack.rb +2 -0
  35. data/lib/omniauth_openid_federation/rack_endpoint.rb +2 -2
  36. data/lib/omniauth_openid_federation/rate_limiter.rb +10 -12
  37. data/lib/omniauth_openid_federation/secure_compare.rb +15 -0
  38. data/lib/omniauth_openid_federation/strategy/authorization_request.rb +220 -0
  39. data/lib/omniauth_openid_federation/strategy/callback_handling.rb +133 -0
  40. data/lib/omniauth_openid_federation/strategy/client_construction.rb +101 -0
  41. data/lib/omniauth_openid_federation/strategy/client_entity_statement.rb +216 -0
  42. data/lib/omniauth_openid_federation/strategy/endpoint_resolution.rb +432 -0
  43. data/lib/omniauth_openid_federation/strategy/failure_handling.rb +75 -0
  44. data/lib/omniauth_openid_federation/strategy/id_token_decoding.rb +268 -0
  45. data/lib/omniauth_openid_federation/strategy/jwks_resolution.rb +262 -0
  46. data/lib/omniauth_openid_federation/strategy/provider_entity_statement.rb +135 -0
  47. data/lib/omniauth_openid_federation/strategy/userinfo_decoding.rb +93 -0
  48. data/lib/omniauth_openid_federation/strategy.rb +30 -1911
  49. data/lib/omniauth_openid_federation/tasks/authentication_flow_tester.rb +237 -0
  50. data/lib/omniauth_openid_federation/tasks/callback_processor.rb +235 -0
  51. data/lib/omniauth_openid_federation/tasks/client_keys_preparer.rb +96 -0
  52. data/lib/omniauth_openid_federation/tasks/local_endpoint_tester.rb +189 -0
  53. data/lib/omniauth_openid_federation/tasks/path_resolver.rb +20 -0
  54. data/lib/omniauth_openid_federation/tasks_helper.rb +28 -808
  55. data/lib/omniauth_openid_federation/time_helpers.rb +7 -0
  56. data/lib/omniauth_openid_federation/user_info.rb +15 -0
  57. data/lib/omniauth_openid_federation/utils.rb +11 -3
  58. data/lib/omniauth_openid_federation/validators.rb +43 -8
  59. data/lib/omniauth_openid_federation/version.rb +1 -1
  60. data/lib/omniauth_openid_federation.rb +9 -3
  61. data/lib/tasks/omniauth_openid_federation.rake +1 -2
  62. data/sig/omniauth_openid_federation.rbs +2 -1
  63. data/sig/strategy.rbs +6 -6
  64. metadata +188 -63
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f8bba8b102bcf8c24fdd775d5d898d92b27c27d8e6df40c63ff23756aee4f8d
4
- data.tar.gz: 900a1851f0a9917b5593f6146d965271180a979d312b9ff5fa15c295578172e5
3
+ metadata.gz: da45b4e4b8bb24f3b4191c0a3c297e23dad26f3d89ffefaceebc0b225117dc59
4
+ data.tar.gz: 1f8f747bc2f5815fc0d9b2005fa21a2c542dc08f6b3f223072cc7b9e95f0f089
5
5
  SHA512:
6
- metadata.gz: 31a7bcc8b8e1661dc9bcc13604e2e92279d81501e0a1da25b8b48a6f9bf35251489720aa1879b6beeb89de32bd2970d72c086e58b1be43d0f4c125809339fb51
7
- data.tar.gz: 5c27ea824e18a49a0fe899f6f6f2ade3c6a10872ade9b2eca3690cada27e06aa5ada813f1cdf34763881bc8c4ee5d1033f0a7ed821a8378602ee72db169150f7
6
+ metadata.gz: 16d2a33e35399bc802f47e6f7d69e7995a20781024bfb22e7d938f8035069dc5f4bfb4a62c0de5aa4a1a42842472e7c19d4bacea342c5cad9cd92d81d59f0d90
7
+ data.tar.gz: efec81bf4ec61d381ee7c03f2dc5e598c9d4d47a7768847f4353c746849cc141b339f301f9f99c3e5f8758d43248bd2682fc7c70fa2df612620f4f6805e878da
data/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 2.1.0 (2026-10-29)
6
+
7
+ - BREAKING: Require Ruby `>= 3.4` (gemspec); drop Rails 6 and TruffleRuby appraisal targets from the tested matrix
8
+ - Fix trust chain resolution to bind fetched entity configurations to the requested entity id and verify trust-anchor statements with pinned `trust_anchors[].jwks`
9
+ - Fix `EntityStatementValidator` authority_hints checks to use the subject entity configuration (`subject_entity_configuration:`)
10
+ - Fix UserInfo merge so `sub` must match the ID token and identity claims (`iss`, `sub`, `aud`, `exp`, `iat`) stay from the ID token
11
+ - Fix signed JWKS handling to fail closed on signature, validation, and fetch errors instead of falling back to unsigned `jwks_uri`
12
+ - Fix signed JWKS cache keys to include the verifying `entity_jwks` digest
13
+ - Fix OAuth callback to validate `state` before handling authorization `error` responses
14
+ - Fix configured `entity_statement_path` missing-file handling to raise `ConfigurationError` instead of silent network fallback
15
+ - Fix endpoint resolution so configured entity-statement files under `Dir.tmpdir` are not deleted after resolve
16
+ - Fix `client_entity_statement_url` so `load_client_entity_statement` fetches from the URL
17
+ - Fix `Utils.validate_file_path!` allowed-directory checks to require a path-separator boundary
18
+ - Stop streaming HTTP responses at the configured byte limit and close oversized responses before buffering the remaining body
19
+ - Add `rfcs/` starter pack: process (RFC 0001), positioning (RFC 0002), and Standards Track design RFCs 0003–0006 for strategy, trust chain, token verification, and federation endpoint
20
+ - Document Ruby `>= 3.4` in README requirements
21
+
22
+ ## 2.0.0 (2026-07-06)
23
+
24
+ - BREAKING: Replace `openid_connect` and `json-jwt` with `OmniauthOpenidFederation::OidcClient`, `AccessToken`, and `IdToken` built on `oauth2` and `jwt`
25
+ - BREAKING: Drop explicit `rack` runtime dependency; load Rack federation endpoints via `require "omniauth_openid_federation/rack"`
26
+ - BREAKING: Validate ID token `iss`, `aud`, and session `nonce` in `callback_phase`
27
+ - BREAKING: Fail closed on trust chain resolution errors instead of returning empty metadata
28
+ - BREAKING: Reject `alg: none` in access token JWT handling
29
+ - BREAKING: Remove broad JWKS decode retry on non-signature errors
30
+ - BREAKING: Reject unknown `crit` claims in entity statement validation
31
+ - BREAKING: Enforce minimum RSA key size (2048 bits) in `Validators.validate_private_key!`
32
+ - Use `jwe` gem for JWE encrypt/decrypt instead of `json-jwt`
33
+ - Refactor `OmniAuth::Strategies::OpenIDFederation` into concern modules under `lib/omniauth_openid_federation/strategy/`
34
+ - Add `OmniauthOpenidFederation::SecureCompare` for constant-time string comparison in the strategy
35
+ - Add `OmniauthOpenidFederation::JwtResponseDecoder` for encrypted/signed JWT userinfo and resource responses
36
+ - Add behavioral contract specs for token exchange, userinfo, and JWE parity with former `openid_connect` flow
37
+ - Add Telia Tunnistus PP entity statement fixture and contract spec (A128GCM and A128CBC-HS256)
38
+ - Add provider-agnostic strategy options: `default_request_object_claims`, `required_request_object_claims`, `allowed_acr_values`, `require_entity_statement_fingerprint`
39
+ - Use `Jwe.encrypted?` for JWE detection in `AccessToken#resource_request` and `TasksHelper`
40
+ - Deduplicate entity statement loading in `access_token.rb` via `load_entity_statement_content`
41
+ - Verify subordinate entity statement signatures in trust chain resolution
42
+ - Wire `verify_ssl` into `HttpClient` SSL options
43
+ - Use `CacheAdapter` in `RateLimiter` instead of `Rails.cache` directly
44
+ - Use issuer-scoped cache keys for federation JWKS and signed JWKS endpoints
45
+ - 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
46
+ - Fix flaky `test_local_endpoint` spec when `SSL_CERT_FILE` is set in the environment
47
+ - Replace internal `decode_id_token` stubs in strategy specs with JWKS-backed setup
48
+ - Add `HttpClient.post` with form data, headers, and connect/read timeouts
49
+ - Retry GET requests on HTTP 429, 502, and 503; POST requests default to no retry
50
+ - Auto-configure SSL CA file when peer verification is enabled
51
+ - Route rake task HTTP through `HttpClient` instead of ad hoc `Net::HTTP`
52
+
3
53
  ## 1.3.2 (2025-12-09)
4
54
 
5
55
  - Added `TimeHelpers` module for compatibility with non-Rails environments
@@ -38,7 +88,7 @@
38
88
  ## 1.1.0 (2025-11-26)
39
89
 
40
90
  - 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 protection failures
91
+ - CSRF protection instrumentation: New authenticity_error event type for reporting OmniAuth CSRF failures
42
92
  - Comprehensive error reporting: Override fail! method in strategy to catch and instrument all authentication failures
43
93
  - CSRF protection documentation: Added comprehensive Step 7 in README explaining CSRF protection configuration for both request and callback phases
44
94
  - 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,14 +1,10 @@
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) [![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)
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://app.codecov.io/github/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/project/overview?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>
7
+ Design changes go through `rfcs/` (RFC 0001).
12
8
 
13
9
  ## Installation
14
10
 
@@ -279,7 +275,14 @@ Use `prepare_request_object_params` proc to modify parameters before they're add
279
275
 
280
276
  ```ruby
281
277
  config.omniauth :openid_federation,
282
- request_object_params: [:ftn_spname], # Allow-list for custom params
278
+ request_object_params: [:ftn_spname], # Allow-list for provider-specific params (example: Finnish FTN)
279
+ default_request_object_claims: {
280
+ ui_locales: "fi",
281
+ acr_values: ENV["OPENID_ACR_VALUES"]
282
+ },
283
+ required_request_object_claims: %w[ftn_spname acr_values ui_locales],
284
+ allowed_acr_values: [ENV["OPENID_ACR_VALUES"]].compact,
285
+ require_entity_statement_fingerprint: true,
283
286
  prepare_request_object_params: proc do |params|
284
287
  # Combine config acr_values with form acr_values
285
288
  form_acr_values = params["acr_values"]&.to_s&.strip
@@ -310,6 +313,96 @@ config.omniauth :openid_federation,
310
313
 
311
314
  The proc will combine this with config values before adding to the signed JWT.
312
315
 
316
+ ## Rodauth Integration
317
+
318
+ This strategy can be used with the Rodauth authentication framework via the
319
+ [`rodauth-omniauth` feature](https://github.com/janko/rodauth-omniauth).
320
+
321
+ ### Minimal Roda/Rodauth setup
322
+
323
+ ```ruby
324
+ require "roda"
325
+ require "rodauth"
326
+ require "rodauth/omniauth"
327
+ require "omniauth_openid_federation"
328
+
329
+ DB = Sequel.sqlite # or your production database
330
+
331
+ DB.create_table? :accounts do
332
+ primary_key :id
333
+ String :email, null: false
334
+ end
335
+
336
+ DB.create_table? :account_identities do
337
+ primary_key :id
338
+ foreign_key :account_id, :accounts, null: false
339
+ String :provider, null: false
340
+ String :uid, null: false
341
+ index [:provider, :uid], unique: true
342
+ end
343
+
344
+ class App < Roda
345
+ plugin :sessions, secret: ENV.fetch("SESSION_SECRET") { SecureRandom.hex(32) }
346
+ plugin :json
347
+
348
+ plugin :rodauth, json: true do
349
+ db DB
350
+ enable :omniauth
351
+
352
+ # Mount OmniAuth under /auth, as recommended by rodauth-omniauth
353
+ omniauth_prefix "/auth"
354
+
355
+ omniauth_provider :openid_federation,
356
+ nil,
357
+ nil,
358
+ strategy_class: OmniAuth::Strategies::OpenIDFederation,
359
+ name: :openid_federation,
360
+ issuer: ENV.fetch("OPENID_ISSUER", "https://provider.example.com"),
361
+ audience: ENV.fetch("OPENID_AUDIENCE", "https://provider.example.com"),
362
+ client_options: {
363
+ identifier: ENV["OPENID_CLIENT_ID"],
364
+ redirect_uri: ENV["OPENID_REDIRECT_URI"] || "https://your-app.example.com/auth/openid_federation/callback",
365
+ host: URI.parse(ENV.fetch("OPENID_ISSUER", "https://provider.example.com")).host,
366
+ scheme: URI.parse(ENV.fetch("OPENID_ISSUER", "https://provider.example.com")).scheme,
367
+ authorization_endpoint: "/oauth2/authorize",
368
+ token_endpoint: "/oauth2/token",
369
+ userinfo_endpoint: "/oauth2/userinfo",
370
+ jwks_uri: "/.well-known/jwks.json",
371
+ private_key: OpenSSL::PKey::RSA.new(
372
+ Base64.decode64(ENV.fetch("OPENID_CLIENT_PRIVATE_KEY_BASE64"))
373
+ )
374
+ },
375
+ entity_statement_url: ENV["OPENID_ENTITY_STATEMENT_URL"],
376
+ entity_statement_fingerprint: ENV["OPENID_ENTITY_STATEMENT_FINGERPRINT"]
377
+ end
378
+
379
+ route do |r|
380
+ # Rodauth authentication + OmniAuth endpoints
381
+ # r.rodauth automatically handles omniauth routes based on omniauth_prefix setting
382
+ r.rodauth
383
+
384
+ r.root do
385
+ if rodauth.logged_in?
386
+ {logged_in: true, account_id: rodauth.session_value}
387
+ else
388
+ {logged_in: false}
389
+ end
390
+ end
391
+ end
392
+ end
393
+ ```
394
+
395
+ With this configuration, the Rodauth `:omniauth` feature:
396
+
397
+ - uses `OmniAuth::Strategies::OpenIDFederation` as the strategy implementation,
398
+ - calls the strategy's request and callback phases under `/auth/openid_federation`,
399
+ - persists external identities into the `account_identities` table following the
400
+ [`rodauth-omniauth` schema](https://github.com/janko/rodauth-omniauth),
401
+ - and exposes helper methods like `rodauth.omniauth_auth`, `rodauth.omniauth_email`,
402
+ and `rodauth.omniauth_name` as documented in the
403
+ [Rodauth documentation](https://rodauth.jeremyevans.net/documentation.html)
404
+ and [`rodauth-omniauth` README](https://github.com/janko/rodauth-omniauth).
405
+
313
406
  ## Rake Tasks
314
407
 
315
408
  ### Prepare Client Keys
@@ -356,6 +449,10 @@ rake openid_federation:test_authentication_flow[
356
449
  - `always_encrypt_request_object` - Force encryption of request objects (default: false)
357
450
  - `request_object_params` - Array of parameter names to include in request object (allow-list)
358
451
  - `prepare_request_object_params` - Proc to modify params before adding to signed request object: `proc { |params| modified_params }`
452
+ - `default_request_object_claims` - Hash of default claims merged before `prepare_request_object_params` (request params override defaults)
453
+ - `required_request_object_claims` - Array of claim names that must be present in the signed request object (raises before redirect)
454
+ - `allowed_acr_values` - When set, ID token `acr` must match one of the configured values
455
+ - `require_entity_statement_fingerprint` - When true, fetching provider entity statements from URL or issuer requires `entity_statement_fingerprint`
359
456
  - `discovery` - Enable automatic endpoint discovery (default: true)
360
457
 
361
458
  ## Security
@@ -376,16 +473,17 @@ rake openid_federation:test_authentication_flow[
376
473
 
377
474
  ## Requirements
378
475
 
379
- - Ruby >= 3.0
380
- - Rails >= 6.1 (or compatible Rack application)
476
+ - Ruby >= 3.4
477
+ - Rails >= 7.2 (or compatible Rack application; see Appraisal gemfiles)
381
478
  - OpenSSL (for RSA key operations)
382
479
 
383
480
  ## Example Files
384
481
 
385
482
  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`
483
+ - `examples/standalone_multiple_endpoints_example.rb` - Standalone Sinatra app with multiple auth endpoints and entrance point-based redirects
484
+ - `examples/config/initializers/devise.rb.example` - Devise integration example (Rails)
485
+ - `examples/config/initializers/omniauth_openid_federation.rb.example` - Federation endpoint configuration
486
+ - `examples/config/open_id_connect_config.rb.example` - Configuration class example
389
487
 
390
488
  ## Development
391
489
 
@@ -393,9 +491,11 @@ See `examples/` directory for complete configuration examples:
393
491
  git clone https://github.com/amkisko/omniauth_openid_federation.rb.git
394
492
  cd omniauth_openid_federation.rb
395
493
  bundle install
396
- bin/rspec
494
+ bundle exec polyrun parallel-rspec --workers 5 -c polyrun.yml
397
495
  ```
398
496
 
497
+ 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: **[spec/README.md](spec/README.md)** and **`polyrun.yml`**.
498
+
399
499
  ## Contributing
400
500
 
401
501
  Contributions welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
@@ -413,12 +513,33 @@ Contributions welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidel
413
513
  - [omniauth](https://github.com/omniauth/omniauth) - Authentication framework
414
514
  - [devise](https://github.com/heartcombo/devise) - Rails authentication solution
415
515
  - [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
516
+ - [jwe](https://rubygems.org/gems/jwe) - JSON Web Encryption (via `OmniauthOpenidFederation::Jwe`)
517
+ - [oauth2](https://github.com/oauth-xx/oauth2) - OAuth 2.0 client (via `OmniauthOpenidFederation::OidcClient`)
418
518
  - [http](https://github.com/httprb/http) - HTTP client
419
519
  - [anyway_config](https://github.com/palkan/anyway_config) - Configuration management
420
520
  - [action_reporter](https://github.com/basecamp/action_reporter) - Error reporting
421
521
 
522
+ ## Links
523
+
524
+ - [GitHub](https://github.com/amkisko/omniauth_openid_federation.rb)
525
+ - [GitLab](https://gitlab.com/amkisko/omniauth_openid_federation.rb)
526
+ - [RubyGems](https://rubygems.org/gems/omniauth_openid_federation)
527
+ - [Versions Atom](https://rubygems.org/gems/omniauth_openid_federation/versions.atom) (feed id `10007301350098`)
528
+ - [libraries.io](https://libraries.io/rubygems/omniauth_openid_federation)
529
+ - [Deps.dev](https://deps.dev/rubygems/omniauth_openid_federation)
530
+ - [SonarCloud](https://sonarcloud.io/project/overview?id=amkisko_omniauth_openid_federation.rb)
531
+ - [Snyk](https://snyk.io/test/github/amkisko/omniauth_openid_federation.rb)
532
+ - [Codecov](https://app.codecov.io/github/amkisko/omniauth_openid_federation.rb)
533
+ - [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/amkisko/omniauth_openid_federation.rb)
534
+
422
535
  ## License
423
536
 
424
537
  MIT License. See [LICENSE.md](LICENSE.md) for details.
538
+
539
+ ## Sponsors
540
+
541
+ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
542
+
543
+ <a href="https://www.kiskolabs.com">
544
+ <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
545
+ </a>
data/SECURITY.md CHANGED
@@ -2,27 +2,24 @@
2
2
 
3
3
  ## Reporting a Vulnerability
4
4
 
5
- **Do NOT** open a public GitHub issue for security vulnerabilities.
5
+ Email security details to: security@kiskolabs.com
6
6
 
7
- Email security details to: **security@kiskolabs.com**
8
-
9
- Include: description, steps to reproduce, potential impact, and suggested fix (if available).
7
+ Send the report only to that address. Include description, steps to reproduce, potential impact, and a suggested fix when you have one.
10
8
 
11
9
  ### Response Timeline
12
10
 
13
- - We will acknowledge receipt of your report
14
- - We will provide an initial assessment
15
- - We will keep you informed of our progress and resolution timeline
11
+ - We will acknowledge receipt of your report.
12
+ - We will provide an initial assessment.
13
+ - We will keep you informed of our progress and resolution timeline.
16
14
 
17
15
  ### Disclosure Policy
18
16
 
19
- - We will work with you to understand and resolve the issue
20
- - We will credit you for the discovery (unless you prefer to remain anonymous)
21
- - We will publish a security advisory after the vulnerability is patched
22
- - We will coordinate public disclosure with you
17
+ - We will work with you to understand and resolve the issue.
18
+ - We will credit you for the discovery unless you prefer to remain anonymous.
19
+ - We will publish a security advisory after the vulnerability is patched.
20
+ - We will coordinate public disclosure with you.
23
21
 
24
22
  ## Automation Security
25
23
 
26
- * **Context Isolation:** It is strictly forbidden to include production credentials, API keys, or Personally Identifiable Information (PII) in prompts sent to third-party LLMs or automation services.
27
-
28
- * **Supply Chain:** All automated dependencies must be verified.
24
+ - Keep production credentials, API keys, and personal data out of prompts sent to third-party models or automation services.
25
+ - Verify automated dependencies.
@@ -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
@@ -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.zone.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.zone.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.zone.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.zone.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.blank?
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.blank?
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.zone.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.blank?
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.blank?
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.zone.now
300
+ created_at: OmniauthOpenidFederation::TimeHelpers.now
301
301
  }
302
302
 
303
303
  # Step 5: Redirect to provider