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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +51 -1
- data/README.md +136 -15
- data/SECURITY.md +11 -14
- 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 +42 -4
- data/lib/omniauth_openid_federation/cache_adapter.rb +6 -3
- data/lib/omniauth_openid_federation/constants.rb +6 -0
- data/lib/omniauth_openid_federation/federation/entity_statement.rb +0 -4
- data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +33 -21
- data/lib/omniauth_openid_federation/federation/signed_jwks.rb +4 -2
- data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +85 -20
- data/lib/omniauth_openid_federation/federation/trust_chain_signature.rb +62 -0
- data/lib/omniauth_openid_federation/federation_endpoint.rb +1 -1
- data/lib/omniauth_openid_federation/http_client.rb +182 -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 +288 -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 +133 -0
- data/lib/omniauth_openid_federation/strategy/client_construction.rb +101 -0
- data/lib/omniauth_openid_federation/strategy/client_entity_statement.rb +216 -0
- data/lib/omniauth_openid_federation/strategy/endpoint_resolution.rb +432 -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 +262 -0
- data/lib/omniauth_openid_federation/strategy/provider_entity_statement.rb +135 -0
- data/lib/omniauth_openid_federation/strategy/userinfo_decoding.rb +93 -0
- data/lib/omniauth_openid_federation/strategy.rb +30 -1911
- 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 +11 -3
- 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 +188 -63
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da45b4e4b8bb24f3b4191c0a3c297e23dad26f3d89ffefaceebc0b225117dc59
|
|
4
|
+
data.tar.gz: 1f8f747bc2f5815fc0d9b2005fa21a2c542dc08f6b3f223072cc7b9e95f0f089
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
[](https://badge.fury.io/rb/omniauth_openid_federation) [](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml) [](https://codecov.io/
|
|
3
|
+
[](https://badge.fury.io/rb/omniauth_openid_federation) [](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml) [](https://app.codecov.io/github/amkisko/omniauth_openid_federation.rb) [](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
|
-
|
|
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
|
|
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.
|
|
380
|
-
- Rails >=
|
|
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/
|
|
387
|
-
- `examples/config/initializers/
|
|
388
|
-
- `examples/config/
|
|
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
|
-
|
|
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://
|
|
417
|
-
- [
|
|
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
|
-
|
|
5
|
+
Email security details to: security@kiskolabs.com
|
|
6
6
|
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|