omniauth_openid_federation 1.0.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff53053ffcbf2f63f91ae342158b5f11e881f3466c8d1ec1a25c06c8d1ff6b9b
4
- data.tar.gz: d11bdc15e0dbbbd710cee19ac670a7425af0f3e37e42765c93df9d4dfac224ac
3
+ metadata.gz: e299269e65c33735c84f2bec3ee0164bc5cb136c8dac5088be188998d826d5bc
4
+ data.tar.gz: c35464efdf1af7957456641275cfa3a374a1600a0570fa7db7a6ceeb1a31417f
5
5
  SHA512:
6
- metadata.gz: '082caf05aaf09898c6bd992f9280570dec07ad19acedf59dcfbf8c60e74af84f03e9f140eab5d4fe4156e97d418bf5c77d623026923d4f748889cc18e555101d'
7
- data.tar.gz: 27db7564021a934505269f60bb08eb1eba84eb763b9ea5fd6c06815e4e92cd72cbefed49779d15b9bb872c729525ec3ae3760204ce1948673e708a3ae0e1f655
6
+ metadata.gz: 9e96511bd8c6972d774435015822297a44892e263b13187ef1a52741186288bf25ded9b15bb0f3c5d856796e7e3b30df9d845c5c3f67987bf7fcd94160c26eda
7
+ data.tar.gz: 6c5a94f3bb8f429cc9dd583b98032418858520cc93658e9fdba1fe7192d8cb03b9b7e11575d22f44ce5e18cd2cf7bf05f5d512d7a7f96a47404dde00445923e9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.2.0 (2025-11-27)
4
+
5
+ - Created `OmniauthOpenidFederation::Engine` class inheriting from `Rails::Engine`
6
+ - Engine provides controllers via standard Rails autoloading mechanisms
7
+ - Routes are now defined in Engine's `config/routes.rb` file
8
+ - Routes must now be mounted using `mount OmniauthOpenidFederation::Engine => "/"` in `config/routes.rb`
9
+ - `FederationEndpoint.mount_routes` is still available for backward compatibility
10
+
11
+ ## 1.1.0 (2025-11-26)
12
+
13
+ - Enhanced instrumentation: All blocking exceptions automatically reported through instrumentation system, including OmniAuth middleware errors (like AuthenticityTokenProtection)
14
+ - CSRF protection instrumentation: New authenticity_error event type for reporting OmniAuth CSRF protection failures
15
+ - Comprehensive error reporting: Override fail! method in strategy to catch and instrument all authentication failures
16
+ - CSRF protection documentation: Added comprehensive Step 7 in README explaining CSRF protection configuration for both request and callback phases
17
+ - CSRF configuration examples: Added complete examples in examples/config/initializers/devise.rb.example and examples/app/controllers/users/omniauth_callbacks_controller.rb.example
18
+ - Deprecation warnings: Added runtime deprecation warnings for json_jwt method and ftn_spname option to guide users to recommended alternatives
19
+ - Code cleanup: Removed deprecated load_signing_key method (unused, returned nil)
20
+ - Updated deprecation notices: Fixed deprecation notices to reference correct replacement methods (request_object_params instead of non-existent provider_extension_params)
21
+ - Renamed option: `allow_authorize_params` → `request_object_params` for clarity (uses RFC 9101 terminology, clearly indicates params go into JWT request object)
22
+
3
23
  ## 1.0.0 (2025-11-26)
4
24
 
5
25
  - Initial public release, production-ready
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # omniauth_openid_federation
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/omniauth_openid_federation.svg)](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)
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)
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
 
@@ -128,6 +128,8 @@ config.omniauth :openid_federation,
128
128
  - `entity_statement_path` is optional - only for offline development (cached copy)
129
129
  - `discovery: true` automatically discovers all endpoints from entity statement
130
130
 
131
+ **Important**: Don't forget to configure CSRF protection (see [Step 7: Configure CSRF Protection](#step-7-configure-csrf-protection)) to ensure proper security for both request and callback phases.
132
+
131
133
  #### For OmniAuth (non-Rails)
132
134
 
133
135
  ```ruby
@@ -176,7 +178,11 @@ OmniauthOpenidFederation::FederationEndpoint.auto_configure(
176
178
 
177
179
  ```ruby
178
180
  # config/routes.rb
179
- OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)
181
+ # RECOMMENDED: Mount the Engine (Rails-idiomatic way)
182
+ mount OmniauthOpenidFederation::Engine => "/"
183
+
184
+ # ALTERNATIVE: Use mount_routes helper (for backward compatibility)
185
+ # OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)
180
186
  ```
181
187
 
182
188
  **Key Points**:
@@ -186,34 +192,109 @@ OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)
186
192
 
187
193
  ### Step 6: Add Routes
188
194
 
189
- #### For Devise
195
+ #### Mount the Engine (Required for Federation Endpoints)
196
+
197
+ The gem provides a Rails Engine that serves the well-known OpenID Federation endpoints. Mount it in your routes:
190
198
 
191
199
  ```ruby
192
200
  # config/routes.rb
193
201
  Rails.application.routes.draw do
