omniauth_openid_federation 1.0.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.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +16 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +822 -0
  5. data/SECURITY.md +129 -0
  6. data/examples/README_INTEGRATION_TESTING.md +399 -0
  7. data/examples/README_MOCK_OP.md +243 -0
  8. data/examples/app/controllers/users/omniauth_callbacks_controller.rb.example +33 -0
  9. data/examples/app/jobs/jwks_rotation_job.rb.example +60 -0
  10. data/examples/app/models/user.rb.example +39 -0
  11. data/examples/config/initializers/devise.rb.example +97 -0
  12. data/examples/config/initializers/federation_endpoint.rb.example +206 -0
  13. data/examples/config/mock_op.yml.example +83 -0
  14. data/examples/config/open_id_connect_config.rb.example +210 -0
  15. data/examples/config/routes.rb.example +12 -0
  16. data/examples/db/migrate/add_omniauth_to_users.rb.example +16 -0
  17. data/examples/integration_test_flow.rb +1334 -0
  18. data/examples/jobs/README.md +194 -0
  19. data/examples/jobs/federation_cache_refresh_job.rb.example +78 -0
  20. data/examples/jobs/federation_files_generation_job.rb.example +87 -0
  21. data/examples/mock_op_server.rb +775 -0
  22. data/examples/mock_rp_server.rb +435 -0
  23. data/lib/omniauth_openid_federation/access_token.rb +504 -0
  24. data/lib/omniauth_openid_federation/cache.rb +39 -0
  25. data/lib/omniauth_openid_federation/cache_adapter.rb +173 -0
  26. data/lib/omniauth_openid_federation/configuration.rb +135 -0
  27. data/lib/omniauth_openid_federation/constants.rb +13 -0
  28. data/lib/omniauth_openid_federation/endpoint_resolver.rb +168 -0
  29. data/lib/omniauth_openid_federation/entity_statement_reader.rb +122 -0
  30. data/lib/omniauth_openid_federation/errors.rb +52 -0
  31. data/lib/omniauth_openid_federation/federation/entity_statement.rb +331 -0
  32. data/lib/omniauth_openid_federation/federation/entity_statement_builder.rb +188 -0
  33. data/lib/omniauth_openid_federation/federation/entity_statement_fetcher.rb +142 -0
  34. data/lib/omniauth_openid_federation/federation/entity_statement_helper.rb +87 -0
  35. data/lib/omniauth_openid_federation/federation/entity_statement_parser.rb +198 -0
  36. data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +502 -0
  37. data/lib/omniauth_openid_federation/federation/metadata_policy_merger.rb +276 -0
  38. data/lib/omniauth_openid_federation/federation/signed_jwks.rb +210 -0
  39. data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +225 -0
  40. data/lib/omniauth_openid_federation/federation_endpoint.rb +949 -0
  41. data/lib/omniauth_openid_federation/http_client.rb +70 -0
  42. data/lib/omniauth_openid_federation/instrumentation.rb +383 -0
  43. data/lib/omniauth_openid_federation/jwks/cache.rb +76 -0
  44. data/lib/omniauth_openid_federation/jwks/decode.rb +174 -0
  45. data/lib/omniauth_openid_federation/jwks/fetch.rb +153 -0
  46. data/lib/omniauth_openid_federation/jwks/normalizer.rb +49 -0
  47. data/lib/omniauth_openid_federation/jwks/rotate.rb +97 -0
  48. data/lib/omniauth_openid_federation/jwks/selector.rb +101 -0
  49. data/lib/omniauth_openid_federation/jws.rb +416 -0
  50. data/lib/omniauth_openid_federation/key_extractor.rb +173 -0
  51. data/lib/omniauth_openid_federation/logger.rb +99 -0
  52. data/lib/omniauth_openid_federation/rack_endpoint.rb +187 -0
  53. data/lib/omniauth_openid_federation/railtie.rb +29 -0
  54. data/lib/omniauth_openid_federation/rate_limiter.rb +55 -0
  55. data/lib/omniauth_openid_federation/strategy.rb +2029 -0
  56. data/lib/omniauth_openid_federation/string_helpers.rb +30 -0
  57. data/lib/omniauth_openid_federation/tasks_helper.rb +428 -0
  58. data/lib/omniauth_openid_federation/utils.rb +166 -0
  59. data/lib/omniauth_openid_federation/validators.rb +126 -0
  60. data/lib/omniauth_openid_federation/version.rb +3 -0
  61. data/lib/omniauth_openid_federation.rb +98 -0
  62. data/lib/tasks/omniauth_openid_federation.rake +376 -0
  63. data/sig/federation.rbs +218 -0
  64. data/sig/jwks.rbs +63 -0
  65. data/sig/omniauth_openid_federation.rbs +254 -0
  66. data/sig/strategy.rbs +60 -0
  67. metadata +352 -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,33 @@
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_before_action :authenticate_user!, only: [:openid_federation, :failure]
6
+
7
+ def openid_federation
8
+ auth = request.env["omniauth.auth"]
9
+
10
+ @user = User.find_or_create_from_omniauth(auth)
11
+
12
+ if @user&.persisted?
13
+ sign_in_and_redirect @user, event: :authentication
14
+ else
15
+ redirect_to root_path, alert: "Authentication failed"
16
+ end
17
+ end
18
+
19
+ def failure
20
+ error_type = request.env["omniauth.error.type"] || :unknown
21
+ error_message = request.env["omniauth.error"]&.message || "Authentication failed"
22
+
23
+ Rails.logger.error({
24
+ message: "OmniAuth authentication failure",
25
+ error_type: error_type,
26
+ error_message: error_message,
27
+ strategy: request.env["omniauth.strategy"]&.name
28
+ })
29
+
30
+ redirect_to root_path, alert: "Authentication failed: #{error_type}"
31
+ end
32
+ end
33
+
@@ -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,97 @@
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
+ config.omniauth :openid_federation,
76
+ name: :openid_federation,
77
+ scope: [:openid],
78
+ response_type: "code",
79
+ discovery: true,
80
+ issuer: provider_issuer,
81
+ client_auth_method: :jwt_bearer,
82
+ client_signing_alg: :RS256,
83
+ audience: endpoints[:audience],
84
+ entity_statement_path: entity_statement_path.to_s,
85
+ client_options: {
86
+ identifier: client_id,
87
+ redirect_uri: redirect_uri,
88
+ private_key: private_key,
89
+ scheme: URI.parse(provider_issuer).scheme,
90
+ host: URI.parse(provider_issuer).host,
91
+ authorization_endpoint: endpoints[:authorization_endpoint],
92
+ token_endpoint: endpoints[:token_endpoint],
93
+ userinfo_endpoint: endpoints[:userinfo_endpoint],
94
+ jwks_uri: endpoints[:jwks_uri]
95
+ }
96
+ end
97
+
@@ -0,0 +1,206 @@
1
+ # Federation Endpoint Configuration
2
+ # This enables publishing an entity statement at /.well-known/openid-federation
3
+ # Required for OpenID Federation 1.0 compliance with signed JWKS support
4
+ #
5
+ # The entity statement is a self-signed JWT that contains:
6
+ # - Entity metadata (endpoints, configuration)
7
+ # - JWKS for signature validation (both signing and encryption keys)
8
+ # - Issuer and subject information
9
+ #
10
+ # Supports two entity types:
11
+ # - openid_relying_party (RP): For clients/relying parties (PRIMARY USE CASE)
12
+ # - openid_provider (OP): For providers/servers (secondary use case)
13
+ #
14
+ # Automatic Key Provisioning:
15
+ # - Extracts JWKS from entity_statement_path if provided (cached, supports key rotation)
16
+ # - Supports separate signing_key and encryption_key (RECOMMENDED for production)
17
+ # - Falls back to single private_key (DEV/TESTING ONLY - not recommended for production)
18
+ # - Automatically generates both signing and encryption keys from provided keys
19
+
20
+ require "omniauth_openid_federation"
21
+
22
+ # ============================================================================
23
+ # Global Configuration (Optional but Recommended)
24
+ # ============================================================================
25
+ OmniauthOpenidFederation.configure do |config|
26
+ # Security instrumentation - get notified about security events, MITM attacks, etc.
27
+ # Example with Sentry:
28
+ # config.instrumentation = ->(event, data) do
29
+ # Sentry.capture_message(
30
+ # "OpenID Federation: #{event}",
31
+ # level: data[:severity] == :error ? :error : :warning,
32
+ # extra: data
33
+ # )
34
+ # end
35
+
36
+ # Example with Honeybadger:
37
+ # config.instrumentation = ->(event, data) do
38
+ # Honeybadger.notify("OpenID Federation: #{event}", context: data)
39
+ # end
40
+
41
+ # Example with custom logger:
42
+ # config.instrumentation = ->(event, data) do
43
+ # Rails.logger.warn("[Security] #{event}: #{data.inspect}")
44
+ # end
45
+
46
+ # Cache configuration (optional)
47
+ # config.cache_ttl = 3600 # Refresh provider keys every hour
48
+ # config.rotate_on_errors = true # Auto-handle provider key rotation
49
+ end
50
+
51
+ # ============================================================================
52
+ # EXAMPLE 1: Relying Party (RP) Configuration (PRIMARY USE CASE)
53
+ # ============================================================================
54
+ # For client applications that authenticate users via OpenID Federation
55
+
56
+ app_url = ENV["APP_URL"] || "https://your-app.example.com"
57
+
58
+ # Production Setup (RECOMMENDED): Separate signing and encryption keys
59
+ signing_key_path = Rails.root.join("config", "client-signing-private-key.pem")
60
+ encryption_key_path = Rails.root.join("config", "client-encryption-private-key.pem")
61
+
62
+ if File.exist?(signing_key_path) && File.exist?(encryption_key_path)
63
+ # Production: Use separate keys
64
+ signing_key = OpenSSL::PKey::RSA.new(File.read(signing_key_path))
65
+ encryption_key = OpenSSL::PKey::RSA.new(File.read(encryption_key_path))
66
+
67
+ OmniauthOpenidFederation::FederationEndpoint.auto_configure(
68
+ issuer: app_url,
69
+ signing_key: signing_key,
70
+ encryption_key: encryption_key,
71
+ entity_statement_path: Rails.root.join("config", "client-entity-statement.jwt"), # Cache for key rotation
72
+ metadata: {
73
+ openid_relying_party: {
74
+ redirect_uris: [
75
+ "#{app_url}/users/auth/openid_federation/callback"
76
+ ],
77
+ client_registration_types: ["automatic"],
78
+ application_type: "web",
79
+ grant_types: ["authorization_code"],
80
+ response_types: ["code"],
81
+ token_endpoint_auth_method: "private_key_jwt",
82
+ token_endpoint_auth_signing_alg: "RS256",
83
+ request_object_signing_alg: "RS256",
84
+ id_token_encrypted_response_alg: "RSA-OAEP",
85
+ id_token_encrypted_response_enc: "A128CBC-HS256"
86
+ }
87
+ },
88
+ expiration_seconds: (ENV["FEDERATION_EXPIRATION_SECONDS"] || 86400).to_i,
89
+ jwks_cache_ttl: (ENV["FEDERATION_JWKS_CACHE_TTL"] || 3600).to_i,
90
+ auto_provision_keys: true
91
+ )
92
+ else
93
+ # Development/Testing (NOT RECOMMENDED FOR PRODUCTION): Single private key
94
+ private_key_path = Rails.root.join("config", "client-private-key.pem")
95
+ unless File.exist?(private_key_path)
96
+ Rails.logger.warn "[FederationEndpoint] Private key not found at #{private_key_path}. Generate it first."
97
+ Rails.logger.warn " Run: bundle exec rake omniauth_openid_federation:prepare_client_keys"
98
+ next
99
+ end
100
+ private_key = OpenSSL::PKey::RSA.new(File.read(private_key_path))
101
+
102
+ OmniauthOpenidFederation::FederationEndpoint.auto_configure(
103
+ issuer: app_url,
104
+ private_key: private_key, # DEV/TESTING ONLY - not recommended for production
105
+ entity_statement_path: Rails.root.join("config", "client-entity-statement.jwt"),
106
+ metadata: {
107
+ openid_relying_party: {
108
+ redirect_uris: [
109
+ "#{app_url}/users/auth/openid_federation/callback"
110
+ ],
111
+ client_registration_types: ["automatic"],
112
+ application_type: "web",
113
+ grant_types: ["authorization_code"],
114
+ response_types: ["code"],
115
+ token_endpoint_auth_method: "private_key_jwt",
116
+ token_endpoint_auth_signing_alg: "RS256",
117
+ request_object_signing_alg: "RS256",
118
+ id_token_encrypted_response_alg: "RSA-OAEP",
119
+ id_token_encrypted_response_enc: "A128CBC-HS256"
120
+ }
121
+ },
122
+ expiration_seconds: (ENV["FEDERATION_EXPIRATION_SECONDS"] || 86400).to_i,
123
+ jwks_cache_ttl: (ENV["FEDERATION_JWKS_CACHE_TTL"] || 3600).to_i,
124
+ auto_provision_keys: true
125
+ )
126
+ end
127
+
128
+ # ============================================================================
129
+ # EXAMPLE 2: OpenID Provider (OP) Configuration (SECONDARY USE CASE)
130
+ # ============================================================================
131
+ # For provider/server applications that serve authentication
132
+ # Uncomment and configure if you're building a provider:
133
+
134
+ # Production Setup (RECOMMENDED): Separate signing and encryption keys
135
+ # provider_url = ENV["PROVIDER_URL"] || "https://provider.example.com"
136
+ #
137
+ # signing_key = OpenSSL::PKey::RSA.new(File.read("config/provider-signing-key.pem"))
138
+ # encryption_key = OpenSSL::PKey::RSA.new(File.read("config/provider-encryption-key.pem"))
139
+ #
140
+ # OmniauthOpenidFederation::FederationEndpoint.auto_configure(
141
+ # issuer: provider_url,
142
+ # signing_key: signing_key,
143
+ # encryption_key: encryption_key,
144
+ # entity_statement_path: Rails.root.join("config", "provider-entity-statement.jwt"),
145
+ # metadata: {
146
+ # openid_provider: {
147
+ # issuer: provider_url,
148
+ # authorization_endpoint: "#{provider_url}/oauth2/authorize",
149
+ # token_endpoint: "#{provider_url}/oauth2/token",
150
+ # userinfo_endpoint: "#{provider_url}/oauth2/userinfo",
151
+ # jwks_uri: "#{provider_url}/.well-known/jwks.json",
152
+ # signed_jwks_uri: "#{provider_url}/.well-known/signed-jwks.json",
153
+ # federation_fetch_endpoint: "#{provider_url}/.well-known/openid-federation/fetch" # Auto-added for OPs
154
+ # }
155
+ # },
156
+ # expiration_seconds: (ENV["FEDERATION_EXPIRATION_SECONDS"] || 86400).to_i,
157
+ # jwks_cache_ttl: (ENV["FEDERATION_JWKS_CACHE_TTL"] || 3600).to_i,
158
+ # auto_provision_keys: true
159
+ # )
160
+
161
+ # Development/Testing (NOT RECOMMENDED FOR PRODUCTION): Single private key
162
+ # provider_url = ENV["PROVIDER_URL"] || "https://provider.example.com"
163
+ # private_key = OpenSSL::PKey::RSA.new(File.read("config/provider-private-key.pem"))
164
+ #
165
+ # OmniauthOpenidFederation::FederationEndpoint.auto_configure(
166
+ # issuer: provider_url,
167
+ # private_key: private_key, # DEV/TESTING ONLY - not recommended for production
168
+ # entity_statement_path: Rails.root.join("config", "provider-entity-statement.jwt"),
169
+ # metadata: {
170
+ # openid_provider: {
171
+ # issuer: provider_url,
172
+ # authorization_endpoint: "#{provider_url}/oauth2/authorize",
173
+ # token_endpoint: "#{provider_url}/oauth2/token",
174
+ # userinfo_endpoint: "#{provider_url}/oauth2/userinfo",
175
+ # jwks_uri: "#{provider_url}/.well-known/jwks.json",
176
+ # signed_jwks_uri: "#{provider_url}/.well-known/signed-jwks.json",
177
+ # federation_fetch_endpoint: "#{provider_url}/.well-known/openid-federation/fetch" # Auto-added for OPs
178
+ # }
179
+ # },
180
+ # expiration_seconds: (ENV["FEDERATION_EXPIRATION_SECONDS"] || 86400).to_i,
181
+ # jwks_cache_ttl: (ENV["FEDERATION_JWKS_CACHE_TTL"] || 3600).to_i,
182
+ # auto_provision_keys: true
183
+ # )
184
+
185
+ # ============================================================================
186
+ # Routes Configuration
187
+ # ============================================================================
188
+ # Add to config/routes.rb:
189
+ #
190
+ # OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)
191
+ #
192
+ # This mounts all endpoints:
193
+ # - GET /.well-known/openid-federation (entity statement)
194
+ # - GET /.well-known/openid-federation/fetch (fetch endpoint - OPs only)
195
+ # - GET /.well-known/jwks.json (standard JWKS)
196
+ # - GET /.well-known/signed-jwks.json (signed JWKS)
197
+ #
198
+ # Or manually:
199
+ # get "/.well-known/openid-federation", to: "omniauth_openid_federation/federation#show"
200
+ # get "/.well-known/openid-federation/fetch", to: "omniauth_openid_federation/federation#fetch"
201
+ # get "/.well-known/jwks.json", to: "omniauth_openid_federation/federation#jwks"
202
+ # get "/.well-known/signed-jwks.json", to: "omniauth_openid_federation/federation#signed_jwks"
203
+
204
+ Rails.logger.info "[FederationEndpoint] Configured. Add the route in config/routes.rb:"
205
+ Rails.logger.info " OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)"
206
+