omniauth_openid_federation 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +44 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +922 -0
  5. data/SECURITY.md +28 -0
  6. data/app/controllers/omniauth_openid_federation/federation_controller.rb +160 -0
  7. data/config/routes.rb +17 -0
  8. data/examples/README_INTEGRATION_TESTING.md +399 -0
  9. data/examples/README_MOCK_OP.md +243 -0
  10. data/examples/app/controllers/users/omniauth_callbacks_controller.rb.example +37 -0
  11. data/examples/app/jobs/jwks_rotation_job.rb.example +60 -0
  12. data/examples/app/models/user.rb.example +39 -0
  13. data/examples/config/initializers/devise.rb.example +131 -0
  14. data/examples/config/initializers/federation_endpoint.rb.example +206 -0
  15. data/examples/config/mock_op.yml.example +83 -0
  16. data/examples/config/open_id_connect_config.rb.example +210 -0
  17. data/examples/config/routes.rb.example +12 -0
  18. data/examples/db/migrate/add_omniauth_to_users.rb.example +16 -0
  19. data/examples/integration_test_flow.rb +1334 -0
  20. data/examples/jobs/README.md +194 -0
  21. data/examples/jobs/federation_cache_refresh_job.rb.example +78 -0
  22. data/examples/jobs/federation_files_generation_job.rb.example +87 -0
  23. data/examples/mock_op_server.rb +775 -0
  24. data/examples/mock_rp_server.rb +435 -0
  25. data/lib/omniauth_openid_federation/access_token.rb +504 -0
  26. data/lib/omniauth_openid_federation/cache.rb +39 -0
  27. data/lib/omniauth_openid_federation/cache_adapter.rb +173 -0
  28. data/lib/omniauth_openid_federation/configuration.rb +135 -0
  29. data/lib/omniauth_openid_federation/constants.rb +13 -0
  30. data/lib/omniauth_openid_federation/endpoint_resolver.rb +168 -0
  31. data/lib/omniauth_openid_federation/engine.rb +17 -0
  32. data/lib/omniauth_openid_federation/entity_statement_reader.rb +129 -0
  33. data/lib/omniauth_openid_federation/errors.rb +52 -0
  34. data/lib/omniauth_openid_federation/federation/entity_statement.rb +331 -0
  35. data/lib/omniauth_openid_federation/federation/entity_statement_builder.rb +188 -0
  36. data/lib/omniauth_openid_federation/federation/entity_statement_fetcher.rb +142 -0
  37. data/lib/omniauth_openid_federation/federation/entity_statement_helper.rb +87 -0
  38. data/lib/omniauth_openid_federation/federation/entity_statement_parser.rb +198 -0
  39. data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +502 -0
  40. data/lib/omniauth_openid_federation/federation/metadata_policy_merger.rb +276 -0
  41. data/lib/omniauth_openid_federation/federation/signed_jwks.rb +210 -0
  42. data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +225 -0
  43. data/lib/omniauth_openid_federation/federation_endpoint.rb +949 -0
  44. data/lib/omniauth_openid_federation/http_client.rb +70 -0
  45. data/lib/omniauth_openid_federation/instrumentation.rb +399 -0
  46. data/lib/omniauth_openid_federation/jwks/cache.rb +76 -0
  47. data/lib/omniauth_openid_federation/jwks/decode.rb +175 -0
  48. data/lib/omniauth_openid_federation/jwks/fetch.rb +153 -0
  49. data/lib/omniauth_openid_federation/jwks/normalizer.rb +49 -0
  50. data/lib/omniauth_openid_federation/jwks/rotate.rb +97 -0
  51. data/lib/omniauth_openid_federation/jwks/selector.rb +101 -0
  52. data/lib/omniauth_openid_federation/jws.rb +410 -0
  53. data/lib/omniauth_openid_federation/key_extractor.rb +173 -0
  54. data/lib/omniauth_openid_federation/logger.rb +99 -0
  55. data/lib/omniauth_openid_federation/rack_endpoint.rb +187 -0
  56. data/lib/omniauth_openid_federation/railtie.rb +15 -0
  57. data/lib/omniauth_openid_federation/rate_limiter.rb +55 -0
  58. data/lib/omniauth_openid_federation/strategy.rb +2114 -0
  59. data/lib/omniauth_openid_federation/string_helpers.rb +30 -0
  60. data/lib/omniauth_openid_federation/tasks_helper.rb +428 -0
  61. data/lib/omniauth_openid_federation/utils.rb +168 -0
  62. data/lib/omniauth_openid_federation/validators.rb +126 -0
  63. data/lib/omniauth_openid_federation/version.rb +3 -0
  64. data/lib/omniauth_openid_federation.rb +99 -0
  65. data/lib/tasks/omniauth_openid_federation.rake +376 -0
  66. data/sig/federation.rbs +218 -0
  67. data/sig/jwks.rbs +63 -0
  68. data/sig/omniauth_openid_federation.rbs +254 -0
  69. data/sig/strategy.rbs +60 -0
  70. metadata +361 -0