202
+ # Mount the Engine to enable /.well-known/openid-federation endpoint
203
+ mount OmniauthOpenidFederation::Engine => "/"
204
+
205
+ # Your other routes...
194
206
  devise_for :users, controllers: {
195
207
  omniauth_callbacks: "users/omniauth_callbacks"
196
208
  }
197
209
  end
198
210
  ```
199
211
 
200
- #### For OmniAuth
212
+ **Note**: The Engine is mounted at root (`"/"`) because OpenID Federation requires endpoints at specific well-known paths (e.g., `/.well-known/openid-federation`). The Engine's routes are defined in the gem and automatically available when mounted.
213
+
214
+ #### For OmniAuth (Non-Devise)
201
215
 
202
216
  ```ruby
203
217
  # config/routes.rb
204
218
  Rails.application.routes.draw do
219
+ mount OmniauthOpenidFederation::Engine => "/"
220
+
205
221
  get "/auth/:provider/callback", to: "sessions#create"
206
222
  get "/auth/failure", to: "sessions#failure"
207
223
  end
208
224
  ```
209
225
 
210
- ### Step 7: Create Callback Controller
226
+ #### Alternative: Manual Route Mounting (Backward Compatibility)
227
+
228
+ If you need custom paths or prefer manual route definition, you can use the `mount_routes` helper (deprecated):
229
+
230
+ ```ruby
231
+ # config/routes.rb
232
+ Rails.application.routes.draw do
233
+ # Use mount_routes helper for custom paths (deprecated - prefer Engine mounting)
234
+ OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)
235
+ # ... your other routes
236
+ end
237
+ ```
238
+
239
+ ### Step 7: Configure CSRF Protection
240
+
241
+ OmniAuth requires CSRF protection configuration to handle both the request phase (initiating OAuth) and callback phase (external provider redirect).
242
+
243
+ **Important**: The request phase uses Rails CSRF tokens (forms must include them), while the callback phase uses OAuth state parameter for CSRF protection (external providers cannot include Rails CSRF tokens).
244
+
245
+ #### For Devise (Rails)
246
+
247
+ ```ruby
248
+ # config/initializers/devise.rb
249
+ if defined?(OmniAuth)
250
+ OmniAuth.config.allowed_request_methods = [:post]
251
+ OmniAuth.config.silence_get_warning = false
252
+
253
+ # Configure CSRF validation to check tokens only for request phase (initiating OAuth)
254
+ # Callback phase uses OAuth state parameter for CSRF protection (validated in strategy)
255
+ # This ensures:
256
+ # - Request phase: Forms must include Rails CSRF tokens (standard Rails protection)
257
+ # - Callback phase: OAuth state parameter provides CSRF protection (external providers can't include Rails tokens)
258
+ OmniAuth.config.request_validation_phase = lambda do |env|
259
+ request = Rack::Request.new(env)
260
+ path = request.path
261
+
262
+ # Skip CSRF validation for callback paths (external providers can't include Rails CSRF tokens)
263
+ # OAuth state parameter provides CSRF protection for callbacks (validated in OpenIDFederation strategy)
264
+ return true if path.end_with?("/callback")
265
+
266
+ # For request phase, use Rails' standard CSRF token validation
267
+ # This ensures forms must include valid CSRF tokens when initiating OAuth
268
+ session = env["rack.session"] || {}
269
+ token = request.params["authenticity_token"] || request.get_header("X-CSRF-Token")
270
+ expected_token = session[:_csrf_token] || session["_csrf_token"]
271
+
272
+ # Validate CSRF token using constant-time comparison
273
+ if token.present? && expected_token.present?
274
+ ActiveSupport::SecurityUtils.secure_compare(token.to_s, expected_token.to_s)
275
+ else
276
+ false
277
+ end
278
+ end
279
+ end
280
+ ```
281
+
282
+ **Security Notes**:
283
+ - **Request phase** (initiating OAuth): Forms must include Rails CSRF tokens via `button_to` or `form_with` helpers
284
+ - **Callback phase** (external provider redirect): OAuth `state` parameter provides CSRF protection (automatically validated in `OpenIDFederation` strategy using constant-time comparison)
285
+ - Both layers provide equivalent security - Rails CSRF tokens for request phase, OAuth state parameter for callbacks
286
+
287
+ ### Step 8: Create Callback Controller
211
288
 
212
289
  #### For Devise
213
290
 
214
291
  ```ruby
215
292
  # app/controllers/users/omniauth_callbacks_controller.rb
216
293
  class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
294
+ # Skip Rails CSRF protection for OAuth callbacks
295
+ # OAuth callbacks from external providers cannot include Rails CSRF tokens
296
+ # CSRF protection is handled by OAuth state parameter validation in the strategy
297
+ skip_before_action :verify_authenticity_token, only: [:openid_federation, :failure]
217
298
  skip_before_action :authenticate_user!, only: [:openid_federation, :failure]
218
299
 
219
300
  def openid_federation
@@ -233,7 +314,9 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
233
314
  end
234
315
  ```
235
316
 
236
- ### Step 8: Create User Model Method
317
+ **Note**: The `skip_before_action :verify_authenticity_token` is required because Rails' `protect_from_forgery` in `ApplicationController` checks CSRF tokens for all POST requests. External providers cannot include Rails CSRF tokens in callbacks, so we skip Rails' check while relying on OAuth state parameter validation (handled by the strategy).
318
+
319
+ ### Step 9: Create User Model Method
237
320
 
238
321
  ```ruby
