omniauth_openid_federation 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +44 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +922 -0
  5. data/SECURITY.md +28 -0
  6. data/app/controllers/omniauth_openid_federation/federation_controller.rb +160 -0
  7. data/config/routes.rb +17 -0
  8. data/examples/README_INTEGRATION_TESTING.md +399 -0
  9. data/examples/README_MOCK_OP.md +243 -0
  10. data/examples/app/controllers/users/omniauth_callbacks_controller.rb.example +37 -0
  11. data/examples/app/jobs/jwks_rotation_job.rb.example +60 -0
  12. data/examples/app/models/user.rb.example +39 -0
  13. data/examples/config/initializers/devise.rb.example +131 -0
  14. data/examples/config/initializers/federation_endpoint.rb.example +206 -0
  15. data/examples/config/mock_op.yml.example +83 -0
  16. data/examples/config/open_id_connect_config.rb.example +210 -0
  17. data/examples/config/routes.rb.example +12 -0
  18. data/examples/db/migrate/add_omniauth_to_users.rb.example +16 -0
  19. data/examples/integration_test_flow.rb +1334 -0
  20. data/examples/jobs/README.md +194 -0
  21. data/examples/jobs/federation_cache_refresh_job.rb.example +78 -0
  22. data/examples/jobs/federation_files_generation_job.rb.example +87 -0
  23. data/examples/mock_op_server.rb +775 -0
  24. data/examples/mock_rp_server.rb +435 -0
  25. data/lib/omniauth_openid_federation/access_token.rb +504 -0
  26. data/lib/omniauth_openid_federation/cache.rb +39 -0
  27. data/lib/omniauth_openid_federation/cache_adapter.rb +173 -0
  28. data/lib/omniauth_openid_federation/configuration.rb +135 -0
  29. data/lib/omniauth_openid_federation/constants.rb +13 -0
  30. data/lib/omniauth_openid_federation/endpoint_resolver.rb +168 -0
  31. data/lib/omniauth_openid_federation/engine.rb +17 -0
  32. data/lib/omniauth_openid_federation/entity_statement_reader.rb +129 -0
  33. data/lib/omniauth_openid_federation/errors.rb +52 -0
  34. data/lib/omniauth_openid_federation/federation/entity_statement.rb +331 -0
  35. data/lib/omniauth_openid_federation/federation/entity_statement_builder.rb +188 -0
  36. data/lib/omniauth_openid_federation/federation/entity_statement_fetcher.rb +142 -0
  37. data/lib/omniauth_openid_federation/federation/entity_statement_helper.rb +87 -0
  38. data/lib/omniauth_openid_federation/federation/entity_statement_parser.rb +198 -0
  39. data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +502 -0
  40. data/lib/omniauth_openid_federation/federation/metadata_policy_merger.rb +276 -0
  41. data/lib/omniauth_openid_federation/federation/signed_jwks.rb +210 -0
  42. data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +225 -0
  43. data/lib/omniauth_openid_federation/federation_endpoint.rb +949 -0
  44. data/lib/omniauth_openid_federation/http_client.rb +70 -0
  45. data/lib/omniauth_openid_federation/instrumentation.rb +399 -0
  46. data/lib/omniauth_openid_federation/jwks/cache.rb +76 -0
  47. data/lib/omniauth_openid_federation/jwks/decode.rb +175 -0
  48. data/lib/omniauth_openid_federation/jwks/fetch.rb +153 -0
  49. data/lib/omniauth_openid_federation/jwks/normalizer.rb +49 -0
  50. data/lib/omniauth_openid_federation/jwks/rotate.rb +97 -0
  51. data/lib/omniauth_openid_federation/jwks/selector.rb +101 -0
  52. data/lib/omniauth_openid_federation/jws.rb +410 -0
  53. data/lib/omniauth_openid_federation/key_extractor.rb +173 -0
  54. data/lib/omniauth_openid_federation/logger.rb +99 -0
  55. data/lib/omniauth_openid_federation/rack_endpoint.rb +187 -0
  56. data/lib/omniauth_openid_federation/railtie.rb +15 -0
  57. data/lib/omniauth_openid_federation/rate_limiter.rb +55 -0
  58. data/lib/omniauth_openid_federation/strategy.rb +2114 -0
  59. data/lib/omniauth_openid_federation/string_helpers.rb +30 -0
  60. data/lib/omniauth_openid_federation/tasks_helper.rb +428 -0
  61. data/lib/omniauth_openid_federation/utils.rb +168 -0
  62. data/lib/omniauth_openid_federation/validators.rb +126 -0
  63. data/lib/omniauth_openid_federation/version.rb +3 -0
  64. data/lib/omniauth_openid_federation.rb +99 -0
  65. data/lib/tasks/omniauth_openid_federation.rake +376 -0
  66. data/sig/federation.rbs +218 -0
  67. data/sig/jwks.rbs +63 -0
  68. data/sig/omniauth_openid_federation.rbs +254 -0
  69. data/sig/strategy.rbs +60 -0
  70. metadata +361 -0
