omniauth_openid_federation 1.0.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff53053ffcbf2f63f91ae342158b5f11e881f3466c8d1ec1a25c06c8d1ff6b9b
4
- data.tar.gz: d11bdc15e0dbbbd710cee19ac670a7425af0f3e37e42765c93df9d4dfac224ac
3
+ metadata.gz: d1df7531d0ec2cac4095580f068c1dc7cb09fe53631d87a647b311c05ee9c89f
4
+ data.tar.gz: 38f06cb5ca370edd986051c4f547b0e53d07d1a61c685cb727c6f2d0e2b34c46
5
5
  SHA512:
6
- metadata.gz: '082caf05aaf09898c6bd992f9280570dec07ad19acedf59dcfbf8c60e74af84f03e9f140eab5d4fe4156e97d418bf5c77d623026923d4f748889cc18e555101d'
7
- data.tar.gz: 27db7564021a934505269f60bb08eb1eba84eb763b9ea5fd6c06815e4e92cd72cbefed49779d15b9bb872c729525ec3ae3760204ce1948673e708a3ae0e1f655
6
+ metadata.gz: 81261964b6f0fd468dde2e70b0331e2435129896114b7d4cd087a366596906f160fac90296f856c9055d15e49e51cd8dd8a5ad83d8234b2bec264c49b625b789
7
+ data.tar.gz: a9d48264edb247c18ce0160fa339f429c3802033e902da6b7c1ef12de5892c44c392415c8ffd57ac6b976190d85a071a199ad3790cc97f0cfe1d4c164d35acba
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.1.0 (2025-11-26)
4
+
5
+ - Enhanced instrumentation: All blocking exceptions automatically reported through instrumentation system, including OmniAuth middleware errors (like AuthenticityTokenProtection)
6
+ - CSRF protection instrumentation: New authenticity_error event type for reporting OmniAuth CSRF protection failures
7
+ - Comprehensive error reporting: Override fail! method in strategy to catch and instrument all authentication failures
8
+ - CSRF protection documentation: Added comprehensive Step 7 in README explaining CSRF protection configuration for both request and callback phases
9
+ - CSRF configuration examples: Added complete examples in examples/config/initializers/devise.rb.example and examples/app/controllers/users/omniauth_callbacks_controller.rb.example
10
+ - Deprecation warnings: Added runtime deprecation warnings for json_jwt method and ftn_spname option to guide users to recommended alternatives
11
+ - Code cleanup: Removed deprecated load_signing_key method (unused, returned nil)
12
+ - Updated deprecation notices: Fixed deprecation notices to reference correct replacement methods (request_object_params instead of non-existent provider_extension_params)
13
+ - Renamed option: `allow_authorize_params` → `request_object_params` for clarity (uses RFC 9101 terminology, clearly indicates params go into JWT request object)
14
+
3
15
  ## 1.0.0 (2025-11-26)
4
16
 
5
17
  - 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
@@ -207,13 +209,65 @@ Rails.application.routes.draw do
207
209
  end
208
210
  ```
209
211
 
210
- ### Step 7: Create Callback Controller
212
+ ### Step 7: Configure CSRF Protection
213
+
214
+ OmniAuth requires CSRF protection configuration to handle both the request phase (initiating OAuth) and callback phase (external provider redirect).
215
+
216
+ **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).
217
+
218
+ #### For Devise (Rails)
219
+
220
+ ```ruby
221
+ # config/initializers/devise.rb
222
+ if defined?(OmniAuth)
223
+ OmniAuth.config.allowed_request_methods = [:post]
224
+ OmniAuth.config.silence_get_warning = false
225
+
226
+ # Configure CSRF validation to check tokens only for request phase (initiating OAuth)
227
+ # Callback phase uses OAuth state parameter for CSRF protection (validated in strategy)
228
+ # This ensures:
229
+ # - Request phase: Forms must include Rails CSRF tokens (standard Rails protection)
230
+ # - Callback phase: OAuth state parameter provides CSRF protection (external providers can't include Rails tokens)
231
+ OmniAuth.config.request_validation_phase = lambda do |env|
232
+ request = Rack::Request.new(env)
233
+ path = request.path
234
+
235
+ # Skip CSRF validation for callback paths (external providers can't include Rails CSRF tokens)
236
+ # OAuth state parameter provides CSRF protection for callbacks (validated in OpenIDFederation strategy)
237
+ return true if path.end_with?("/callback")
238
+
239
+ # For request phase, use Rails' standard CSRF token validation
240
+ # This ensures forms must include valid CSRF tokens when initiating OAuth
241
+ session = env["rack.session"] || {}
242
+ token = request.params["authenticity_token"] || request.get_header("X-CSRF-Token")
243
+ expected_token = session[:_csrf_token] || session["_csrf_token"]
244
+
245
+ # Validate CSRF token using constant-time comparison
246
+ if token.present? && expected_token.present?
247
+ ActiveSupport::SecurityUtils.secure_compare(token.to_s, expected_token.to_s)
248
+ else
249
+ false
250
+ end
251
+ end
252
+ end
253
+ ```
254
+
255
+ **Security Notes**:
256
+ - **Request phase** (initiating OAuth): Forms must include Rails CSRF tokens via `button_to` or `form_with` helpers
257
+ - **Callback phase** (external provider redirect): OAuth `state` parameter provides CSRF protection (automatically validated in `OpenIDFederation` strategy using constant-time comparison)
258
+ - Both layers provide equivalent security - Rails CSRF tokens for request phase, OAuth state parameter for callbacks
259
+
260
+ ### Step 8: Create Callback Controller
211
261
 
212
262
  #### For Devise
213
263
 
214
264
  ```ruby