239
322
  # app/models/user.rb
@@ -363,7 +446,8 @@ end
363
446
  ```
364
447
 
365
448
  **Instrumented Events**:
366
- - `csrf_detected` - CSRF attack detected (state mismatch)
449
+ - `csrf_detected` - CSRF attack detected (state mismatch in callback phase)
450
+ - `authenticity_error` - OmniAuth CSRF protection blocked request (Rails CSRF token validation failed in request phase)
367
451
  - `signature_verification_failed` - JWT signature verification failed (possible MITM)
368
452
  - `decryption_failed` - Token decryption failed (possible MITM or key mismatch)
369
453
  - `token_validation_failed` - Token validation failed (possible tampering)
@@ -372,9 +456,14 @@ end
372
456
  - `entity_statement_validation_failed` - Entity statement validation failed (possible MITM)
373
457
  - `fingerprint_mismatch` - Entity statement fingerprint mismatch (possible MITM)
374
458
  - `trust_chain_validation_failed` - Trust chain validation failed
375
- - `unexpected_authentication_break` - Unexpected authentication failure
459
+ - `unexpected_authentication_break` - Unexpected authentication failure (missing code, token exchange errors, unknown errors)
376
460
  - `missing_required_claims` - Token missing required claims
377
461
 
462
+ **Note**: All blocking exceptions are automatically reported through instrumentation, including:
463
+ - OmniAuth middleware errors (like `AuthenticityTokenProtection` blocking requests)
464
+ - Strategy-level errors (CSRF detected, missing code, token exchange failures)
465
+ - Unknown error types (reported as `unexpected_authentication_break`)
466
+
378
467
  **Security Note**: All sensitive data (tokens, keys, fingerprints) is automatically sanitized before being sent to your instrumentation callback.
379
468
 
380
469
  **Key Rotation Types**:
@@ -521,7 +610,11 @@ OmniauthOpenidFederation::FederationEndpoint.auto_configure(
521
610
 
522
611
  ```ruby
523
612
  # config/routes.rb