@@ -0,0 +1,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
+
@@ -0,0 +1,210 @@
1
+ # OpenID Connect Configuration Class
2
+ # This example shows how to structure OpenID Federation configuration
3
+ # using a configuration class pattern (similar to Anyway::Config)
4
+ #
5
+ # Usage:
6
+ # 1. Copy this file to config/configs/open_id_connect_config.rb
7
+ # 2. Customize the configuration attributes for your needs
8
+ # 3. Use it in your initializers (see federation_endpoint.rb.example)
9
+ #
10
+ # Environment Variables:
11
+ # OPEN_ID_CONNECT_ENABLED=true
12
+ # OPEN_ID_CONNECT_CLIENT_ID=your-client-id
13
+ # OPEN_ID_CONNECT_PRIVATE_KEY_PATH=config/client-private-key.pem
14
+ # OPEN_ID_CONNECT_SIGNING_KEY_PATH=config/client-signing-private-key.pem
15
+ # OPEN_ID_CONNECT_ENCRYPTION_KEY_PATH=config/client-encryption-private-key.pem
16
+ # OPEN_ID_CONNECT_REDIRECT_URI=https://your-app.example.com/users/auth/openid_federation/callback
17
+ # OPEN_ID_CONNECT_ENTITY_STATEMENT_URL=https://provider.example.com/.well-known/openid-federation
18
+ # OPEN_ID_CONNECT_CLIENT_ENTITY_STATEMENT_PATH=config/client-entity-statement.jwt
19
+ # OPEN_ID_CONNECT_CLIENT_ENTITY_STATEMENT_URL=https://your-app.example.com/.well-known/openid-federation
20
+ # OPEN_ID_CONNECT_ORGANIZATION_NAME=Your Organization Name
21
+
22
+ require "openssl"
23
+ require "json"
24
+
25
+ # Base configuration class (if using Anyway::Config gem)
26
+ # If not using Anyway::Config, you can use a simpler pattern:
27
+ #
28
+ # class ApplicationConfig
29
+ # class << self
30
+ # delegate_missing_to :instance
31
+ #
32
+ # private
33
+ #
34
+ # def instance
35
+ # @instance ||= new
36
+ # end
37
+ # end
38
+ #
39
+ # def enabled?
40
+ # ["true", "1"].include?(ENV["#{config_name.to_s.upcase}_ENABLED"]&.to_s&.downcase)
41
+ # end
42
+ # end
43
+
44
+ class OpenIdConnectConfig < ApplicationConfig
45
+ config_name :open_id_connect
46
+
47
+ # Configuration attributes
48
+ # These can be set via environment variables with OPEN_ID_CONNECT_ prefix
49
+ # or via YAML files (if using Anyway::Config)
50
+ attr_config :enabled,
51
+ :client_id,
52
+ :private_key_path,
53
+ :private_key_base64,
54
+ :signing_key_path,
55
+ :signing_key_base64,
56
+ :encryption_key_path,
57
+ :encryption_key_base64,
58
+ :redirect_uri,
59
+ :entity_statement_path,
60
+ :entity_statement_url,
61
+ :entity_statement_fingerprint,
62
+ :client_entity_statement_path,
63
+ :client_entity_statement_url,
64
+ :client_entity_identifier,
65
+ :ftn_spname,
66
+ :acr_values,
67
+ :organization_name
68
+
69
+ # Load private key from file or base64 encoded string
70
+ # Used for both signing and encryption (DEV/TESTING ONLY)
71
+ # For production, use separate signing_key and encryption_key
72
+ def private_key
73
+ @private_key ||= begin
74
+ private_key_pem = if private_key_base64.present?
75
+ Base64.decode64(private_key_base64)
76
+ elsif private_key_path.present?
77
+ File.read(Rails.root.join(private_key_path))
78
+ end
79
+
80
+ raise "Private key not found. Set OPEN_ID_CONNECT_PRIVATE_KEY_PATH or OPEN_ID_CONNECT_PRIVATE_KEY_BASE64" if private_key_pem.blank?
81
+
82
+ OpenSSL::PKey::RSA.new(private_key_pem)
83
+ end
84
+ end
85
+
86
+ # Load signing key from file or base64 encoded string
87
+ # RECOMMENDED for production: Use separate signing key
88
+ def signing_key
89
+ return nil if signing_key_path.blank? && signing_key_base64.blank?
90
+
91
+ @signing_key ||= begin
92
+ signing_key_pem = if signing_key_base64.present?
93
+ Base64.decode64(signing_key_base64)
94
+ elsif signing_key_path.present?
95
+ File.read(Rails.root.join(signing_key_path))
96
+ end
97
+
98
+ return nil if signing_key_pem.blank?
99
+
100
+ OpenSSL::PKey::RSA.new(signing_key_pem)
101
+ end
102
+ end
103
+
104
+ # Load encryption key from file or base64 encoded string
105
+ # RECOMMENDED for production: Use separate encryption key
106
+ def encryption_key
107
+ return nil if encryption_key_path.blank? && encryption_key_base64.blank?
108
+
109
+ @encryption_key ||= begin
110
+ encryption_key_pem = if encryption_key_base64.present?
111
+ Base64.decode64(encryption_key_base64)
112
+ elsif encryption_key_path.present?
113
+ File.read(Rails.root.join(encryption_key_path))
114
+ end
115
+
116
+ return nil if encryption_key_pem.blank?
117
+
118
+ OpenSSL::PKey::RSA.new(encryption_key_pem)
119
+ end
120
+ end
121
+
122
+ # Note: client_jwk_signing_key is now automatically extracted by the
123
+ # omniauth_openid_federation gem from client_entity_statement_path or
124
+ # client_entity_statement_url when provided.
125
+ # No manual extraction needed - the library handles this automatically.
126
+
127
+ # OpenID Federation 1.0 Section 9: Entity Configuration endpoint
128
+ # Entity statements MUST be available at /.well-known/openid-federation
129
+ # URL is the source of truth per OpenID Federation spec
130
+ def entity_statement_absolute_path
131
+ return nil if entity_statement_path.blank?
132
+ @entity_statement_absolute_path ||= Rails.root.join(entity_statement_path)
133
+ end
134
+
135
+ # Get provider entity statement URL
136
+ # OpenID Federation 1.0 Section 9: MUST be at /.well-known/openid-federation
137
+ # URL is always required per OpenID Federation specification
138
+ def entity_statement_url
139
+ return values[:entity_statement_url] if values[:entity_statement_url].present?
140
+ nil # Return nil if not configured - must be provided via environment variable
141
+ end
142
+
143
+ # Get client entity statement absolute path
144
+ def client_entity_statement_absolute_path
145
+ return nil if client_entity_statement_path.blank?
146
+ @client_entity_statement_absolute_path ||= Rails.root.join(client_entity_statement_path)
147
+ end
148
+
149
+ # Get client entity statement path for strategy configuration
150
+ # Prefers URL over path - only returns path if URL is not available
151
+ def client_entity_statement_path_for_strategy
152
+ return nil if client_entity_statement_url.present?
153
+ client_entity_statement_absolute_path
154
+ end
155
+
156
+ # Get client registration type based on entity statement configuration
157
+ # :automatic - Uses entity statement for automatic client registration
158
+ # :explicit - Uses explicit client_id and manual registration
159
+ def client_registration_type
160
+ (values[:client_entity_statement_url].present? || values[:client_entity_statement_path].present?) ? :automatic : :explicit
161
+ end
162
+
163
+ # Generate client entity statement URL
164
+ # URL is for external consumers (providers) - we never access it ourselves
165
+ # Uses standard path /.well-known/openid-federation (mounted by library)
166
+ def client_entity_statement_url
167
+ return values[:client_entity_statement_url] if values[:client_entity_statement_url].present?
168
+ # Default to standard federation endpoint if not explicitly configured
169
+ app_url = ENV["APP_URL"] || "https://your-app.example.com"
170
+ "#{app_url}/.well-known/openid-federation"
171
+ end
172
+ end
173
+
174
+ # Usage in initializer (config/initializers/omniauth_openid_federation.rb):
175
+ #
176
+ # open_id_config = OpenIdConnectConfig.new
177
+ # if open_id_config.enabled? && open_id_config.private_key.present?
178
+ # app_url = ENV["APP_URL"] || "https://your-app.example.com"
179
+ #
180
+ # OmniauthOpenidFederation::FederationEndpoint.auto_configure(
181
+ # issuer: app_url,
182
+ # # Production (RECOMMENDED): Use separate keys
183
+ # signing_key: open_id_config.signing_key,
184
+ # encryption_key: open_id_config.encryption_key,
185
+ # # Development/Testing (NOT RECOMMENDED FOR PRODUCTION): Single key
186
+ # # private_key: open_id_config.private_key,
187
+ # entity_statement_path: open_id_config.client_entity_statement_absolute_path,
188
+ # metadata: {
189
+ # openid_relying_party: {
190
+ # redirect_uris: [
191
+ # open_id_config.redirect_uri
192
+ # ],
193
+ # client_registration_types: ["automatic"],
194
+ # application_type: "web",
195
+ # grant_types: ["authorization_code"],
196
+ # response_types: ["code"],
197
+ # token_endpoint_auth_method: "private_key_jwt",
198
+ # token_endpoint_auth_signing_alg: "RS256",
199
+ # request_object_signing_alg: "RS256",
200
+ # id_token_encrypted_response_alg: "RSA-OAEP",
201
+ # id_token_encrypted_response_enc: "A128CBC-HS256",
202
+ # organization_name: open_id_config.organization_name
203
+ # }
204
+ # },
205
+ # expiration_seconds: (ENV["FEDERATION_EXPIRATION_SECONDS"] || 86400).to_i,
206
+ # jwks_cache_ttl: (ENV["FEDERATION_JWKS_CACHE_TTL"] || 3600).to_i,
207
+ # auto_provision_keys: true
208
+ # )
209
+ # end
210
+
@@ -0,0 +1,12 @@
1
+ # Example routes.rb showing how to mount the federation endpoint
2
+ Rails.application.routes.draw do
3
+ # Mount the federation endpoint (required for serving entity statements)
4
+ # This enables the /.well-known/openid-federation endpoint
5
+ OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)
6
+
7
+ # Or mount manually:
8
+ # get "/.well-known/openid-federation", to: "omniauth_openid_federation/federation#show", as: :openid_federation
9
+
10
+ # Your other routes...
11
+ end
12
+
@@ -0,0 +1,16 @@
1
+ # Example migration for adding OmniAuth fields to users table
2
+ # Copy this to db/migrate/XXXXXX_add_omniauth_to_users.rb
3
+
4
+ class AddOmniauthToUsers < ActiveRecord::Migration[7.0]
5
+ def change
6
+ add_column :users, :provider, :string
7
+ add_column :users, :uid, :string
8
+ add_index :users, [:provider, :uid], unique: true
9
+
10
+ # Optional: add user info fields if not already present
11
+ add_column :users, :name, :string unless column_exists?(:users, :name)
12
+ add_column :users, :first_name, :string unless column_exists?(:users, :first_name)
13
+ add_column :users, :last_name, :string unless column_exists?(:users, :last_name)
14
+ end
15
+ end
16
+