215
265
  # app/controllers/users/omniauth_callbacks_controller.rb
216
266
  class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
267
+ # Skip Rails CSRF protection for OAuth callbacks
268
+ # OAuth callbacks from external providers cannot include Rails CSRF tokens
269
+ # CSRF protection is handled by OAuth state parameter validation in the strategy
270
+ skip_before_action :verify_authenticity_token, only: [:openid_federation, :failure]
217
271
  skip_before_action :authenticate_user!, only: [:openid_federation, :failure]
218
272
 
219
273
  def openid_federation
@@ -233,7 +287,9 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
233
287
  end
234
288
  ```
235
289
 
236
- ### Step 8: Create User Model Method
290
+ **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).
291
+
292
+ ### Step 9: Create User Model Method
237
293
 
238
294
  ```ruby
239
295
  # app/models/user.rb
@@ -363,7 +419,8 @@ end
363
419
  ```
364
420
 
365
421
  **Instrumented Events**:
366
- - `csrf_detected` - CSRF attack detected (state mismatch)
422
+ - `csrf_detected` - CSRF attack detected (state mismatch in callback phase)
423
+ - `authenticity_error` - OmniAuth CSRF protection blocked request (Rails CSRF token validation failed in request phase)
367
424
  - `signature_verification_failed` - JWT signature verification failed (possible MITM)
368
425
  - `decryption_failed` - Token decryption failed (possible MITM or key mismatch)
369
426
  - `token_validation_failed` - Token validation failed (possible tampering)
@@ -372,9 +429,14 @@ end
372
429
  - `entity_statement_validation_failed` - Entity statement validation failed (possible MITM)
373
430
  - `fingerprint_mismatch` - Entity statement fingerprint mismatch (possible MITM)
374
431
  - `trust_chain_validation_failed` - Trust chain validation failed
375
- - `unexpected_authentication_break` - Unexpected authentication failure
432
+ - `unexpected_authentication_break` - Unexpected authentication failure (missing code, token exchange errors, unknown errors)
376
433
  - `missing_required_claims` - Token missing required claims
377
434
 
435
+ **Note**: All blocking exceptions are automatically reported through instrumentation, including:
436
+ - OmniAuth middleware errors (like `AuthenticityTokenProtection` blocking requests)
437
+ - Strategy-level errors (CSRF detected, missing code, token exchange failures)
438
+ - Unknown error types (reported as `unexpected_authentication_break`)
439
+
378
440
  **Security Note**: All sensitive data (tokens, keys, fingerprints) is automatically sanitized before being sent to your instrumentation callback.
379
441
 
380
442
  **Key Rotation Types**:
@@ -742,6 +804,13 @@ See inline code documentation for complete API reference.
742
804
  - Provider may have rotated keys (auto-handled with `rotate_on_errors: true`)
743
805
  - Clear cache: `Rails.cache.delete_matched("openid_federation_jwks_*")`
744
806
 
807
+ **"Attack prevented by OmniAuth::AuthenticityTokenProtection" or "OmniAuth::AuthenticityError"**
808
+ - **Request phase (initiating OAuth)**: Ensure forms include Rails CSRF tokens using `button_to` or `form_with` helpers
809
+ - **Callback phase (external provider redirect)**: Ensure CSRF protection is configured correctly (see [Step 7: Configure CSRF Protection](#step-7-configure-csrf-protection))
810
+ - Verify `OmniAuth.config.request_validation_phase` is configured to skip CSRF validation for callback paths
811
+ - Ensure `skip_before_action :verify_authenticity_token` is present in the callback controller for callback actions
812
+ - Check that OAuth state parameter validation is working (handled automatically by the strategy)
813
+
745
814
  ## Security
746
815
 
747
816
  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],
@@ -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
 
@@ -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
@@ -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.1.0".freeze
3
3
  end
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.1.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