omniauth_openid_federation 1.2.2 → 1.3.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.
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?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)
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/graph/badge.svg)
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
 
@@ -10,7 +10,6 @@ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
10
10
  <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
11
11
  </a>
12
12
 
13
-
14
13
  ## Installation
15
14
 
16
15
  ```ruby
@@ -24,31 +23,25 @@ bundle install
24
23
 
25
24
  ## Features
26
25
 
27
- - ✅ **Signed Request Objects (RFC 9101)** - RS256 signing of authorization requests (per OpenID Federation spec: "MUST be signed")
28
- - ✅ **Optional Request Object Encryption** - Optional RSA-OAEP encryption when provider requires it (per spec: "MAY be encrypted")
26
+ - ✅ **Signed Request Objects (RFC 9101)** - RS256 signing of authorization requests
27
+ - ✅ **Optional Request Object Encryption** - RSA-OAEP encryption when provider requires it
29
28
  - ✅ **ID Token Encryption/Decryption** - RSA-OAEP encryption and A128CBC-HS256 decryption
30
29
  - ✅ **OpenID Federation 1.0** - Full entity statement support and federation metadata
31
30
  - ✅ **Federation Endpoint** - Publish entity statements at `/.well-known/openid-federation`
32
- - ✅ **Automatic Key Provisioning** - Automatic extraction/generation of signing and encryption keys with caching support
31
+ - ✅ **Automatic Key Provisioning** - Automatic extraction/generation of signing and encryption keys
33
32
  - ✅ **Separate Key Support** - Production-ready support for separate signing and encryption keys
34
- - ✅ **Entity Type Support** - Full support for both `openid_relying_party` (RP) and `openid_provider` (OP) entity types
35
- - ✅ **Signed JWKS Support** - Automatic validation for key rotation compliance
36
- - ✅ **Automatic Provider Key Rotation** - Handles external provider key rotation automatically via Signed JWKS (client key rotation is manual)
37
33
  - ✅ **Client Assertion (private_key_jwt)** - Secure client authentication
38
- - ✅ **Security Hardened** - OWASP compliant, rate limiting, path traversal protection
39
- - ✅ **Production Ready** - Thread-safe, comprehensive error handling
34
+ - ✅ **Security Hardened** - OWASP compliant, input validation, rate limiting
40
35
 
41
36
  ## Quick Start
42
37
 
43
- The library relies on **URLs and fingerprint verification** for security. Always fetch entity statements from provider URLs - local files are cached copies for configuration use. Everything is automated via discovery.
44
-
45
38
  ### Step 1: Get Provider Information
46
39
 
47
40
  Your provider will provide:
48
41
  - **Entity statement URL**: `https://provider.example.com/.well-known/openid-federation`
49
- - **Expected fingerprint hash**: For verification (security guard)
42
+ - **Expected fingerprint hash**: For verification
50
43
 
51
- **Always use URLs**: Fetch and cache the entity statement locally using the URL and fingerprint:
44
+ Fetch and cache the entity statement:
52
45
 
53
46
  ```bash
54
47
  rake openid_federation:fetch_entity_statement[
@@ -58,13 +51,9 @@ rake openid_federation:fetch_entity_statement[
58
51
  ]
59
52
  ```
60
53
 
61
- This fetches from the URL, verifies the fingerprint, and stores locally. The local file is a cached copy of the URL - always use the URL as the source of truth.
62
-
63
54
  ### Step 2: Generate Client Keys
64
55
 
65
- Generate RSA key pair for client authentication:
66
-
67
- ```bash
56
+ ```bash
68
57
  rake openid_federation:prepare_client_keys
69
58
  ```
70
59
 
@@ -72,9 +61,13 @@ This generates:
72
61
  - Private key: `config/client-private-key.pem` (keep secure, never commit)
73
62
  - Public JWKS: `config/client-jwks.json` (send to provider for explicit registration)
74
63
 
75
- **Security**: Never commit private keys. Add to `.gitignore`:
64
+ **Security Warning**:
65
+ - **NEVER commit production private keys to your repository**
66
+ - For production: Use environment variables (`OPENID_CLIENT_PRIVATE_KEY_BASE64`) or secure key management systems
67
+ - For development: Add private key files to `.gitignore`:
76
68
  ```
