omniauth_openid_federation 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of omniauth_openid_federation might be problematic. Click here for more details.

Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +36 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +922 -0
  5. data/SECURITY.md +28 -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 +37 -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 +131 -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/engine.rb +21 -0
  30. data/lib/omniauth_openid_federation/entity_statement_reader.rb +129 -0
  31. data/lib/omniauth_openid_federation/errors.rb +52 -0
  32. data/lib/omniauth_openid_federation/federation/entity_statement.rb +331 -0
  33. data/lib/omniauth_openid_federation/federation/entity_statement_builder.rb +188 -0
  34. data/lib/omniauth_openid_federation/federation/entity_statement_fetcher.rb +142 -0
  35. data/lib/omniauth_openid_federation/federation/entity_statement_helper.rb +87 -0
  36. data/lib/omniauth_openid_federation/federation/entity_statement_parser.rb +198 -0
  37. data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +502 -0
  38. data/lib/omniauth_openid_federation/federation/metadata_policy_merger.rb +276 -0
  39. data/lib/omniauth_openid_federation/federation/signed_jwks.rb +210 -0
  40. data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +225 -0
  41. data/lib/omniauth_openid_federation/federation_endpoint.rb +949 -0
  42. data/lib/omniauth_openid_federation/http_client.rb +70 -0
  43. data/lib/omniauth_openid_federation/instrumentation.rb +399 -0
  44. data/lib/omniauth_openid_federation/jwks/cache.rb +76 -0
  45. data/lib/omniauth_openid_federation/jwks/decode.rb +175 -0
  46. data/lib/omniauth_openid_federation/jwks/fetch.rb +153 -0
  47. data/lib/omniauth_openid_federation/jwks/normalizer.rb +49 -0
  48. data/lib/omniauth_openid_federation/jwks/rotate.rb +97 -0
  49. data/lib/omniauth_openid_federation/jwks/selector.rb +101 -0
  50. data/lib/omniauth_openid_federation/jws.rb +410 -0
  51. data/lib/omniauth_openid_federation/key_extractor.rb +173 -0
  52. data/lib/omniauth_openid_federation/logger.rb +99 -0
  53. data/lib/omniauth_openid_federation/rack_endpoint.rb +187 -0
  54. data/lib/omniauth_openid_federation/railtie.rb +15 -0
  55. data/lib/omniauth_openid_federation/rate_limiter.rb +55 -0
  56. data/lib/omniauth_openid_federation/strategy.rb +2114 -0
  57. data/lib/omniauth_openid_federation/string_helpers.rb +30 -0
  58. data/lib/omniauth_openid_federation/tasks_helper.rb +428 -0
  59. data/lib/omniauth_openid_federation/utils.rb +168 -0
  60. data/lib/omniauth_openid_federation/validators.rb +126 -0
  61. data/lib/omniauth_openid_federation/version.rb +3 -0
  62. data/lib/omniauth_openid_federation.rb +99 -0
  63. data/lib/tasks/omniauth_openid_federation.rake +376 -0
  64. data/sig/federation.rbs +218 -0
  65. data/sig/jwks.rbs +63 -0
  66. data/sig/omniauth_openid_federation.rbs +254 -0
  67. data/sig/strategy.rbs +60 -0
  68. metadata +359 -0
@@ -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
+
@@ -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
+
@@ -0,0 +1,83 @@
1
+ # Mock OpenID Provider (OP) Server Configuration
2
+ #
3
+ # Copy this file to config/mock_op.yml and customize for your testing needs
4
+
5
+ # Entity Identifier (Entity ID) for the OP
6
+ entity_id: "https://op.example.com"
7
+
8
+ # Server host and port
9
+ server_host: "localhost:9292"
10
+
11
+ # Private key for signing entity statements and ID tokens
12
+ # Can be:
13
+ # - PEM format (with BEGIN/END markers)
14
+ # - Base64 encoded PEM
15
+ # - Path to key file (not recommended for production)
16
+ # If not provided, a new key will be generated (for testing only)
17
+ signing_key: |
18
+ -----BEGIN RSA PRIVATE KEY-----
19
+ MIIEpAIBAAKCAQEA...
20
+ -----END RSA PRIVATE KEY-----
21
+
22
+ # Trust Anchors for validating incoming RPs
23
+ # Each trust anchor must have:
24
+ # - entity_id: Trust Anchor Entity Identifier
25
+ # - jwks: Trust Anchor JWKS for validation
26
+ trust_anchors:
27
+ - entity_id: "https://ta.example.com"
28
+ jwks:
29
+ keys:
30
+ - kty: "RSA"
31
+ use: "sig"
32
+ kid: "ta-key-1"
33
+ n: "..."
34
+ e: "AQAB"
35
+
36
+ # Authority hints (for OP's Entity Configuration)
37
+ # List of Immediate Superiors' Entity Identifiers
38
+ # Leave empty if this OP is a Trust Anchor
39
+ authority_hints:
40
+ - "https://federation.example.com"
41
+
42
+ # OP Metadata (OpenID Provider metadata)
43
+ op_metadata:
44
+ issuer: "https://op.example.com"
45
+ authorization_endpoint: "https://op.example.com/auth"
46
+ token_endpoint: "https://op.example.com/token"
47
+ userinfo_endpoint: "https://op.example.com/userinfo"
48
+ jwks_uri: "https://op.example.com/.well-known/jwks.json"
49
+ signed_jwks_uri: "https://op.example.com/.well-known/signed-jwks.json"
50
+ client_registration_types_supported:
51
+ - "automatic"
52
+ - "explicit"
53
+ response_types_supported:
54
+ - "code"
55
+ grant_types_supported:
56
+ - "authorization_code"
57
+ id_token_signing_alg_values_supported:
58
+ - "RS256"
59
+ scopes_supported:
60
+ - "openid"
61
+ - "profile"
62
+ - "email"
63
+
64
+ # Subordinate Statements (for Fetch Endpoint)
65
+ # Maps subject Entity IDs to their Subordinate Statement configuration
66
+ subordinate_statements:
67
+ "https://rp.example.com":
68
+ metadata:
69
+ openid_relying_party:
70
+ redirect_uris:
71
+ - "https://rp.example.com/callback"
72
+ client_registration_types:
73
+ - "automatic"
74
+ application_type: "web"
75
+ metadata_policy:
76
+ openid_relying_party:
77
+ grant_types:
78
+ subset_of:
79
+ - "authorization_code"
80
+ response_types:
81
+ subset_of:
82
+ - "code"
83
+