524
- OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)
613
+ # RECOMMENDED: Mount the Engine (Rails-idiomatic way)
614
+ mount OmniauthOpenidFederation::Engine => "/"
615
+
616
+ # ALTERNATIVE: Use mount_routes helper (for backward compatibility)
617
+ # OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)
525
618
  ```
526
619
 
527
620
  **What `auto_configure` does automatically**:
@@ -742,6 +835,13 @@ See inline code documentation for complete API reference.
742
835
  - Provider may have rotated keys (auto-handled with `rotate_on_errors: true`)
743
836
  - Clear cache: `Rails.cache.delete_matched("openid_federation_jwks_*")`
744
837
 
838
+ **"Attack prevented by OmniAuth::AuthenticityTokenProtection" or "OmniAuth::AuthenticityError"**
839
+ - **Request phase (initiating OAuth)**: Ensure forms include Rails CSRF tokens using `button_to` or `form_with` helpers
840
+ - **Callback phase (external provider redirect)**: Ensure CSRF protection is configured correctly (see [Step 7: Configure CSRF Protection](#step-7-configure-csrf-protection))
841
+ - Verify `OmniAuth.config.request_validation_phase` is configured to skip CSRF validation for callback paths
842
+ - Ensure `skip_before_action :verify_authenticity_token` is present in the callback controller for callback actions
843
+ - Check that OAuth state parameter validation is working (handled automatically by the strategy)
844
+
745
845
  ## Security
746
846
 
747
847
  See [SECURITY.md](SECURITY.md) for detailed security features, protections, and vulnerability reporting.
data/SECURITY.md CHANGED
@@ -4,126 +4,25 @@
4
4
 
5
5
  **Do NOT** open a public GitHub issue for security vulnerabilities.
6
6
 
7
- Email security details to: **contact@kiskolabs.com**
7
+ Email security details to: **security@kiskolabs.com**
8
8
 
9
9
  Include: description, steps to reproduce, potential impact, and suggested fix (if available).
10
10
 
11
- **Response Timeline:**
12
- - Acknowledgment within 48 hours
13
- - Initial assessment within 7 days
14
- - Coordinated disclosure after patching
11
+ ### Response Timeline
15
12
 
16
- ## Library Security Features
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
17
16
 
18
- ### Implemented Protections
17
+ ### Disclosure Policy
19
18
 
20
- **✅ Constant-Time State Comparison**
21
- - Uses `Rack::Utils.secure_compare` for state parameter validation to prevent timing attacks
22
- - Location: `strategy.rb:182`
23
-
24
- **✅ Path Traversal Protection**
25
- - `Utils.validate_file_path!` prevents `..` sequences and validates against allowed directories
26
- - File paths are restricted to configured allowed directories
27
-
28
- **✅ Signed Request Objects (RFC 9101)**
29
- - All authorization requests **MUST** use signed request objects (mandatory per OpenID Federation 1.0 spec)
30
- - Signing is enforced at library level - cannot be bypassed
31
- - Request objects **MAY** be encrypted (optional) when provider requires it or `always_encrypt_request_object` is enabled
32
- - Prevents parameter tampering and ensures request authenticity
33
-
34
- **✅ JWT Algorithm Validation**
35
- - JWT library validates algorithms and signatures
36
- - Unsigned tokens (`alg: none`) are explicitly handled and rejected for signed tokens
37
- - Only strong algorithms accepted (RS256, etc.)
38
-
39
- **✅ Logging Sanitization**
40
- - Sensitive data (tokens, keys) is never logged
41
- - File paths are sanitized before logging
42
- - URLs are sanitized (query parameters removed) in debug logs
43
-
44
- **✅ Timeout Limits**
45
- - HTTP requests have configurable timeouts to prevent long-running requests
46
-
47
- ### Known Limitations & Risks
48
-
49
- **⚠️ SSRF Risk (Server-Side Request Forgery)**
50
- - The library fetches entity statements and JWKS from URLs without validating against internal network access
51
- - **Risk:** URLs could target localhost, private IPs, or cloud metadata endpoints
52
- - **Mitigation:** Application should validate URLs before passing to library, or implement URL validation in library configuration
53
-
54
- **⚠️ Memory Safety**
55
- - Ruby does not provide secure memory management for sensitive data
56
- - Private keys may persist in memory until garbage collection
57
- - Memory dumps may contain key material
58
- - **Mitigation:** Use secure environments and access controls
59
-
60
- **⚠️ SSL/TLS Verification**
61
- - SSL verification is automatically disabled in Rails development mode
62
- - **Production:** Application must ensure SSL verification is enabled
63
-
64
- **⚠️ Entity Statement Validation**
65
- - Fingerprint validation is optional - skipping validation may allow malicious entity statements
66
- - **Recommendation:** Always validate entity statement fingerprints when fetching from untrusted sources
67
-
68
- **⚠️ Key Rotation Window**
69
- - Brief window (up to cache TTL, default 24 hours) where rotated keys might not be immediately available
70
- - Library handles this with retry logic, but applications should monitor rotation events
71
-
72
- ## Input/Output Points
73
-
74
- ### Input Points (Library Handles)
75
-
76
- 1. **OAuth Callback Parameters** (`code`, `state`, `error`)
77
- - State validated with constant-time comparison
78
- - Authorization codes are single-use and time-limited
79
-
80
- 2. **OAuth Request Parameters** (`acr_values`, `login_hint`, etc.)
81
- - All parameters are signed in JWT request objects (RFC 9101)
82
- - Parameters validated before inclusion
83
-
84
- 3. **Entity Statement URLs**
85
- - Fetched via HTTP client
86
- - **SSRF Risk:** No validation against internal network access
87
-
88
- 4. **File Paths** (configuration)
89
- - Validated with `Utils.validate_file_path!` to prevent path traversal
90
- - Restricted to allowed directories
91
-
92
- 5. **JWT/JWE Tokens** (from OAuth provider)
93
- - Algorithm validation enforced
94
- - Signature validation required for signed tokens
95
- - Entity statement fingerprints provide additional validation
96
-
97
- ### Output Points (Library Exposes)
98
-
99
- 1. **HTTP Responses** (if using `RackEndpoint`)
100
- - `/.well-known/openid-federation` - Entity statement (JWT)
101
- - `/.well-known/jwks.json` - Public JWKS
102
- - `/.well-known/signed-jwks.json` - Signed JWKS (JWT)
103
- - Only public keys exposed (private keys never exposed)
104
-
105
- 2. **Logs**
106
- - Sensitive data (tokens, keys) never logged
107
- - File paths and URLs sanitized before logging
108
-
109
- 3. **Error Messages**
110
- - File paths sanitized in error messages
111
- - Generic error messages (stack traces only in development)
112
-
113
- ## Security Considerations
114
-
115
- - **Timing Attacks**: State parameter protected with constant-time comparison
116
- - **Path Traversal**: ✅ `Utils.validate_file_path!` prevents `..` sequences
117
- - **JWT Algorithm Confusion**: ✅ Algorithm validation enforced, `alg: none` rejected
118
- - **Replay Attacks**: Authorization codes are single-use and time-limited
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
119
23
 
120
24
  ## Automation Security
121
25
 
122
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.
123
27
 
124
28
  * **Supply Chain:** All automated dependencies must be verified.
125
-
126
- ## Contact
127
-
128
- **Security concerns**: contact@kiskolabs.com
129
- **General support**: https://github.com/amkisko/omniauth_openid_federation.rb/issues
@@ -2,6 +2,10 @@
2
2
  # Copy this to app/controllers/users/omniauth_callbacks_controller.rb
3
3
 
4
4
  class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
5
+ # Skip Rails CSRF protection for OAuth callbacks
6
+ # OAuth callbacks from external providers cannot include Rails CSRF tokens
7
+ # CSRF protection is handled by OAuth state parameter validation in the strategy
8
+ skip_before_action :verify_authenticity_token, only: [:openid_federation, :failure]
5
9
  skip_before_action :authenticate_user!, only: [:openid_federation, :failure]
6
10
 
7
11
  def openid_federation
@@ -72,6 +72,40 @@ OmniauthOpenidFederation::EndpointResolver.validate_and_build_audience(
72
72
  Devise.setup do |config|
73
73
  # ... your other Devise configuration ...
74
74
 
75
+ # OmniAuth 2.0+ defaults to POST only for CSRF protection (CVE-2015-9284)
76
+ # Always use POST for security - forms must include CSRF token
77
+ if defined?(OmniAuth)
78
+ OmniAuth.config.allowed_request_methods = [:post]
79
+ OmniAuth.config.silence_get_warning = false
80
+
81
+ # Configure CSRF validation to check tokens only for request phase (initiating OAuth)
82
+ # Callback phase uses OAuth state parameter for CSRF protection (validated in strategy)
83
+ # This ensures:
84
+ # - Request phase: Forms must include Rails CSRF tokens (standard Rails protection)
85
+ # - Callback phase: OAuth state parameter provides CSRF protection (external providers can't include Rails tokens)
86
+ OmniAuth.config.request_validation_phase = lambda do |env|
87
+ request = Rack::Request.new(env)
88
+ path = request.path
89
+
90
+ # Skip CSRF validation for callback paths (external providers can't include Rails CSRF tokens)
91
+ # OAuth state parameter provides CSRF protection for callbacks (validated in OpenIDFederation strategy)
92
+ return true if path.end_with?("/callback")
93
+
94
+ # For request phase, use Rails' standard CSRF token validation
95
+ # This ensures forms must include valid CSRF tokens when initiating OAuth
96
+ session = env["rack.session"] || {}
97
+ token = request.params["authenticity_token"] || request.get_header("X-CSRF-Token")
98
+ expected_token = session[:_csrf_token] || session["_csrf_token"]
99
+
100
+ # Validate CSRF token using constant-time comparison
101
+ if token.present? && expected_token.present?
102
+ ActiveSupport::SecurityUtils.secure_compare(token.to_s, expected_token.to_s)
103
+ else
104
+ false
105
+ end
106
+ end
107
+ end
108
+
75
109
  config.omniauth :openid_federation,
76
110
  name: :openid_federation,
77
111
  scope: [:openid],
@@ -0,0 +1,21 @@
1
+ # Rails Engine for OpenID Federation endpoints
2
+ # Provides controllers and routes for well-known OpenID Federation endpoints
3
+ #
4
+ # @see https://guides.rubyonrails.org/engines.html Rails Engines Guide
5
+ module OmniauthOpenidFederation
6
+ class Engine < ::Rails::Engine
7
+ # Don't isolate namespace because we need routes at specific well-known paths
8
+ # (/.well-known/openid-federation) rather than under a mount point
9
+ # isolate_namespace OmniauthOpenidFederation
10
+
11
+ # Explicitly require the controller to avoid Zeitwerk conflicts
12
+ # For local path gems, autoload_once_paths can cause conflicts with main app's loader
13
+ # We require the controller explicitly in to_prepare to ensure it's available for routing
14
+ config.to_prepare do
15
+ # Use self.class to access Engine class methods (root is a class method)
16
+ engine_root = OmniauthOpenidFederation::Engine.root
17
+ controller_path = engine_root.join("app", "controllers", "omniauth_openid_federation", "federation_controller.rb")
18
+ require controller_path.to_s if controller_path.exist?
19
+ end
20
+ end
21
+ end
@@ -115,6 +115,13 @@ module OmniauthOpenidFederation
115
115
  # Log security error but return nil to maintain backward compatibility
116
116
  Logger.warn("[EntityStatementReader] Security error: #{e.message}")
117
117
  nil
118
+ rescue Errno::EACCES, Errno::EISDIR, Errno::ENOENT
119
+ # Handle file system errors gracefully to avoid exposing file system structure
120
+ # EACCES: Permission denied
121
+ # EISDIR: Is a directory
122
+ # ENOENT: No such file or directory (race condition after File.exist?)
123
+ Logger.warn("[EntityStatementReader] File access error: #{Utils.sanitize_path(entity_statement_path)}")
124
+ nil
118
125
  end
119
126
  end
120
127
  end
@@ -393,8 +393,8 @@ module OmniauthOpenidFederation
393
393
  if unknown_claims.any?
394
394
  # For now, we'll log a warning but not reject
395
395
  # In a strict implementation, we should reject if we don't understand the claims
396
+ # Future enhancement: Add strict_mode option to reject unknown crit claims
396
397
  OmniauthOpenidFederation::Logger.warn("[EntityStatementValidator] Entity statement contains crit claim with unknown claims: #{unknown_claims.join(", ")}. These claims MUST be understood and processed.")
397
- # TODO: Make this configurable - strict mode should reject unknown crit claims
398
398
  end
399
399
  end
400
400
 
@@ -547,22 +547,21 @@ module OmniauthOpenidFederation
547
547
 
548
548
  # Mount the federation endpoint routes in Rails routes
549
549
  #
550
- # Add this to your config/routes.rb:
550
+ # RECOMMENDED: Use the Engine (Rails-idiomatic way):
551
551
  # Rails.application.routes.draw do
552
- # OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)
552
+ # mount OmniauthOpenidFederation::Engine => "/"
553
553
  # end
554
554
  #
555
- # This mounts all four endpoints:
555
+ # This mounts all four endpoints at the root level:
556
556
  # - GET /.well-known/openid-federation (entity statement)
557
557
  # - GET /.well-known/openid-federation/fetch (fetch endpoint for Subordinate Statements)
558
558
  # - GET /.well-known/jwks.json (standard JWKS)
559
559
  # - GET /.well-known/signed-jwks.json (signed JWKS)
560
560
  #
561
- # Or manually:
562
- # get "/.well-known/openid-federation", to: "omniauth_openid_federation/federation#show"
563
- # get "/.well-known/openid-federation/fetch", to: "omniauth_openid_federation/federation#fetch"
564
- # get "/.well-known/jwks.json", to: "omniauth_openid_federation/federation#jwks"
565
- # get "/.well-known/signed-jwks.json", to: "omniauth_openid_federation/federation#signed_jwks"
561
+ # ALTERNATIVE: Use mount_routes helper (for backward compatibility or custom paths):
562
+ # Rails.application.routes.draw do
563
+ # OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)
564
+ # end
566
565
  #
567
566
  # @param router [ActionDispatch::Routing::Mapper] The routes mapper (pass `self` from routes.rb)
568
567
  # @param entity_statement_path [String] Path for entity statement endpoint (default: "/.well-known/openid-federation")
@@ -570,6 +569,7 @@ module OmniauthOpenidFederation
570
569
  # @param jwks_path [String] Path for standard JWKS endpoint (default: "/.well-known/jwks.json")
571
570
  # @param signed_jwks_path [String] Path for signed JWKS endpoint (default: "/.well-known/signed-jwks.json")
572
571
  # @param as [String, Symbol] Route name prefix (default: :openid_federation)
572
+ # @deprecated Use `mount OmniauthOpenidFederation::Engine => "/"` instead (Rails-idiomatic way)
573
573
  def mount_routes(router, entity_statement_path: "/.well-known/openid-federation", fetch_path: "/.well-known/openid-federation/fetch", jwks_path: "/.well-known/jwks.json", signed_jwks_path: "/.well-known/signed-jwks.json", as: :openid_federation)
574
574
  # Controller uses Rails-conventional naming (OmniauthOpenidFederation)
575
575
  # which matches natural inflection from omniauth_openid_federation
@@ -46,6 +46,7 @@ module OmniauthOpenidFederation
46
46
  EVENT_ISSUER_MISMATCH = "issuer_mismatch"
47
47
  EVENT_EXPIRED_TOKEN = "expired_token"
48
48
  EVENT_INVALID_NONCE = "invalid_nonce"
49
+ EVENT_AUTHENTICITY_ERROR = "authenticity_error"
49
50
 
50
51
  class << self
51
52
  # Notify about a security event
@@ -348,6 +349,21 @@ module OmniauthOpenidFederation
348
349
  )
349
350
  end
350
351
 
352
+ # Notify about authenticity token error (OmniAuth CSRF protection)
353
+ #
354
+ # @param data [Hash] Additional context (error_type, error_message, phase, request_info)
355
+ # @return [void]
356
+ def notify_authenticity_error(data = {})
357
+ notify(
358
+ EVENT_AUTHENTICITY_ERROR,
359
+ data: {
360
+ reason: "OmniAuth authenticity token validation failed - CSRF protection blocked request",
361
+ **data
362
+ },
363
+ severity: :error
364
+ )
365
+ end
366
+
351
367
  private
352
368
 
353
369
  # Sanitize data to remove sensitive information
@@ -165,8 +165,9 @@ module OmniauthOpenidFederation
165
165
  # @return [Array<Hash>] Array with [payload, header]
166
166
  # @raise [ValidationError] If JWT validation fails
167
167
  # @raise [SignatureError] If signature verification fails
168
- # @deprecated Use jwt() method instead
168
+ # @deprecated Use jwt() method instead. This method will be removed in a future version.
169
169
  def self.json_jwt(encoded_jwt, jwks_uri, retried: false, entity_statement_keys: nil)
170
+ OmniauthOpenidFederation::Logger.warn("[Jwks::Decode] json_jwt is deprecated. Use jwt() method instead.")
170
171
  jwt(encoded_jwt, jwks_uri, retried: retried, entity_statement_keys: entity_statement_keys)
171
172
  end
172
173
  end
@@ -65,7 +65,7 @@ module OmniauthOpenidFederation
65
65
  attr_accessor :private_key, :state, :nonce
66
66
  # Provider-specific extension parameters (outside JWT)
67
67
  # Some providers may require additional parameters that are not part of the JWT
68
- # @deprecated Use provider_extension_params hash instead
68
+ # @deprecated Use request_object_params option in strategy instead (adds params to the JWT request object)
69
69
  attr_accessor :ftn_spname
70
70
 
71
71
  # Initialize JWT request object builder
@@ -256,12 +256,6 @@ module OmniauthOpenidFederation
256
256
  JWT.encode(claim, @private_key, "RS256", header)
257
257
  end
258
258
 
259
- def load_signing_key
260
- # Deprecated: Use KeyExtractor.extract_signing_key instead
261
- # This method is kept for backward compatibility but should not be used
262
- nil
263
- end
264
-
265
259
  def signing_key_kid
266
260
  metadata = load_metadata_from_entity_statement
267
261
  return nil unless metadata
@@ -1,26 +1,12 @@
1
- # Railtie to load rake tasks and provide Rails integration
1
+ # Railtie to load rake tasks
2
+ # Note: Controllers and routes are now handled by the Engine (lib/omniauth_openid_federation/engine.rb)
3
+ # This Railtie is kept for backward compatibility and for loading rake tasks
2
4
  if defined?(Rails)
3
5
  module OmniauthOpenidFederation
4
6
  class Railtie < Rails::Railtie
5
- # Add gem's controllers to autoload paths
6
- # This ensures the controller can be found by Rails routing
7
- initializer "omniauth_openid_federation.add_autoload_paths", before: :set_autoload_paths do |app|
8
- controllers_path = File.join(File.dirname(__FILE__), "..", "..", "app", "controllers")
9
- app.config.autoload_once_paths << controllers_path if File.exist?(controllers_path)
10
- end
11
-
12
- # Load controller when Rails is available (for development reloading)
13
- config.to_prepare do
14
- controller_path = File.join(File.dirname(__FILE__), "..", "..", "app", "controllers", "omniauth_openid_federation", "federation_controller.rb")
15
- require controller_path if File.exist?(controller_path)
16
- end
17
-
18
7
  rake_tasks do
19
8
  # Load rake tasks from lib/tasks
20
9
  # Rails automatically loads lib/tasks/**/*.rake, but we ensure they're loaded here too
21
- # File.dirname(__FILE__) = lib/omniauth_openid_federation
22
- # .. = lib
23
- # tasks = lib/tasks
24
10
  task_files = Dir[File.join(File.dirname(__FILE__), "..", "tasks", "**", "*.rake")]
25
11
  task_files.each { |task_file| load task_file } if task_files.any?
26
12
  end
@@ -186,6 +186,83 @@ module OmniAuth
186
186
  client
187
187
  end
188
188
 
189
+ # Override fail! to instrument all authentication failures
190
+ # This catches failures from OmniAuth middleware (like AuthenticityTokenProtection)
191
+ # as well as failures from within the strategy
192
+ #
193
+ # @param error_type [Symbol] Error type identifier
194
+ # @param exception [Exception] Exception object
195
+ # @return [void]
196
+ def fail!(error_type, exception = nil)
197
+ # Determine if this error has already been instrumented
198
+ # Errors instrumented before calling fail! will have a flag set
199
+ already_instrumented = env["omniauth_openid_federation.instrumented"] == true
200
+
201
+ unless already_instrumented
202
+ # Extract error information
203
+ error_message = exception&.message || error_type.to_s
204
+ error_class = exception&.class&.name || "UnknownError"
205
+
206
+ # Determine the phase (request or callback)
207
+ phase = request.path.end_with?("/callback") ? "callback_phase" : "request_phase"
208
+
209
+ # Build request info
210
+ request_info = {
211
+ remote_ip: request.env["REMOTE_ADDR"],
212
+ user_agent: request.env["HTTP_USER_AGENT"],
213
+ path: request.path,
214
+ method: request.request_method
215
+ }
216
+
217
+ # Instrument based on error type
218
+ case error_type.to_sym
219
+ when :authenticity_error
220
+ # OmniAuth CSRF protection error (from middleware)
221
+ OmniauthOpenidFederation::Instrumentation.notify_authenticity_error(
222
+ error_type: error_type.to_s,
223
+ error_message: error_message,
224
+ error_class: error_class,
225
+ phase: phase,
226
+ request_info: request_info
227
+ )
228
+ when :csrf_detected
229
+ # This should already be instrumented before calling fail!, but instrument here as fallback
230
+ # (e.g., if fail! is called directly without prior instrumentation)
231
+ OmniauthOpenidFederation::Instrumentation.notify_csrf_detected(
232
+ error_type: error_type.to_s,
233
+ error_message: error_message,
234
+ phase: phase,
235
+ request_info: request_info
236
+ )
237
+ when :missing_code, :token_exchange_error
238
+ # These should already be instrumented before calling fail!, but instrument here as fallback
239
+ # (e.g., if fail! is called directly without prior instrumentation)
240
+ OmniauthOpenidFederation::Instrumentation.notify_unexpected_authentication_break(
241
+ stage: phase,
242
+ error_message: error_message,
243
+ error_class: error_class,
244
+ error_type: error_type.to_s,
245
+ request_info: request_info
246
+ )
247
+ else
248
+ # Unknown error type - instrument as unexpected authentication break
249
+ OmniauthOpenidFederation::Instrumentation.notify_unexpected_authentication_break(
250
+ stage: phase,
251
+ error_message: error_message,
252
+ error_class: error_class,
253
+ error_type: error_type.to_s,
254
+ request_info: request_info
255
+ )
256
+ end
257
+ end
258
+
259
+ # Mark as instrumented to prevent double instrumentation
260
+ env["omniauth_openid_federation.instrumented"] = true
261
+
262
+ # Call parent fail! method
263
+ super
264
+ end
265
+
189
266
  # Override request_phase to use our custom authorize_uri instead of client.auth_code
190
267
  # The base OAuth2 strategy calls client.auth_code.authorize_url, but OpenIDConnect::Client
191
268
  # doesn't have an auth_code method - it uses authorization_uri directly
@@ -218,6 +295,8 @@ module OmniAuth
218
295
  path: request.path
219
296
  }
220
297
  )
298
+ # Mark as instrumented to prevent double instrumentation in fail!
299
+ env["omniauth_openid_federation.instrumented"] = true
221
300
  fail!(:csrf_detected, OmniauthOpenidFederation::SecurityError.new("CSRF detected"))
222
301
  return
223
302
  end
@@ -238,6 +317,8 @@ module OmniAuth
238
317
  path: request.path
239
318
  }
240
319
  )
320
+ # Mark as instrumented to prevent double instrumentation in fail!
321
+ env["omniauth_openid_federation.instrumented"] = true
241
322
  fail!(:missing_code, OmniauthOpenidFederation::ValidationError.new("Missing authorization code"))
242
323
  return
243
324
  end
@@ -258,6 +339,8 @@ module OmniAuth
258
339
  path: request.path
259
340
  }
260
341
  )
342
+ # Mark as instrumented to prevent double instrumentation in fail!
343
+ env["omniauth_openid_federation.instrumented"] = true
261
344
  fail!(:token_exchange_error, e)
262
345
  return
263
346
  end
@@ -419,13 +502,15 @@ module OmniAuth
419
502
 
420
503
  # Add provider-specific extension parameters if configured
421
504
  # Note: Some providers may require additional parameters outside the JWT
422
- # The deprecated ftn_spname option is supported for backward compatibility
505
+ # @deprecated ftn_spname option - Use request_object_params instead for adding params to the JWT request object
423
506
  if options.ftn_spname && !options.ftn_spname.to_s.empty?
507
+ OmniauthOpenidFederation::Logger.warn("[Strategy] ftn_spname option is deprecated. Use request_object_params: ['ftn_spname'] instead.")
424
508
  jws_builder.ftn_spname = options.ftn_spname
425
509
  end
426
510
 
427
- # Allow dynamic authorize params if configured
428
- options.allow_authorize_params&.each do |key|
511
+ # Allow dynamic request object params from HTTP request if configured
512
+ # These parameters are added as claims to the JWT request object (RFC 9101)
513
+ options.request_object_params&.each do |key|
429
514
  value = request_params[key.to_s]
430
515
  jws_builder.add_claim(key.to_sym, value) if value && !value.to_s.empty?
431
516
  end
@@ -470,7 +555,7 @@ module OmniAuth
470
555
 
471
556
  # Add provider-specific extension parameters outside JWT if configured
472
557
  # These are allowed per provider requirements (some providers require additional parameters)
473
- # The deprecated ftn_spname option is supported for backward compatibility
558
+ # @deprecated ftn_spname option - Use request_object_params instead for adding params to the JWT request object
474
559
  if options.ftn_spname && !options.ftn_spname.to_s.empty?
475
560
  query_params[:ftn_spname] = options.ftn_spname
476
561
  end
@@ -83,6 +83,8 @@ module OmniauthOpenidFederation
83
83
  resolved = File.expand_path(path_str)
84
84
 
85
85
  # Validate it's within allowed directories if specified
86
+ # When allowed_dirs is nil, we trust the developer to pass appropriate paths
87
+ # Path traversal protection (.. and ~) is still enforced above
86
88
  if allowed_dirs && !allowed_dirs.empty?
87
89
  allowed = allowed_dirs.any? do |dir|
88
90
  expanded_dir = File.expand_path(dir)
@@ -1,3 +1,3 @@
1
1
  module OmniauthOpenidFederation
2
- VERSION = "1.0.0".freeze
2
+ VERSION = "1.2.0".freeze
3
3
  end
@@ -82,8 +82,9 @@ module OmniauthOpenidFederation
82
82
  end
83
83
  end
84
84
 
85
- # Load Railtie for Rails integration (rake tasks, etc.)
85
+ # Load Engine for Rails integration (controllers, routes, etc.)
86
86
  if defined?(Rails)
87
+ require_relative "omniauth_openid_federation/engine"
87
88
  require_relative "omniauth_openid_federation/railtie"
88
89
  end
89
90
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth_openid_federation
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -83,16 +83,22 @@ dependencies:
83
83
  name: rack
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
- - - "~>"
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '2.0'
89
+ - - "<"
87
90
  - !ruby/object:Gem::Version
88
- version: '3.2'
91
+ version: '4'
89
92
  type: :runtime
90
93
  prerelease: false
91
94
  version_requirements: !ruby/object:Gem::Requirement
92
95
  requirements:
93
- - - "~>"
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '2.0'
99
+ - - "<"
94
100
  - !ruby/object:Gem::Version
95
- version: '3.2'
101
+ version: '4'
96
102
  - !ruby/object:Gem::Dependency
97
103
  name: rspec
98
104
  requirement: !ruby/object:Gem::Requirement
@@ -285,6 +291,7 @@ files:
285
291
  - lib/omniauth_openid_federation/configuration.rb
286
292
  - lib/omniauth_openid_federation/constants.rb
287
293
  - lib/omniauth_openid_federation/endpoint_resolver.rb
294
+ - lib/omniauth_openid_federation/engine.rb
288
295
  - lib/omniauth_openid_federation/entity_statement_reader.rb
289
296
  - lib/omniauth_openid_federation/errors.rb
290
297
  - lib/omniauth_openid_federation/federation/entity_statement.rb