77
- config/*-private-key.pem
69
+ .federation*
70
+ *.pem
78
71
  ```
79
72
 
80
73
  ### Step 3: Register Client
@@ -85,215 +78,137 @@ config/*-private-key.pem
85
78
 
86
79
  **Automatic Registration** (if provider supports it):
87
80
  - No pre-registration needed
88
- - Client entity statement is auto-generated via `FederationEndpoint` (see Step 5)
89
81
  - Set `client_entity_statement_url` to `https://your-app.com/.well-known/openid-federation`
90
82
 
91
83
  ### Step 4: Configure OmniAuth Strategy
92
84
 
93
- #### For Devise (Rails)
94
-
95
85
  ```ruby
96
86
  # config/initializers/devise.rb
97
87
  require "omniauth_openid_federation"
98
88
 
99
- private_key = OpenSSL::PKey::RSA.new(File.read("config/client-private-key.pem"))
100
-
101
- # Always provide the entity statement URL
102
- entity_statement_url = "https://provider.example.com/.well-known/openid-federation"
103
- entity_statement_fingerprint = "expected-fingerprint-hash"
104
-
105
- # Fetch and cache entity statement from URL (run this via rake task or in initializer)
106
- # rake openid_federation:fetch_entity_statement[entity_statement_url, entity_statement_fingerprint, "config/provider-entity-statement.jwt"]
107
-
108
- config.omniauth :openid_federation,
109
- discovery: true, # Enables automatic endpoint discovery
110
- # Option 1: Provide URL (recommended - library fetches and caches automatically)
111
- entity_statement_url: entity_statement_url, # Always provide URL as source of truth
112
- entity_statement_fingerprint: entity_statement_fingerprint, # Fingerprint for verification
113
- # Option 2: Provide issuer (library builds URL from issuer + /.well-known/openid-federation)
114
- # issuer: "https://provider.example.com",
115
- # Option 3: Provide cached path (optional - for offline development)
116
- # entity_statement_path: "config/provider-entity-statement.jwt", # Cached copy from URL
117
- client_options: {
118
- identifier: ENV["OPENID_CLIENT_ID"],
119
- redirect_uri: "#{ENV["APP_URL"]}/users/auth/openid_federation/callback",
120
- private_key: private_key
121
- }
122
- ```
123
-
124
- **Key Points**:
125
- - `entity_statement_url` is recommended - library automatically fetches and caches
126
- - `entity_statement_fingerprint` is used for verification when fetching from URL
127
- - `issuer` can be used instead - library builds URL from issuer + `/.well-known/openid-federation`
128
- - `entity_statement_path` is optional - only for offline development (cached copy)
129
- - `discovery: true` automatically discovers all endpoints from entity statement
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.
89
+ # Global settings (optional)
90
+ OmniauthOpenidFederation.configure do |config|
91
+ config.cache_ttl = 24 * 60 * 60
92
+ config.rotate_on_errors = true
93
+ config.http_timeout = 10
94
+ config.max_retries = 3
95
+ end
132
96
 
133
- #### For OmniAuth (non-Rails)
97
+ if ENV["OPENID_ENABLED"] == "true"
98
+ # Load private key from environment variable (recommended for production)
99
+ private_key = if ENV["OPENID_CLIENT_PRIVATE_KEY_BASE64"]
100
+ OpenSSL::PKey::RSA.new(Base64.decode64(ENV["OPENID_CLIENT_PRIVATE_KEY_BASE64"]))
101
+ elsif ENV["OPENID_CLIENT_PRIVATE_KEY_PATH"]
102
+ OpenSSL::PKey::RSA.new(File.read(Rails.root.join(ENV["OPENID_CLIENT_PRIVATE_KEY_PATH"])))
103
+ else
104
+ OpenSSL::PKey::RSA.new(File.read(Rails.root.join("config", "client-private-key.pem")))
105
+ end
134
106
 
135
- ```ruby
136
- # config/initializers/omniauth.rb
137
- require "omniauth_openid_federation"
107
+ entity_statement_path = ENV["OPENID_ENTITY_STATEMENT_PATH"] ||
108
+ Rails.root.join("config", ".federation-entity-statement.jwt").to_s
109
+
110
+ # Configure CSRF protection
111
+ if defined?(OmniAuth)
112
+ OmniAuth.config.allowed_request_methods = [:post]
113
+ OmniAuth.config.request_validation_phase = lambda do |env|
114
+ request = Rack::Request.new(env)
115
+ return true if request.path.end_with?("/callback")
116
+
117
+ session = env["rack.session"] || {}
118
+ token = request.params["authenticity_token"] || request.get_header("X-CSRF-Token")
119
+ expected_token = session[:_csrf_token] || session["_csrf_token"]
120
+
121
+ if token.present? && expected_token.present?
122
+ ActiveSupport::SecurityUtils.secure_compare(token.to_s, expected_token.to_s)
123
+ else
124
+ false
125
+ end
126
+ end
127
+ end
138
128
 
139
- entity_statement_url = "https://provider.example.com/.well-known/openid-federation"
140
- entity_statement_fingerprint = "expected-fingerprint-hash"
141
-
142
- Rails.application.config.middleware.use OmniAuth::Builder do
143
- provider :openid_federation,
144
- discovery: true,
145
- entity_statement_path: "config/provider-entity-statement.jwt", # Cached copy from URL
146
- entity_statement_url: entity_statement_url, # Always provide URL as source of truth
147
- entity_statement_fingerprint: entity_statement_fingerprint, # Fingerprint for verification
148
- client_options: {
149
- identifier: ENV["OPENID_CLIENT_ID"],
150
- redirect_uri: "https://your-app.com/auth/openid_federation/callback",
151
- private_key: OpenSSL::PKey::RSA.new(File.read("config/client-private-key.pem"))
152
- }
129
+ Devise.setup do |config|
130
+ config.omniauth :openid_federation,
131
+ strategy_class: OmniAuth::Strategies::OpenIDFederation,
132
+ name: :openid_federation,
133
+ scope: [:openid],
134
+ response_type: "code",
135
+ discovery: true,
136
+ client_auth_method: :jwt_bearer,
137
+ client_signing_alg: :RS256,
138
+ entity_statement_path: entity_statement_path,
139
+ always_encrypt_request_object: true,
140
+ client_options: {
141
+ identifier: ENV["OPENID_CLIENT_ID"],
142
+ redirect_uri: ENV["OPENID_REDIRECT_URI"] || "#{ENV["APP_URL"]}/users/auth/openid_federation/callback",
143
+ private_key: private_key
144
+ }
145
+ end
153
146
  end
154
147
  ```
155
148
 
156
149
  ### Step 5: Configure Federation Endpoint (For Automatic Registration)
157
150
 
158
- If using automatic registration, publish your client entity statement:
159
-
160
151
  ```ruby
161
152
  # config/initializers/omniauth_openid_federation.rb
162
- OmniauthOpenidFederation::FederationEndpoint.auto_configure(
163
- issuer: ENV["APP_URL"],
164
- private_key: private_key,
165
- entity_statement_path: "config/client-entity-statement.jwt", # Optional: cached copy for offline dev
166
- metadata: {
167
- openid_provider: {
168
- issuer: ENV["APP_URL"],
169
- authorization_endpoint: "#{ENV["APP_URL"]}/users/auth/openid_federation",
170
- token_endpoint: "#{ENV["APP_URL"]}/users/auth/openid_federation",
171
- userinfo_endpoint: "#{ENV["APP_URL"]}/users/auth/openid_federation",
172
- jwks_uri: "#{ENV["APP_URL"]}/.well-known/jwks.json",
173
- signed_jwks_uri: "#{ENV["APP_URL"]}/.well-known/signed-jwks.json"
174
- }
175
- }
176
- )
177
- ```
178
-
179
- ```ruby
180
- # config/routes.rb
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)
186
- ```
187
-
188
- **Key Points**:
189
- - `auto_configure` automatically extracts/generates JWKS from keys
190
- - Only application-specific endpoints need to be provided in metadata
191
- - Well-known endpoints are auto-generated
192
-
193
- ### Step 6: Add Routes
194
-
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:
198
-
199
- ```ruby
200
- # config/routes.rb
201
- Rails.application.routes.draw do
202
- # Mount the Engine to enable /.well-known/openid-federation endpoint
203
- mount OmniauthOpenidFederation::Engine => "/"
153
+ if ENV["OPENID_ENABLED"] == "true"
154
+ app_url = ENV["APP_URL"] || "https://your-app.example.com"
204
155
 
205
- # Your other routes...
206
- devise_for :users, controllers: {
207
- omniauth_callbacks: "users/omniauth_callbacks"
208
- }
156
+ private_key = if ENV["OPENID_CLIENT_PRIVATE_KEY_BASE64"]
157
+ OpenSSL::PKey::RSA.new(Base64.decode64(ENV["OPENID_CLIENT_PRIVATE_KEY_BASE64"]))
158
+ elsif ENV["OPENID_CLIENT_PRIVATE_KEY_PATH"]
159
+ OpenSSL::PKey::RSA.new(File.read(Rails.root.join(ENV["OPENID_CLIENT_PRIVATE_KEY_PATH"])))
160
+ else
161
+ OpenSSL::PKey::RSA.new(File.read(Rails.root.join("config", "client-private-key.pem")))
162
+ end
163
+
164
+ client_entity_statement_path = ENV["OPENID_CLIENT_ENTITY_STATEMENT_PATH"] ||
165
+ Rails.root.join("config", "client-entity-statement.jwt").to_s
166
+
167
+ OmniauthOpenidFederation::FederationEndpoint.auto_configure(
168
+ issuer: app_url,
169
+ private_key: private_key,
170
+ entity_statement_path: client_entity_statement_path,
171
+ metadata: {
172
+ openid_relying_party: {
173
+ redirect_uris: [
174
+ ENV["OPENID_REDIRECT_URI"] || "#{app_url}/users/auth/openid_federation/callback"
175
+ ],
176
+ client_registration_types: ["automatic"],
177
+ application_type: "web",
178
+ grant_types: ["authorization_code"],
179
+ response_types: ["code"],
180
+ token_endpoint_auth_method: "private_key_jwt",
181
+ token_endpoint_auth_signing_alg: "RS256",
182
+ request_object_signing_alg: "RS256",
183
+ id_token_encrypted_response_alg: "RSA-OAEP",
184
+ id_token_encrypted_response_enc: "A128CBC-HS256",
185
+ organization_name: ENV["OPENID_ORGANIZATION_NAME"]
186
+ }
187
+ }
188
+ )
209
189
  end
210
190
  ```
211
191
 
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)
192
+ ### Step 6: Add Routes
215
193
 
216
194
  ```ruby
217
195
  # config/routes.rb
218
- Rails.application.routes.draw do
196
+ if ENV["OPENID_ENABLED"] == "true"
219
197
  mount OmniauthOpenidFederation::Engine => "/"
220
-
221
- get "/auth/:provider/callback", to: "sessions#create"
222
- get "/auth/failure", to: "sessions#failure"
223
198
  end
224
- ```
225
-
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
199
 
230
- ```ruby
231
- # config/routes.rb
232
200
  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
201
+ devise_for :users, controllers: {
202
+ omniauth_callbacks: "users/omniauth_callbacks"
203
+ }
279
204
  end
280
205
  ```
281
206
 
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
288
-
289
- #### For Devise
207
+ ### Step 7: Create Callback Controller
290
208
 
291
209
  ```ruby
292
210
  # app/controllers/users/omniauth_callbacks_controller.rb
293
211
  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
212
  skip_before_action :verify_authenticity_token, only: [:openid_federation, :failure]
298
213
  skip_before_action :authenticate_user!, only: [:openid_federation, :failure]
299
214
 
@@ -314,9 +229,7 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
314
229
  end
315
230
  ```
316
231
 
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
232
+ ### Step 8: Create User Model Method
320
233
 
321
234
  ```ruby
322
235
  # app/models/user.rb
@@ -327,18 +240,14 @@ class User < ApplicationRecord
327
240
  if user
328
241
  user.update(
329
242
  email: auth.info.email,
330
- name: auth.info.name,
331
- first_name: auth.info.first_name,
332
- last_name: auth.info.last_name
243
+ name: auth.info.name
333
244
  )
334
245
  else
335
246
  user = create(
336
247
  provider: auth.provider,
337
248
  uid: auth.uid,
338
249
  email: auth.info.email,
339
- name: auth.info.name,
340
- first_name: auth.info.first_name,
341
- last_name: auth.info.last_name
250
+ name: auth.info.name
342
251
  )
343
252
  end
344
253
 
@@ -347,576 +256,169 @@ class User < ApplicationRecord
347
256
  end
348
257
  ```
349
258
 
350
- ## Rake Tasks
351
-
352
- ### Prepare Client Keys
353
-
354
- ```bash
355
- rake openid_federation:prepare_client_keys
356
- rake openid_federation:prepare_client_keys[separate,config] # Separate signing/encryption keys
357
- ```
358
-
359
- ### Fetch Entity Statement
360
-
361
- Fetches entity statement from provider URL, verifies fingerprint, and caches locally:
362
-
363
- ```bash
364
- rake openid_federation:fetch_entity_statement[
365
- "https://provider.example.com/.well-known/openid-federation",
366
- "expected-fingerprint-hash",
367
- "config/provider-entity-statement.jwt"
368
- ]
369
- ```
370
-
371
- **Note**: Always use the URL as the source of truth - the local file is just a cached copy.
372
-
373
- ### Parse Entity Statement
374
-
375
- ```bash
376
- rake openid_federation:parse_entity_statement["config/provider-entity-statement.jwt"]
377
- ```
378
-
379
- ### Test Local Entity Statement Endpoint
380
-
381
- Validates your local entity statement endpoint and tests all linked endpoints. Useful for verifying your federation endpoint implementation:
382
-
383
- ```bash
384
- # Default (localhost:3000)
385
- rake openid_federation:test_local_endpoint
386
-
387
- # Custom base URL
388
- rake openid_federation:test_local_endpoint[http://localhost:3000]
389
-
390
- # Via environment variable
391
- BASE_URL=http://localhost:3000 rake openid_federation:test_local_endpoint
392
- ```
393
-
394
- This task:
395
- - Fetches and validates the entity statement from `/.well-known/openid-federation`
396
- - Shows key configuration status (single vs separate keys) with recommendations
397
- - Tests all endpoints mentioned in the entity statement
398
- - Displays validation warnings without blocking execution
399
-
400
- See all tasks: `rake -T openid_federation`
259
+ ## Passing Custom Parameters
401
260
 
402
- ### Cache Configuration and Key Rotation
261
+ ### Using `request_object_params` (Allow-List)
403
262
 
404
- Configure automatic key rotation:
263
+ Pass custom parameters via `request_object_params` allow-list:
405
264
 
406
265
  ```ruby
407
- OmniauthOpenidFederation.configure do |config|
408
- config.cache_ttl = 3600 # Refresh provider keys every hour
409
- config.rotate_on_errors = true # Auto-handle provider key rotation
410
- end
266
+ config.omniauth :openid_federation,
267
+ request_object_params: ["custom_param", "another_param"],
268
+ # ... other options
411
269
  ```
412
270
 
413
- ### Security Instrumentation
414
-
415
- Configure custom instrumentation for security events, MITM attack detection, and authentication mismatches:
271
+ Parameters in the allow-list are automatically included in the JWT request object if present in the HTTP request.
416
272
 
417
- ```ruby
418
- OmniauthOpenidFederation.configure do |config|
419
- # Configure with Sentry
420
- config.instrumentation = ->(event, data) do
421
- Sentry.capture_message(
422
- "OpenID Federation: #{event}",
423
- level: data[:severity] == :error ? :error : :warning,
424
- extra: data
425
- )
426
- end
427
- end
428
- ```
273
+ ### Using `prepare_request_object_params` (Proc)
429
274
 
430
- **With Honeybadger**:
431
- ```ruby
432
- OmniauthOpenidFederation.configure do |config|
433
- config.instrumentation = ->(event, data) do
434
- Honeybadger.notify("OpenID Federation: #{event}", context: data)
435
- end
436
- end
437
- ```
275
+ Use `prepare_request_object_params` proc to modify parameters before they're added to the signed request object. This is useful for:
276
+ - Combining config values with form values (e.g., base `acr_values` + provider-specific)
277
+ - Adding config-based parameters (e.g., `ftn_spname` from config)
278
+ - Transforming or validating parameters
438
279
 
439
- **With custom logger**:
440
280
  ```ruby
441
- OmniauthOpenidFederation.configure do |config|
442
- config.instrumentation = ->(event, data) do
443
- Rails.logger.warn("[Security] #{event}: #{data.inspect}")
444
- end
445
- end
446
- ```
447
-
448
- **Instrumented Events**:
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)
451
- - `signature_verification_failed` - JWT signature verification failed (possible MITM)
452
- - `decryption_failed` - Token decryption failed (possible MITM or key mismatch)
453
- - `token_validation_failed` - Token validation failed (possible tampering)
454
- - `key_rotation_detected` - Key rotation detected (normal operation)
455
- - `kid_not_found` - Key ID not found in JWKS (possible key rotation or MITM)
456
- - `entity_statement_validation_failed` - Entity statement validation failed (possible MITM)
457
- - `fingerprint_mismatch` - Entity statement fingerprint mismatch (possible MITM)
458
- - `trust_chain_validation_failed` - Trust chain validation failed
459
- - `unexpected_authentication_break` - Unexpected authentication failure (missing code, token exchange errors, unknown errors)
460
- - `missing_required_claims` - Token missing required claims
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
-
467
- **Security Note**: All sensitive data (tokens, keys, fingerprints) is automatically sanitized before being sent to your instrumentation callback.
468
-
469
- **Key Rotation Types**:
470
- - **Provider Keys** (from external providers): ✅ Automatic via Signed JWKS - library automatically detects and uses new provider keys
471
- - **Client Keys** (your own keys): ⚠️ **Manual rotation required** - you must generate new RSA keys and update entity statement
472
-
473
- **Client Key Rotation Process** (Manual Steps Required):
474
- 1. **Generate new RSA keys** (manual):
475
- ```bash
476
- bundle exec rake omniauth_openid_federation:prepare_client_keys[key_type=separate]
477
- ```
478
- 2. **Update entity statement file** (manual): Update `entity_statement_path` with new keys, or let the library regenerate it
479
- 3. **Library automatically uses new keys** (automatic): Library extracts JWKS from updated entity statement file on next cache refresh
480
-
481
- **Note**: The library automatically generates JWKS from your RSA keys, but you must manually generate new RSA keys when rotating. The library then automatically uses the new keys from the updated entity statement file. See [Automatic Key Provisioning](#automatic-key-provisioning) for details.
482
-
483
- ### Publishing Federation Endpoint
484
-
485
- Publish your entity statement at `/.well-known/openid-federation` using `auto_configure`.
486
-
487
- The library supports two entity types:
488
- - **openid_relying_party (RP)**: For clients/relying parties (PRIMARY USE CASE)
489
- - **openid_provider (OP)**: For providers/servers (secondary use case)
490
-
491
- #### Relying Party (RP) Configuration (Primary Use Case)
492
-
493
- **First, generate your RSA keys** (if not already generated):
494
-
495
- ```bash
496
- # Generate separate signing and encryption keys (RECOMMENDED for production)
497
- bundle exec rake omniauth_openid_federation:prepare_client_keys[key_type=separate]
498
-
499
- # Or generate single key for dev/testing (NOT RECOMMENDED for production)
500
- bundle exec rake omniauth_openid_federation:prepare_client_keys[key_type=single]
281
+ config.omniauth :openid_federation,
282
+ request_object_params: [:ftn_spname], # Allow-list for custom params
283
+ prepare_request_object_params: proc do |params|
284
+ # Combine config acr_values with form acr_values
285
+ form_acr_values = params["acr_values"]&.to_s&.strip
286
+ config_acr_values = ENV["OPENID_ACR_VALUES"].to_s.strip
287
+
288
+ if config_acr_values.present? && form_acr_values.present?
289
+ params["acr_values"] = "#{config_acr_values} #{form_acr_values}".strip
290
+ elsif config_acr_values.present?
291
+ params["acr_values"] = config_acr_values
292
+ end
293
+
294
+ # Add custom parameter from config
295
+ params["ftn_spname"] = ENV["OPENID_FTN_SPNAME"] if ENV["OPENID_FTN_SPNAME"].present?
296
+
297
+ params
298
+ end,
299
+ # ... other options
501
300
  ```
502
301
 
503
- This creates:
504
- - `config/client-signing-private-key.pem` and `config/client-encryption-private-key.pem` (separate keys)
505
- - OR `config/client-private-key.pem` (single key for dev/testing)
506
-
507
- **Then configure the federation endpoint** - the library automatically generates JWKS from your keys:
302
+ **Form Example** (pass clean values, proc handles combining):
508
303
 
509
304
  ```ruby
510
- # config/initializers/omniauth_openid_federation.rb
511
- # Production Setup (RECOMMENDED): Separate signing and encryption keys
512
- # The library automatically generates JWKS from these keys
513
- OmniauthOpenidFederation::FederationEndpoint.auto_configure(
514
- issuer: "https://your-app.com",
515
- signing_key: OpenSSL::PKey::RSA.new(File.read("config/client-signing-private-key.pem")),
516
- encryption_key: OpenSSL::PKey::RSA.new(File.read("config/client-encryption-private-key.pem")),
517
- entity_statement_path: "config/client-entity-statement.jwt", # Cache for automatic key rotation
518
- metadata: {
519
- openid_relying_party: {
520
- redirect_uris: ["https://your-app.com/users/auth/openid_federation/callback"],
521
- client_registration_types: ["automatic"],
522
- application_type: "web",
523
- grant_types: ["authorization_code"],
524
- response_types: ["code"],
525
- token_endpoint_auth_method: "private_key_jwt",
526
- token_endpoint_auth_signing_alg: "RS256",
527
- request_object_signing_alg: "RS256",
528
- id_token_encrypted_response_alg: "RSA-OAEP",
529
- id_token_encrypted_response_enc: "A128CBC-HS256"
530
- }
531
- },
532
- auto_provision_keys: true # Library automatically generates JWKS from provided keys
533
- )
305
+ # In your form - pass only provider-specific value
306
+ <%= button_to "Login", user_openid_federation_omniauth_authorize_path,
307
+ method: :post,
308
+ params: { acr_values: "provider_specific_level" } %>
534
309
  ```
535
310
 
536
- **Development/Testing** (NOT RECOMMENDED FOR PRODUCTION):
537
- ```ruby
538
- OmniauthOpenidFederation::FederationEndpoint.auto_configure(
539
- issuer: "https://your-app.com",
540
- private_key: private_key, # DEV/TESTING ONLY - single key for both signing and encryption
541
- entity_statement_path: "config/client-entity-statement.jwt",
542
- metadata: {
543
- openid_relying_party: { ... }
544
- },
545
- auto_provision_keys: true
546
- )
547
- ```
311
+ The proc will combine this with config values before adding to the signed JWT.
548
312
 
549
- #### OpenID Provider (OP) Configuration (Secondary Use Case)
313
+ ## Rake Tasks
550
314
 
551
- **First, generate your RSA keys** (if not already generated):
315
+ ### Prepare Client Keys
552
316
 
553
317
  ```bash
554
- # Generate separate signing and encryption keys (RECOMMENDED for production)
555
- bundle exec rake omniauth_openid_federation:prepare_client_keys[key_type=separate,output_dir=config]
556
-
557
- # Or generate single key for dev/testing (NOT RECOMMENDED for production)
558
- bundle exec rake omniauth_openid_federation:prepare_client_keys[key_type=single,output_dir=config]
559
- ```
560
-
561
- **Then configure the federation endpoint** - the library automatically generates JWKS from your keys:
562
-
563
- ```ruby
564
- # For provider/server applications
565
- # Production Setup (RECOMMENDED): Separate signing and encryption keys
566
- # The library automatically generates JWKS from these keys
567
- signing_key = OpenSSL::PKey::RSA.new(File.read("config/client-signing-private-key.pem"))
568
- encryption_key = OpenSSL::PKey::RSA.new(File.read("config/client-encryption-private-key.pem"))
569
-
570
- OmniauthOpenidFederation::FederationEndpoint.auto_configure(
571
- issuer: "https://provider.example.com",
572
- signing_key: signing_key,
573
- encryption_key: encryption_key,
574
- entity_statement_path: "config/provider-entity-statement.jwt",
575
- metadata: {
576
- openid_provider: {
577
- issuer: "https://provider.example.com",
578
- authorization_endpoint: "https://provider.example.com/oauth2/authorize",
579
- token_endpoint: "https://provider.example.com/oauth2/token",
580
- userinfo_endpoint: "https://provider.example.com/oauth2/userinfo",
581
- jwks_uri: "https://provider.example.com/.well-known/jwks.json",
582
- signed_jwks_uri: "https://provider.example.com/.well-known/signed-jwks.json"
583
- # federation_fetch_endpoint is automatically added for OPs
584
- }
585
- },
586
- auto_provision_keys: true # Library automatically generates JWKS from provided keys
587
- )
588
- ```
589
-
590
- **Development/Testing** (NOT RECOMMENDED FOR PRODUCTION):
591
- ```ruby
592
- # Single private key for both signing and encryption (DEV/TESTING ONLY)
593
- OmniauthOpenidFederation::FederationEndpoint.auto_configure(
594
- issuer: "https://provider.example.com",
595
- private_key: private_key, # DEV/TESTING ONLY - not recommended for production
596
- entity_statement_path: "config/provider-entity-statement.jwt",
597
- metadata: {
598
- openid_provider: {
599
- issuer: "https://provider.example.com",
600
- authorization_endpoint: "https://provider.example.com/oauth2/authorize",
601
- token_endpoint: "https://provider.example.com/oauth2/token",
602
- userinfo_endpoint: "https://provider.example.com/oauth2/userinfo",
603
- jwks_uri: "https://provider.example.com/.well-known/jwks.json",
604
- signed_jwks_uri: "https://provider.example.com/.well-known/signed-jwks.json"
605
- }
606
- },
607
- auto_provision_keys: true
608
- )
318
+ rake openid_federation:prepare_client_keys
609
319
  ```
610
320
 
611
- ```ruby
612
- # config/routes.rb
613
- # RECOMMENDED: Mount the Engine (Rails-idiomatic way)
614
- mount OmniauthOpenidFederation::Engine => "/"
321
+ ### Fetch Entity Statement
615
322
 
616
- # ALTERNATIVE: Use mount_routes helper (for backward compatibility)
617
- # OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)
323
+ ```bash
324
+ rake openid_federation:fetch_entity_statement[
325
+ "https://provider.example.com/.well-known/openid-federation",
326
+ "expected-fingerprint-hash",
327
+ "config/provider-entity-statement.jwt"
328
+ ]
618
329
  ```
619
330
 
620
- **What `auto_configure` does automatically**:
621
- - Extracts JWKS from entity statement file or generates from provided keys
622
- - Supports separate signing/encryption keys (RECOMMENDED) or single key (dev/testing)
623
- - Auto-detects entity type and generates well-known endpoints
624
- - Uses `entity_statement_path` as cache for key rotation
625
-
626
- **Manual Configuration** (advanced, not recommended):
331
+ ### Test Authentication Flow
627
332
 
628
- If you need manual control, use `configure` instead of `auto_configure`:
629
-
630
- ```ruby
631
- OmniauthOpenidFederation::FederationEndpoint.configure do |config|
632
- config.issuer = "https://your-app.com"
633
- config.subject = "https://your-app.com"
634
- config.signing_key = signing_key # RECOMMENDED: Separate signing key
635
- config.encryption_key = encryption_key # RECOMMENDED: Separate encryption key
636
- config.jwks = jwks # Must provide manually
637
- config.metadata = { ... }
638
- end
333
+ ```bash
334
+ rake openid_federation:test_authentication_flow[
335
+ "https://provider.example.com/login",
336
+ "https://your-app.com",
337
+ "urn:mace:incommon:iap:silver"
338
+ ]
639
339
  ```
640
340
 
641
- ### Automatic Key Provisioning
642
-
643
- The `auto_configure` method automatically generates JWKS from your RSA keys (generate keys first using the rake task).
644
-
645
- **Priority Order**:
646
- 1. Extracts JWKS from `entity_statement_path` if file exists (supports key rotation)
647
- 2. Generates JWKS from separate `signing_key` and `encryption_key` (RECOMMENDED)
648
- 3. Generates JWKS from single `private_key` (dev/testing only)
649
-
650
- **Key Rotation** (Semi-Automatic):
651
- 1. **Manual**: Generate new RSA keys using `rake omniauth_openid_federation:prepare_client_keys`
652
- 2. **Manual**: Update entity statement file at `entity_statement_path` with new keys
653
- 3. **Automatic**: Library extracts and uses new keys from updated file on next cache refresh
654
-
655
341
  ## Configuration Options
656
342
 
657
343
  ### Required
658
344
 
659
- - `client_options[:identifier]` - Client ID from provider
660
- - `client_options[:redirect_uri]` - Callback URL
661
- - `client_options[:private_key]` - RSA private key for signing
662
- - **One of the following** (for provider entity statement):
663
- - `entity_statement_url` - Provider entity statement URL (recommended - library fetches and caches automatically)
664
- - `issuer` - Provider issuer URI (library builds entity statement URL from issuer + `/.well-known/openid-federation`)
665
- - `entity_statement_path` - Provider entity statement path (optional - for offline development)
345
+ - `client_options.identifier` - Client ID from provider
346
+ - `client_options.redirect_uri` - Callback URL
347
+ - `client_options.private_key` - RSA private key for signing
348
+ - `entity_statement_path` - Path to cached entity statement file
666
349
 
667
350
  ### Optional
668
351
 
669
- - `discovery` - Enable automatic endpoint discovery (default: `true`)
670
- - `entity_statement_fingerprint` - Expected SHA-256 fingerprint for verification (recommended when using `entity_statement_url` or `issuer`)
671
- - `entity_statement_path` - Path to provider entity statement (optional - for offline development, cached copy)
672
- - `always_encrypt_request_object` - Always encrypt request objects if encryption keys are available (default: `false`, see [Request Object Security](#request-object-security-signing-vs-encryption) below)
673
- - `client_entity_statement_url` - URL to client entity statement (for automatic registration)
674
- - `client_entity_statement_path` - Path to client entity statement (fallback if URL not available)
675
- - `client_registration_type` - `:explicit` (default) or `:automatic` (auto-detected if client_entity_statement_url/path provided)
676
- - `client_entity_identifier` - Entity identifier for automatic registration
677
- - `scope` - OAuth scopes (default: `[:openid]`)
678
- - `response_type` - Response type (default: `"code"`)
679
- - `client_auth_method` - Client authentication (default: `:jwt_bearer`)
680
- - `client_signing_alg` - Signing algorithm (default: `:RS256`)
681
- - `fetch_userinfo` - Whether to fetch userinfo endpoint (default: `true`)
682
- - `acr_values` - Authentication Context Class Reference values (provider-specific)
683
- - `key_source` - `:local` (default) or `:federation` (advanced)
684
-
685
- ### Global Configuration
686
-
687
- Configure global settings via `OmniauthOpenidFederation.configure`:
688
-
689
- ```ruby
690
- OmniauthOpenidFederation.configure do |config|
691
- # Cache configuration
692
- config.cache_ttl = 3600 # JWKS cache TTL in seconds
693
- config.rotate_on_errors = true # Auto-rotate on key-related errors
694
-
695
- # Security instrumentation (Sentry, Honeybadger, etc.)
696
- config.instrumentation = ->(event, data) do
697
- Sentry.capture_message("OpenID Federation: #{event}", level: :warning, extra: data)
698
- end
699
-
700
- # HTTP configuration
701
- config.http_timeout = 10
702
- config.max_retries = 3
703
- config.verify_ssl = true
704
- end
705
- ```
706
-
707
- ### Request Object Security (Signing vs Encryption)
352
+ - `entity_statement_url` - URL to fetch entity statement (auto-fetches if provided)
353
+ - `entity_statement_fingerprint` - Fingerprint for verification
354
+ - `client_entity_statement_url` - Client entity statement URL (for automatic registration)
355
+ - `client_entity_statement_path` - Client entity statement path (cached copy)
356
+ - `always_encrypt_request_object` - Force encryption of request objects (default: false)
357
+ - `request_object_params` - Array of parameter names to include in request object (allow-list)
358
+ - `prepare_request_object_params` - Proc to modify params before adding to signed request object: `proc { |params| modified_params }`
359
+ - `discovery` - Enable automatic endpoint discovery (default: true)
708
360
 
709
- **Per OpenID Federation 1.0 and RFC 9101:**
710
- - **Signing (MANDATORY)**: Request objects **MUST be signed** using RS256 (always enforced, cannot be disabled)
711
- - **Encryption (OPTIONAL)**: Request objects **MAY be encrypted** when provider requires it or when `always_encrypt_request_object: true`
712
-
713
- **Encryption Behavior:**
714
- - **Default** (`always_encrypt_request_object: false`): Only encrypts if provider metadata specifies `request_object_encryption_alg`
715
- - **When `true`**: Encrypts even if provider doesn't require it (if encryption keys available)
716
- - **Use case**: High-security deployments requiring defense-in-depth beyond minimum spec
717
-
718
- **Note**: Signing provides authentication and integrity. Encryption adds confidentiality but is optional and adds overhead.
719
-
720
- ### Detailed Configuration Examples
721
-
722
- #### Devise with Environment Variables (Recommended)
723
-
724
- ```ruby
725
- # config/initializers/devise.rb
726
- require "omniauth_openid_federation"
727
-
728
- private_key = if ENV["OPENID_CLIENT_PRIVATE_KEY"]
729
- OpenSSL::PKey::RSA.new(Base64.decode64(ENV["OPENID_CLIENT_PRIVATE_KEY"]))
730
- else
731
- OpenSSL::PKey::RSA.new(File.read("config/client-private-key.pem"))
732
- end
733
-
734
- config.omniauth :openid_federation,
735
- discovery: true, # Auto-discovers endpoints from entity statement
736
- entity_statement_url: ENV["OPENID_ENTITY_STATEMENT_URL"], # Always provide URL
737
- entity_statement_fingerprint: ENV["OPENID_ENTITY_STATEMENT_FINGERPRINT"], # Fingerprint for verification
738
- entity_statement_path: "config/provider-entity-statement.jwt", # Cached copy from URL (fetch via rake task)
739
- client_entity_statement_url: "#{ENV["APP_URL"]}/.well-known/openid-federation", # For automatic registration
740
- client_options: {
741
- identifier: ENV["OPENID_CLIENT_ID"],
742
- redirect_uri: "#{ENV["APP_URL"]}/users/auth/openid_federation/callback",
743
- private_key: private_key
744
- }
745
- # All endpoints are auto-discovered - no manual configuration needed
746
- ```
747
-
748
- #### OmniAuth with URL-based Entity Statement (Production)
749
-
750
- ```ruby
751
- # config/initializers/omniauth.rb
752
- require "omniauth_openid_federation"
753
-
754
- entity_statement_url = "https://provider.example.com/.well-known/openid-federation"
755
- entity_statement_fingerprint = "expected-fingerprint-hash"
756
-
757
- Rails.application.config.middleware.use OmniAuth::Builder do
758
- provider :openid_federation,
759
- discovery: true,
760
- entity_statement_url: entity_statement_url, # Always provide URL
761
- entity_statement_fingerprint: entity_statement_fingerprint, # Fingerprint for verification
762
- entity_statement_path: "config/provider-entity-statement.jwt", # Cached copy from URL
763
- client_options: {
764
- identifier: ENV["OPENID_CLIENT_ID"],
765
- redirect_uri: "https://your-app.com/auth/openid_federation/callback",
766
- private_key: OpenSSL::PKey::RSA.new(File.read("config/client-private-key.pem"))
767
- }
768
- end
769
- ```
770
-
771
- **Key Points**:
772
- - **Always provide `entity_statement_url`** - this is the source of truth
773
- - `entity_statement_fingerprint` is used for verification when fetching
774
- - `entity_statement_path` points to the cached copy fetched from the URL
775
- - All endpoints are automatically discovered - no manual endpoint configuration
776
-
777
- ## API Reference
778
-
779
- ### `OmniauthOpenidFederation::Jws`
780
-
781
- Builds and signs JWT request objects:
782
-
783
- ```ruby
784
- jws = OmniauthOpenidFederation::Jws.new(
785
- client_id: "client-id",
786
- redirect_uri: "https://example.com/callback",
787
- scope: "openid",
788
- issuer: "https://provider.example.com",
789
- audience: "https://provider.example.com",
790
- private_key: private_key
791
- )
792
- signed_jwt = jws.sign
793
- ```
794
-
795
- ### `OmniauthOpenidFederation::Federation::EntityStatement`
796
-
797
- Fetches and validates entity statements:
798
-
799
- ```ruby
800
- statement = OmniauthOpenidFederation::Federation::EntityStatement.fetch!(
801
- "https://provider.example.com/.well-known/openid-federation",
802
- fingerprint: "expected-fingerprint"
803
- )
804
- metadata = statement.parse
805
- ```
806
-
807
- ### `OmniauthOpenidFederation::Federation::SignedJWKS`
808
-
809
- Fetches and validates signed JWKS:
810
-
811
- ```ruby
812
- signed_jwks = OmniauthOpenidFederation::Federation::SignedJWKS.fetch!(
813
- signed_jwks_uri,
814
- entity_jwks
815
- )
816
- ```
361
+ ## Security
817
362
 
818
- See inline code documentation for complete API reference.
363
+ - All user input is validated and sanitized
364
+ - Configuration values are trusted (not validated)
365
+ - Signed request objects are required (RFC 9101)
366
+ - CSRF protection via Rails tokens (request phase) and OAuth state (callback phase)
367
+ - Private keys should never be committed to version control
819
368
 
820
369
  ## Troubleshooting
821
370
 
822
- **"Private key is required"**
823
- - Generate keys: `rake openid_federation:prepare_client_keys`
824
- - Verify key path and format (PEM)
825
-
826
- **"Audience is required"**
827
- - Provide `entity_statement_url` and `entity_statement_path` (auto-resolves audience from entity statement)
828
-
829
- **"Entity statement fingerprint mismatch"**
830
- - Verify `entity_statement_fingerprint` with provider
831
- - Fetch fresh entity statement from URL: `rake openid_federation:fetch_entity_statement[entity_statement_url, entity_statement_fingerprint, entity_statement_path]`
832
- - Always use the provider URL as the source of truth
833
-
834
- **"JWT signature verification failed"**
835
- - Provider may have rotated keys (auto-handled with `rotate_on_errors: true`)
836
- - Clear cache: `Rails.cache.delete_matched("openid_federation_jwks_*")`
371
+ **"Missing authorization code"**: Check that redirect_uri matches provider configuration exactly.
837
372
 
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)
373
+ **"Failed to exchange authorization code"**: Verify private key is correct and client_id matches provider.
844
374
 
845
- ## Security
846
-
847
- See [SECURITY.md](SECURITY.md) for detailed security features, protections, and vulnerability reporting.
375
+ **"Entity statement not found"**: Ensure entity statement is fetched and cached locally, or provide `entity_statement_url`.
848
376
 
849
377
  ## Requirements
850
378
 
851
379
  - Ruby >= 3.0
852
- - Rails >= 6.0 (optional)
853
- - `omniauth-oauth2` ~> 1.8
854
- - `openid_connect` ~> 2.3
855
- - `jwe` ~> 1.1
856
- - `jwt` ~> 3.1
857
- - `http` ~> 5.3
380
+ - Rails >= 6.1 (or compatible Rack application)
381
+ - OpenSSL (for RSA key operations)
858
382
 
859
383
  ## Example Files
860
384
 
861
385
  See `examples/` directory for complete configuration examples:
862
386
  - `examples/config/initializers/devise.rb.example`
863
- - `examples/app/controllers/users/omniauth_callbacks_controller.rb.example`
864
- - `examples/app/models/user.rb.example`
387
+ - `examples/config/initializers/omniauth_openid_federation.rb.example`
388
+ - `examples/config/open_id_connect_config.rb.example`
865
389
 
866
390
  ## Development
867
391
 
868
- Run release.rb script to prepare code for publishing, it has all the required checks and tests.
869
-
870
- ```bash
871
- usr/bin/release.rb
872
- ```
873
-
874
- ### Development: Using from Local Repository
875
-
876
- When developing the gem or testing changes in your application, you can point your Gemfile to a local path:
877
-
878
- ```ruby
879
- # In your application's Gemfile
880
- gem "omniauth_openid_federation", path: "../omniauth_openid_federation.rb"
881
- ```
882
-
883
- Then run:
884
-
885
392
  ```bash
393
+ git clone https://github.com/amkisko/omniauth_openid_federation.rb.git
394
+ cd omniauth_openid_federation.rb
886
395
  bundle install
396
+ bin/rspec
887
397
  ```
888
398
 
889
- **Note:** When using `path:` in your Gemfile, Bundler will use the local gem directly. Changes you make to the gem code will be immediately available in your application without needing to rebuild or reinstall the gem. This is ideal for development and testing.
890
-
891
399
  ## Contributing
892
400
 
893
- Bug reports and pull requests are welcome on GitHub at https://github.com/amkisko/omniauth_openid_federation.rb
894
-
895
- Contribution policy:
896
- - New features are not necessarily added to the gem
897
- - Pull request should have test coverage for affected parts
898
- - Pull request should have changelog entry
899
-
900
- Review policy:
901
- - It might take up to 2 calendar weeks to review and merge critical fixes
902
- - It might take up to 6 calendar months to review and merge pull request
903
- - It might take up to 1 calendar year to review an issue
401
+ Contributions welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
904
402
 
403
+ ## References
905
404
 
906
- ## Publishing
405
+ ### Specifications
907
406
 
908
- ```sh
909
- rm omniauth_openid_federation-*.gem
910
- gem build omniauth_openid_federation.gemspec
911
- gem push omniauth_openid_federation-*.gem
912
- ```
407
+ - [OpenID Federation 1.0](https://openid.net/specs/openid-federation-1_0.html)
408
+ - [RFC 9101: OAuth 2.0 Authorization Server Issuer Identification](https://www.rfc-editor.org/rfc/rfc9101.html)
409
+ - [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
913
410
 
914
- ## References
411
+ ### Related Gems
915
412
 
916
- - [OpenID Federation 1.0 Specification](https://openid.net/specs/openid-federation-1_0.html)
917
- - [RFC 9101 - OAuth 2.0 Authorization Request](https://datatracker.ietf.org/doc/html/rfc9101)
918
- - [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
413
+ - [omniauth](https://github.com/omniauth/omniauth) - Authentication framework
414
+ - [devise](https://github.com/heartcombo/devise) - Rails authentication solution
415
+ - [jwt](https://github.com/jwt/ruby-jwt) - JSON Web Token implementation
416
+ - [jwe](https://github.com/nov/jwe) - JSON Web Encryption
417
+ - [openid_connect](https://github.com/nov/openid_connect) - OpenID Connect client
418
+ - [http](https://github.com/httprb/http) - HTTP client
419
+ - [anyway_config](https://github.com/palkan/anyway_config) - Configuration management
420
+ - [action_reporter](https://github.com/basecamp/action_reporter) - Error reporting
919
421
 
920
422
  ## License
921
423
 
922
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
424
+ MIT License. See [LICENSE.md](LICENSE.md) for details.