@@ -0,0 +1,243 @@
1
+ # Mock OpenID Provider (OP) Server
2
+
3
+ A standalone Rack/Sinatra application for testing OpenID Federation flows.
4
+
5
+ ## Features
6
+
7
+ - ✅ Entity Configuration endpoint (`/.well-known/openid-federation`)
8
+ - ✅ Fetch Endpoint (`/.well-known/openid-federation/fetch`) for Subordinate Statements
9
+ - ✅ Authorization Endpoint (`/auth`) with trust chain resolution
10
+ - ✅ Token Endpoint (`/token`) with ID Token signing
11
+ - ✅ JWKS endpoints (standard and signed)
12
+ - ✅ UserInfo endpoint (mock)
13
+
14
+ ## Quick Start
15
+
16
+ ### 1. Install Dependencies
17
+
18
+ ```bash
19
+ bundle install
20
+ ```
21
+
22
+ ### 2. Configure
23
+
24
+ Copy the example configuration:
25
+
26
+ ```bash
27
+ cp examples/config/mock_op.yml.example examples/config/mock_op.yml
28
+ ```
29
+
30
+ Edit `examples/config/mock_op.yml` with your settings:
31
+
32
+ ```yaml
33
+ entity_id: "https://op.example.com"
34
+ server_host: "localhost:9292"
35
+ signing_key: |
36
+ -----BEGIN RSA PRIVATE KEY-----
37
+ ...
38
+ -----END RSA PRIVATE KEY-----
39
+ trust_anchors:
40
+ - entity_id: "https://ta.example.com"
41
+ jwks:
42
+ keys: [...]
43
+ ```
44
+
45
+ ### 3. Generate Keys (if needed)
46
+
47
+ ```bash
48
+ # Generate OP signing key
49
+ openssl genrsa -out op-private-key.pem 2048
50
+ openssl rsa -in op-private-key.pem -pubout -out op-public-key.pem
51
+
52
+ # Extract JWKS from public key (or use the rake task)
53
+ rake openid_federation:prepare_client_keys
54
+ ```
55
+
56
+ ### 4. Run Server
57
+
58
+ ```bash
59
+ # Option 1: Direct Ruby execution
60
+ ruby examples/mock_op_server.rb
61
+
62
+ # Option 2: Using Rack
63
+ rackup examples/mock_op_server.ru
64
+
65
+ # Option 3: With specific port
66
+ rackup -p 9292 examples/mock_op_server.ru
67
+ ```
68
+
69
+ ## Configuration Options
70
+
71
+ ### Environment Variables
72
+
73
+ Instead of YAML, you can use environment variables:
74
+
75
+ ```bash
76
+ export OP_ENTITY_ID="https://op.example.com"
77
+ export OP_SERVER_HOST="localhost:9292"
78
+ export OP_SIGNING_KEY="$(cat op-private-key.pem)"
79
+ export OP_TRUST_ANCHORS='[{"entity_id":"https://ta.example.com","jwks":{"keys":[...]}}]'
80
+ export OP_AUTHORITY_HINTS="https://federation.example.com"
81
+ ```
82
+
83
+ ### YAML Configuration
84
+
85
+ See `examples/config/mock_op.yml.example` for full configuration options.
86
+
87
+ ## Testing Scenarios
88
+
89
+ ### 1. Direct Entity Statement (No Trust Chain)
90
+
91
+ ```bash
92
+ # Fetch OP's Entity Configuration
93
+ curl http://localhost:9292/.well-known/openid-federation
94
+
95
+ # Use in RP configuration
96
+ config.omniauth :openid_federation,
97
+ issuer: "https://op.example.com",
98
+ entity_statement_url: "http://localhost:9292/.well-known/openid-federation",
99
+ client_options: { ... }
100
+ ```
101
+
102
+ ### 2. Trust Chain Resolution
103
+
104
+ ```bash
105
+ # Configure trust anchors in mock_op.yml
106
+ trust_anchors:
107
+ - entity_id: "https://ta.example.com"
108
+ jwks: {...}
109
+
110
+ # RP with trust chain
111
+ # The OP will resolve the RP's trust chain automatically
112
+ curl "http://localhost:9292/auth?client_id=https://rp.example.com&redirect_uri=https://rp.example.com/callback"
113
+ ```
114
+
115
+ ### 3. Subordinate Statements
116
+
117
+ ```bash
118
+ # Configure subordinate statements in mock_op.yml
119
+ subordinate_statements:
120
+ "https://rp.example.com":
121
+ metadata: {...}
122
+ metadata_policy: {...}
123
+
124
+ # Fetch Subordinate Statement
125
+ curl "http://localhost:9292/.well-known/openid-federation/fetch?sub=https://rp.example.com"
126
+ ```
127
+
128
+ ## Endpoints
129
+
130
+ | Endpoint | Method | Description |
131
+ |----------|--------|-------------|
132
+ | `/.well-known/openid-federation` | GET | Entity Configuration (JWT) |
133
+ | `/.well-known/openid-federation/fetch` | GET | Fetch Subordinate Statement (requires `sub` parameter) |
134
+ | `/.well-known/jwks.json` | GET | Standard JWKS (JSON) |
135
+ | `/.well-known/signed-jwks.json` | GET | Signed JWKS (JWT) |
136
+ | `/auth` | GET | Authorization Endpoint (requires `client_id`, `redirect_uri`) |
137
+ | `/token` | POST | Token Endpoint (requires `code`, `grant_type=authorization_code`) |
138
+ | `/userinfo` | GET | UserInfo Endpoint (mock data) |
139
+ | `/` | GET | Health check and endpoint list |
140
+
141
+ ## Example Flow
142
+
143
+ ### 1. RP Discovers OP
144
+
145
+ ```bash
146
+ # RP fetches OP's Entity Configuration
147
+ curl http://localhost:9292/.well-known/openid-federation
148
+ ```
149
+
150
+ ### 2. RP Initiates Authentication
151
+
152
+ ```bash
153
+ # RP redirects user to authorization endpoint
154
+ # client_id is the RP's Entity ID (for automatic registration)
155
+ curl "http://localhost:9292/auth?client_id=https://rp.example.com&redirect_uri=https://rp.example.com/callback&state=xyz&nonce=abc"
156
+ ```
157
+
158
+ ### 3. OP Resolves RP's Trust Chain
159
+
160
+ The OP automatically:
161
+ - Resolves RP's trust chain using `TrustChainResolver`
162
+ - Merges metadata policies using `MetadataPolicyMerger`
163
+ - Validates RP's effective metadata
164
+ - Redirects back to RP with authorization code
165
+
166
+ ### 4. RP Exchanges Code for Tokens
167
+
168
+ ```bash
169
+ curl -X POST http://localhost:9292/token \
170
+ -d "grant_type=authorization_code" \
171
+ -d "code=<authorization_code>" \
172
+ -d "redirect_uri=https://rp.example.com/callback" \
173
+ -d "client_id=https://rp.example.com"
174
+ ```
175
+
176
+ ### 5. RP Validates ID Token
177
+
178
+ The RP validates the ID Token using the OP's JWKS from the effective metadata.
179
+
180
+ ## Integration with Real RP
181
+
182
+ To test with a real RP application:
183
+
184
+ ```ruby
185
+ # In RP's config/initializers/devise.rb
186
+ config.omniauth :openid_federation,
187
+ issuer: "http://localhost:9292",
188
+ entity_statement_url: "http://localhost:9292/.well-known/openid-federation",
189
+ trust_anchors: [
190
+ {
191
+ entity_id: "https://ta.example.com",
192
+ jwks: trust_anchor_jwks
193
+ }
194
+ ],
195
+ client_options: {
196
+ identifier: "https://rp.example.com", # RP's Entity ID
197
+ redirect_uri: "http://localhost:3000/users/auth/openid_federation/callback",
198
+ private_key: rp_private_key
199
+ }
200
+ ```
201
+
202
+ ## Limitations
203
+
204
+ This is a **mock server for testing only**:
205
+
206
+ - ⚠️ No real user authentication (always returns mock user)
207
+ - ⚠️ Authorization codes stored in memory (lost on restart)
208
+ - ⚠️ No database persistence
209
+ - ⚠️ No production security hardening
210
+ - ⚠️ ID Tokens contain mock user data
211
+
212
+ ## Production Considerations
213
+
214
+ For production use, you would need:
215
+
216
+ - Real user authentication system
217
+ - Database for authorization codes and tokens
218
+ - Proper session management
219
+ - Security hardening (rate limiting, CSRF protection, etc.)
220
+ - Real user data in ID Tokens
221
+ - Proper error handling and logging
222
+
223
+ ## Troubleshooting
224
+
225
+ **"Federation endpoint not configured"**
226
+ - Ensure `signing_key` is provided in config
227
+ - Check that `entity_id` is set
228
+
229
+ **"Trust chain resolution failed"**
230
+ - Verify `trust_anchors` are correctly configured
231
+ - Ensure trust anchor JWKS are valid
232
+ - Check that RP's Entity ID is resolvable
233
+
234
+ **"Subordinate Statement not found"**
235
+ - Configure `subordinate_statements` in `mock_op.yml`
236
+ - Ensure subject Entity ID matches exactly
237
+
238
+ ## See Also
239
+
240
+ - [OpenID Federation 1.0 Specification](https://openid.net/specs/openid-federation-1_0.html)
241
+ - [Main README](../README.md)
242
+ - [Federation Endpoint Documentation](../README.md#publishing-federation-endpoint)
243
+
@@ -0,0 +1,37 @@
1
+ # Example OmniAuth callback controller for Devise
2
+ # Copy this to app/controllers/users/omniauth_callbacks_controller.rb
3
+
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]
9
+ skip_before_action :authenticate_user!, only: [:openid_federation, :failure]
10
+
11
+ def openid_federation
12
+ auth = request.env["omniauth.auth"]
13
+
14
+ @user = User.find_or_create_from_omniauth(auth)
15
+
16
+ if @user&.persisted?
17
+ sign_in_and_redirect @user, event: :authentication
18
+ else
19
+ redirect_to root_path, alert: "Authentication failed"
20
+ end
21
+ end
22
+
23
+ def failure
24
+ error_type = request.env["omniauth.error.type"] || :unknown
25
+ error_message = request.env["omniauth.error"]&.message || "Authentication failed"
26
+
27
+ Rails.logger.error({
28
+ message: "OmniAuth authentication failure",
29
+ error_type: error_type,
30
+ error_message: error_message,
31
+ strategy: request.env["omniauth.strategy"]&.name
32
+ })
33
+
34
+ redirect_to root_path, alert: "Authentication failed: #{error_type}"
35
+ end
36
+ end
37
+
@@ -0,0 +1,60 @@
1
+ # Background job for proactive JWKS key rotation
2
+ # This job runs periodically to refresh JWKS cache before expiration,
3
+ # ensuring keys are always up-to-date without blocking client requests.
4
+ #
5
+ # Usage:
6
+ # 1. Schedule this job to run periodically (e.g., every 12 hours)
7
+ # 2. Configure cache TTL to be longer than job frequency (e.g., 24 hours)
8
+ # 3. This ensures keys are refreshed proactively, not reactively
9
+ #
10
+ # Example scheduling (using GoodJob, Sidekiq, or similar):
11
+ #
12
+ # # config/initializers/schedule.rb (for GoodJob)
13
+ # GoodJob::Cron::Schedule.add("jwks_rotation", {
14
+ # cron: "0 */12 * * *", # Every 12 hours
15
+ # class: "JwksRotationJob"
16
+ # })
17
+ #
18
+ # # Or with Sidekiq-Cron
19
+ # Sidekiq::Cron::Job.create(
20
+ # name: "JWKS Rotation",
21
+ # cron: "0 */12 * * *",
22
+ # class: "JwksRotationJob"
23
+ # )
24
+ class JwksRotationJob < ApplicationJob
25
+ queue_as :default
26
+
27
+ # Rotate JWKS for a specific provider
28
+ #
29
+ # @param jwks_uri [String] The JWKS URI to refresh
30
+ # @param entity_statement_path [String, nil] Path to entity statement for signed JWKS
31
+ def perform(jwks_uri, entity_statement_path: nil)
32
+ OmniauthOpenidFederation.rotate_jwks(jwks_uri, entity_statement_path: entity_statement_path)
33
+ rescue => e
34
+ # Log error but don't fail - request-level rotation will handle it
35
+ Rails.logger.error("[JwksRotationJob] Failed to rotate JWKS for #{jwks_uri}: #{e.class} - #{e.message}")
36
+ # Optionally, send to error tracking service (Sentry, Rollbar, etc.)
37
+ # Sentry.capture_exception(e) if defined?(Sentry)
38
+ raise # Re-raise to allow job retry if configured
39
+ end
40
+
41
+ # Rotate JWKS for all configured providers
42
+ # This is useful when you have multiple providers configured
43
+ def self.rotate_all
44
+ # Example: Get all providers from Devise config
45
+ providers = Devise.omniauth_configs.keys.select { |k| k.to_s.start_with?("openid") }
46
+
47
+ providers.each do |provider_name|
48
+ config = Devise.omniauth_configs[provider_name]
49
+ options = config.options
50
+ client_options = options[:client_options] || options["client_options"] || {}
51
+ jwks_uri = client_options[:jwks_uri] || client_options["jwks_uri"]
52
+ entity_statement_path = options[:entity_statement_path] || options["entity_statement_path"]
53
+
54
+ if jwks_uri
55
+ perform_later(jwks_uri, entity_statement_path: entity_statement_path)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
@@ -0,0 +1,39 @@
1
+ # Example User model with OmniAuth integration
2
+ # Add these methods to your existing User model
3
+
4
+ class User < ApplicationRecord
5
+ # Required columns (add via migration):
6
+ # - provider (string)
7
+ # - uid (string)
8
+ # - email (string)
9
+ # - name (string)
10
+ # - first_name (string, optional)
11
+ # - last_name (string, optional)
12
+
13
+ def self.find_or_create_from_omniauth(auth)
14
+ user = find_by(provider: auth.provider, uid: auth.uid)
15
+
16
+ if user
17
+ # Update existing user info
18
+ user.update(
19
+ email: auth.info.email,
20
+ name: auth.info.name,
21
+ first_name: auth.info.first_name,
22
+ last_name: auth.info.last_name
23
+ )
24
+ else
25
+ # Create new user
26
+ user = create(
27
+ provider: auth.provider,
28
+ uid: auth.uid,
29
+ email: auth.info.email,
30
+ name: auth.info.name,
31
+ first_name: auth.info.first_name,
32
+ last_name: auth.info.last_name
33
+ )
34
+ end
35
+
36
+ user
37
+ end
38
+ end
39
+
@@ -0,0 +1,131 @@
1
+ # Example Devise configuration for OmniAuth OpenID Federation
2
+ # Copy this to config/initializers/devise.rb and customize for your provider
3
+
4
+ require "omniauth_openid_federation"
5
+
6
+ # Configure global settings (optional but recommended)
7
+ OmniauthOpenidFederation.configure do |config|
8
+ # Security instrumentation - get notified about security events, MITM attacks, etc.
9
+ # Example with Sentry:
10
+ # config.instrumentation = ->(event, data) do
11
+ # Sentry.capture_message(
12
+ # "OpenID Federation: #{event}",
13
+ # level: data[:severity] == :error ? :error : :warning,
14
+ # extra: data
15
+ # )
16
+ # end
17
+
18
+ # Example with Honeybadger:
19
+ # config.instrumentation = ->(event, data) do
20
+ # Honeybadger.notify("OpenID Federation: #{event}", context: data)
21
+ # end
22
+
23
+ # Example with custom logger:
24
+ # config.instrumentation = ->(event, data) do
25
+ # Rails.logger.warn("[Security] #{event}: #{data.inspect}")
26
+ # end
27
+
28
+ # Cache configuration (optional)
29
+ # config.cache_ttl = 3600 # Refresh provider keys every hour
30
+ # config.rotate_on_errors = true # Auto-handle provider key rotation
31
+ end
32
+
33
+ # Provider configuration
34
+ provider_issuer = ENV["OPENID_PROVIDER_ISSUER"] || "https://provider.example.com"
35
+ client_id = ENV["OPENID_CLIENT_ID"] || "your-client-id"
36
+ redirect_uri = "#{ENV["APP_URL"] || "https://your-app.com"}/users/auth/openid_federation/callback"
37
+
38
+ # File paths
39
+ private_key_path = Rails.root.join("config", "client-private-key.pem")
40
+ entity_statement_path = Rails.root.join("config", "provider-entity-statement.jwt")
41
+
42
+ # Load private key
43
+ unless File.exist?(private_key_path)
44
+ raise "Private key not found at #{private_key_path}. Generate it first using the example in README."
45
+ end
46
+ private_key = OpenSSL::PKey::RSA.new(File.read(private_key_path))
47
+
48
+ # Resolve endpoints from entity statement or manual configuration
49
+ endpoints = if File.exist?(entity_statement_path)
50
+ # Use entity statement if available (recommended for OpenID Federation)
51
+ OmniauthOpenidFederation::EndpointResolver.resolve(
52
+ entity_statement_path: entity_statement_path.to_s,
53
+ config: {}
54
+ )
55
+ else
56
+ # Fallback to manual configuration
57
+ {
58
+ authorization_endpoint: ENV["OPENID_AUTHORIZATION_ENDPOINT"] || "/oauth2/authorize",
59
+ token_endpoint: ENV["OPENID_TOKEN_ENDPOINT"] || "/oauth2/token",
60
+ userinfo_endpoint: ENV["OPENID_USERINFO_ENDPOINT"] || "/oauth2/userinfo",
61
+ jwks_uri: ENV["OPENID_JWKS_URI"] || "/.well-known/jwks.json",
62
+ audience: provider_issuer
63
+ }
64
+ end
65
+
66
+ # Validate endpoints
67
+ OmniauthOpenidFederation::EndpointResolver.validate_and_build_audience(
68
+ endpoints,
69
+ issuer_uri: URI.parse(provider_issuer)
70
+ )
71
+
72
+ Devise.setup do |config|
73
+ # ... your other Devise configuration ...
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
+
109
+ config.omniauth :openid_federation,
110
+ name: :openid_federation,
111
+ scope: [:openid],
112
+ response_type: "code",
113
+ discovery: true,
114
+ issuer: provider_issuer,
115
+ client_auth_method: :jwt_bearer,
116
+ client_signing_alg: :RS256,
117
+ audience: endpoints[:audience],
118
+ entity_statement_path: entity_statement_path.to_s,
119
+ client_options: {
120
+ identifier: client_id,
121
+ redirect_uri: redirect_uri,
122
+ private_key: private_key,
123
+ scheme: URI.parse(provider_issuer).scheme,
124
+ host: URI.parse(provider_issuer).host,
125
+ authorization_endpoint: endpoints[:authorization_endpoint],
126
+ token_endpoint: endpoints[:token_endpoint],
127
+ userinfo_endpoint: endpoints[:userinfo_endpoint],
128
+ jwks_uri: endpoints[:jwks_uri]
129
+ }
130
+ end
131
+