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
data/SECURITY.md ADDED
@@ -0,0 +1,28 @@
1
+ # SECURITY
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ **Do NOT** open a public GitHub issue for security vulnerabilities.
6
+
7
+ Email security details to: **security@kiskolabs.com**
8
+
9
+ Include: description, steps to reproduce, potential impact, and suggested fix (if available).
10
+
11
+ ### Response Timeline
12
+
13
+ - We will acknowledge receipt of your report
14
+ - We will provide an initial assessment
15
+ - We will keep you informed of our progress and resolution timeline
16
+
17
+ ### Disclosure Policy
18
+
19
+ - We will work with you to understand and resolve the issue
20
+ - We will credit you for the discovery (unless you prefer to remain anonymous)
21
+ - We will publish a security advisory after the vulnerability is patched
22
+ - We will coordinate public disclosure with you
23
+
24
+ ## Automation Security
25
+
26
+ * **Context Isolation:** It is strictly forbidden to include production credentials, API keys, or Personally Identifiable Information (PII) in prompts sent to third-party LLMs or automation services.
27
+
28
+ * **Supply Chain:** All automated dependencies must be verified.
@@ -0,0 +1,160 @@
1
+ # Federation Controller for serving entity statements and JWKS
2
+ # @see https://openid.net/specs/openid-federation-1_0.html OpenID Federation 1.0 Specification
3
+ #
4
+ # Serves four endpoints:
5
+ # - /.well-known/openid-federation - Entity statement JWT
6
+ # - /.well-known/openid-federation/fetch - Fetch endpoint for Subordinate Statements
7
+ # - /.well-known/jwks.json - Standard JWKS (JSON)
8
+ # - /.well-known/signed-jwks.json - Signed JWKS (JWT)
9
+ #
10
+ # This controller is automatically available when the gem is used in a Rails application.
11
+ # Uses Rails-conventional naming (OmniauthOpenidFederation) to match natural inflection
12
+ require "omniauth_openid_federation/cache_adapter"
13
+
14
+ module OmniauthOpenidFederation
15
+ class FederationController < ActionController::Base
16
+ # Serve the entity statement
17
+ #
18
+ # GET /.well-known/openid-federation
19
+ #
20
+ # Returns the entity statement JWT as plain text with appropriate content type.
21
+ def show
22
+ entity_statement = OmniauthOpenidFederation::FederationEndpoint.generate_entity_statement
23
+
24
+ # Set appropriate headers for entity statement
25
+ # Per OpenID Federation 1.0 Section 9.2, MUST use application/entity-statement+jwt
26
+ response.headers["Content-Type"] = "application/entity-statement+jwt"
27
+ response.headers["Cache-Control"] = "public, max-age=3600" # Cache for 1 hour
28
+
29
+ render plain: entity_statement
30
+ rescue OmniauthOpenidFederation::ConfigurationError => e
31
+ OmniauthOpenidFederation::Logger.error("[FederationController] Configuration error: #{e.message}")
32
+ render plain: "Federation endpoint not configured", status: :service_unavailable
33
+ rescue => e
34
+ OmniauthOpenidFederation::Logger.error("[FederationController] Error generating entity statement: #{e.class} - #{e.message}")
35
+ render plain: "Internal server error", status: :internal_server_error
36
+ end
37
+
38
+ # Serve fetch endpoint (for Subordinate Statements)
39
+ #
40
+ # GET /.well-known/openid-federation/fetch?sub=<subject_entity_id>
41
+ #
42
+ # Returns a Subordinate Statement JWT for the specified subject entity.
43
+ # Per OpenID Federation 1.0 Section 6.1.
44
+ def fetch
45
+ # Extract 'sub' query parameter (required per spec)
46
+ subject_entity_id = params[:sub]
47
+
48
+ unless subject_entity_id
49
+ render json: {error: "invalid_request", error_description: "Missing required parameter: sub"}, status: :bad_request
50
+ return
51
+ end
52
+
53
+ # Validate that subject is not the issuer (invalid request per spec)
54
+ config = OmniauthOpenidFederation::FederationEndpoint.configuration
55
+ if subject_entity_id == config.issuer
56
+ render json: {error: "invalid_request", error_description: "Subject cannot be the issuer"}, status: :bad_request
57
+ return
58
+ end
59
+
60
+ # Get Subordinate Statement
61
+ subordinate_statement = OmniauthOpenidFederation::FederationEndpoint.get_subordinate_statement(subject_entity_id)
62
+
63
+ unless subordinate_statement
64
+ render json: {error: "not_found", error_description: "Subordinate Statement not found for subject: #{subject_entity_id}"}, status: :not_found
65
+ return
66
+ end
67
+
68
+ # Set appropriate headers per spec (application/entity-statement+jwt)
69
+ response.headers["Content-Type"] = "application/entity-statement+jwt"
70
+ response.headers["Cache-Control"] = "public, max-age=3600" # Cache for 1 hour
71
+
72
+ render plain: subordinate_statement
73
+ rescue OmniauthOpenidFederation::ConfigurationError => e
74
+ OmniauthOpenidFederation::Logger.error("[FederationController] Configuration error: #{e.message}")
75
+ render json: {error: "Federation endpoint not configured"}, status: :service_unavailable
76
+ rescue => e
77
+ OmniauthOpenidFederation::Logger.error("[FederationController] Error fetching subordinate statement: #{e.class} - #{e.message}")
78
+ render json: {error: "Internal server error"}, status: :internal_server_error
79
+ end
80
+
81
+ # Serve standard JWKS
82
+ #
83
+ # GET /.well-known/jwks.json
84
+ #
85
+ # Returns the current JWKS in JSON format.
86
+ # Uses config.current_jwks or config.current_jwks_proc if configured,
87
+ # otherwise falls back to entity statement JWKS.
88
+ def jwks
89
+ config = OmniauthOpenidFederation::FederationEndpoint.configuration
90
+
91
+ # Get current JWKS (may differ from entity statement JWKS)
92
+ jwks_to_serve = OmniauthOpenidFederation::FederationEndpoint.current_jwks
93
+
94
+ # Apply server-side caching if available
95
+ cache_key = "federation:jwks:#{Digest::SHA256.hexdigest(jwks_to_serve.to_json)}"
96
+ cache_ttl = config.jwks_cache_ttl || 3600
97
+
98
+ jwks_json = if OmniauthOpenidFederation::CacheAdapter.available?
99
+ OmniauthOpenidFederation::CacheAdapter.fetch(cache_key, expires_in: cache_ttl) do
100
+ jwks_to_serve.to_json
101
+ end
102
+ else
103
+ jwks_to_serve.to_json
104
+ end
105
+
106
+ response.headers["Content-Type"] = "application/json"
107
+ response.headers["Cache-Control"] = "public, max-age=3600" # Client-side cache for 1 hour
108
+
109
+ render json: JSON.parse(jwks_json)
110
+ rescue OmniauthOpenidFederation::ConfigurationError => e
111
+ OmniauthOpenidFederation::Logger.error("[FederationController] Configuration error: #{e.message}")
112
+ render json: {error: "Federation endpoint not configured"}, status: :service_unavailable
113
+ rescue => e
114
+ OmniauthOpenidFederation::Logger.error("[FederationController] Error serving JWKS: #{e.class} - #{e.message}")
115
+ render json: {error: "Internal server error"}, status: :internal_server_error
116
+ end
117
+
118
+ # Serve signed JWKS
119
+ #
120
+ # GET /.well-known/signed-jwks.json
121
+ #
122
+ # Returns the current JWKS wrapped in a JWT, signed with entity statement key.
123
+ # Note: Despite the .json extension (per OpenID Federation spec), this endpoint returns
124
+ # application/jwt (a JWT), not plain JSON. The Content-Type header correctly indicates application/jwt.
125
+ # Uses config.signed_jwks_payload or config.signed_jwks_payload_proc if configured,
126
+ # otherwise falls back to entity statement JWKS.
127
+ def signed_jwks
128
+ config = OmniauthOpenidFederation::FederationEndpoint.configuration
129
+
130
+ # Generate signed JWKS JWT
131
+ signed_jwks_jwt = OmniauthOpenidFederation::FederationEndpoint.generate_signed_jwks
132
+
133
+ # Apply server-side caching if available
134
+ cache_key = "federation:signed_jwks:#{Digest::SHA256.hexdigest(signed_jwks_jwt)}"
135
+ cache_ttl = config.jwks_cache_ttl || 3600
136
+
137
+ cached_jwt = if OmniauthOpenidFederation::CacheAdapter.available?
138
+ OmniauthOpenidFederation::CacheAdapter.fetch(cache_key, expires_in: cache_ttl) do
139
+ signed_jwks_jwt
140
+ end
141
+ else
142
+ signed_jwks_jwt
143
+ end
144
+
145
+ response.headers["Content-Type"] = "application/jwt"
146
+ response.headers["Cache-Control"] = "public, max-age=3600" # Client-side cache for 1 hour
147
+
148
+ render plain: cached_jwt
149
+ rescue OmniauthOpenidFederation::ConfigurationError => e
150
+ OmniauthOpenidFederation::Logger.error("[FederationController] Configuration error: #{e.message}")
151
+ render plain: "Federation endpoint not configured", status: :service_unavailable
152
+ rescue OmniauthOpenidFederation::SignatureError => e
153
+ OmniauthOpenidFederation::Logger.error("[FederationController] Signature error: #{e.message}")
154
+ render plain: "Internal server error", status: :internal_server_error
155
+ rescue => e
156
+ OmniauthOpenidFederation::Logger.error("[FederationController] Error generating signed JWKS: #{e.class} - #{e.message}")
157
+ render plain: "Internal server error", status: :internal_server_error
158
+ end
159
+ end
160
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,17 @@
1
+ # Routes for OpenID Federation well-known endpoints
2
+ # These routes are mounted at the root level (not namespaced) because
3
+ # OpenID Federation spec requires specific well-known paths
4
+ OmniauthOpenidFederation::Engine.routes.draw do
5
+ # OpenID Federation 1.0 Section 9: Entity Configuration endpoint
6
+ # MUST be at /.well-known/openid-federation
7
+ get "/.well-known/openid-federation", to: "omniauth_openid_federation/federation#show", as: :openid_federation
8
+
9
+ # Fetch endpoint for Subordinate Statements (Section 6.1)
10
+ get "/.well-known/openid-federation/fetch", to: "omniauth_openid_federation/federation#fetch", as: :openid_federation_fetch
11
+
12
+ # Standard JWKS endpoint
13
+ get "/.well-known/jwks.json", to: "omniauth_openid_federation/federation#jwks", as: :openid_federation_jwks
14
+
15
+ # Signed JWKS endpoint (OpenID Federation requirement)
16
+ get "/.well-known/signed-jwks.json", to: "omniauth_openid_federation/federation#signed_jwks", as: :openid_federation_signed_jwks
17
+ end
@@ -0,0 +1,399 @@
1
+ # OpenID Federation Integration Testing
2
+
3
+ This directory contains comprehensive mock servers and integration tests for the complete OpenID Federation flow.
4
+
5
+ ## Architecture
6
+
7
+ The integration test setup consists of:
8
+
9
+ 1. **Mock OP Server** (`mock_op_server.rb`) - Simulates an OpenID Provider
10
+ 2. **Mock RP Server** (`mock_rp_server.rb`) - Simulates a Relying Party (Client)
11
+ 3. **Integration Test Flow** (`integration_test_flow.rb`) - Automated tests for the complete flow
12
+
13
+ ## Complete OpenID Federation Flow
14
+
15
+ ### 1. Provider Exposes Entity Statement with JWKS
16
+
17
+ The OP server exposes its entity configuration at:
18
+ ```
19
+ GET /.well-known/openid-federation
20
+ ```
21
+
22
+ Returns a signed JWT containing:
23
+ - Provider metadata (authorization_endpoint, token_endpoint, etc.)
24
+ - JWKS (public keys for signing/encryption)
25
+ - Authority hints (if subordinate to a Trust Anchor)
26
+
27
+ ### 2. Client Exposes Entity Statement with JWKS
28
+
29
+ The RP server exposes its entity configuration at:
30
+ ```
31
+ GET /.well-known/openid-federation
32
+ ```
33
+
34
+ Returns a signed JWT containing:
35
+ - RP metadata (redirect_uris, client_name, etc.)
36
+ - JWKS (public keys for signing request objects)
37
+ - Authority hints (if subordinate to a Trust Anchor)
38
+
39
+ ### 3. Client Fetches Provider Statement with Keys
40
+
41
+ When the RP initiates login:
42
+ 1. RP fetches OP's entity statement from `/.well-known/openid-federation`
43
+ 2. RP validates the entity statement signature
44
+ 3. RP extracts OP's JWKS for future token validation
45
+ 4. RP extracts OP's metadata (endpoints, capabilities)
46
+
47
+ ### 4. Client Sends Login Request
48
+
49
+ RP generates a signed request object (JWT) containing:
50
+ - `client_id`: RP's Entity ID
51
+ - `redirect_uri`: Callback URL
52
+ - `scope`: Requested scopes
53
+ - `state`: CSRF protection
54
+ - `nonce`: Replay protection
55
+ - `aud`: OP's Entity ID
56
+
57
+ The request object is:
58
+ - **Always signed** with RP's private key (RFC 9101 requirement)
59
+ - **Optionally encrypted** if OP requires encryption
60
+
61
+ RP redirects to OP's authorization endpoint:
62
+ ```
63
+ GET /auth?request=<signed_jwt>
64
+ ```
65
+
66
+ ### 5. Provider Fetches Client Statement and Keys
67
+
68
+ When OP receives the authorization request:
69
+ 1. OP extracts `client_id` from the request object
70
+ 2. OP fetches RP's entity statement from `/.well-known/openid-federation`
71
+ 3. OP validates RP's entity statement signature
72
+ 4. OP extracts RP's JWKS from the entity statement
73
+ 5. OP validates the request object signature using RP's public key
74
+ 6. OP resolves RP's trust chain (if trust anchors configured)
75
+ 7. OP applies metadata policies from trust chain
76
+ 8. OP validates `redirect_uri` against RP's allowed redirect URIs
77
+
78
+ ### 6. Exchange and Authenticated Login
79
+
80
+ After user authorization:
81
+ 1. OP redirects back to RP with authorization code
82
+ 2. RP exchanges code for tokens at OP's token endpoint
83
+ 3. OP returns ID token (signed with OP's private key)
84
+ 4. RP validates ID token signature using OP's JWKS
85
+ 5. RP validates ID token claims (iss, aud, exp, nonce)
86
+ 6. User is authenticated
87
+
88
+ ## Error Scenarios Supported
89
+
90
+ The mock servers support error injection via `?error_mode=<mode>` parameter:
91
+
92
+ ### Invalid Statement
93
+ ```
94
+ GET /.well-known/openid-federation?error_mode=invalid_statement
95
+ ```
96
+ Returns malformed JWT to test error handling.
97
+
98
+ ### Wrong Keys
99
+ ```
100
+ GET /.well-known/jwks.json?error_mode=wrong_keys
101
+ GET /.well-known/openid-federation?error_mode=wrong_keys
102
+ ```
103
+ Returns JWKS with keys that don't match the signing key.
104
+
105
+ ### Invalid Request
106
+ ```
107
+ GET /auth?error_mode=invalid_request
108
+ ```
109
+ Rejects request object validation to test error handling.
110
+
111
+ ### Invalid Signature
112
+ ```
113
+ GET /.well-known/signed-jwks.json?error_mode=invalid_signature
114
+ ```
115
+ Returns signed JWKS with invalid signature.
116
+
117
+ ### Expired Statement
118
+ ```
119
+ GET /.well-known/openid-federation?error_mode=expired_statement
120
+ ```
121
+ Returns expired entity statement to test expiration handling.
122
+
123
+ ### Missing Metadata
124
+ ```
125
+ GET /.well-known/openid-federation?error_mode=missing_metadata
126
+ ```
127
+ Returns entity statement without metadata to test validation.
128
+
129
+ ## Usage
130
+
131
+ ### Quick Start (Automated - Recommended)
132
+
133
+ The integration test flow can automatically start servers, generate keys, and run all tests:
134
+
135
+ ```bash
136
+ # Single command - fully automated
137
+ ruby examples/integration_test_flow.rb
138
+ ```
139
+
140
+ This will:
141
+ 1. Create temporary directories for keys and configs
142
+ 2. Generate RSA keys for both OP and RP (using rake task logic)
143
+ 3. Configure and start both servers automatically
144
+ 4. Wait for servers to be ready
145
+ 5. Run all integration tests
146
+ 6. Clean up (kill servers, remove tmp dirs) on exit
147
+
148
+ ### Manual Start (For Debugging)
149
+
150
+ If you want to start servers manually for debugging:
151
+
152
+ **Terminal 1 - OP Server:**
153
+ ```bash
154
+ ruby examples/mock_op_server.rb
155
+ # Server runs on http://localhost:9292
156
+ ```
157
+
158
+ **Terminal 2 - RP Server:**
159
+ ```bash
160
+ ruby examples/mock_rp_server.rb
161
+ # Server runs on http://localhost:9293
162
+ ```
163
+
164
+ **Terminal 3 - Integration Tests:**
165
+ ```bash
166
+ # Disable auto-start to use manually started servers
167
+ AUTO_START_SERVERS=false ruby examples/integration_test_flow.rb
168
+ ```
169
+
170
+ ### Environment Variables
171
+
172
+ The integration test flow supports the following environment variables:
173
+
174
+ ```bash
175
+ # Server URLs
176
+ OP_URL=http://localhost:9292 # OP server URL
177
+ RP_URL=http://localhost:9293 # RP server URL
178
+
179
+ # Server Ports
180
+ OP_PORT=9292 # OP server port
181
+ RP_PORT=9293 # RP server port
182
+
183
+ # Entity IDs
184
+ OP_ENTITY_ID=https://op.example.com # OP entity identifier
185
+ RP_ENTITY_ID=https://rp.example.com # RP entity identifier
186
+
187
+ # Temporary Directory
188
+ TMP_DIR=tmp/integration_test # Directory for keys/configs (default: tmp/integration_test)
189
+
190
+ # Auto-start servers (true/false)
191
+ AUTO_START_SERVERS=true # Auto-start servers (default: true)
192
+
193
+ # Cleanup on exit (true/false)
194
+ CLEANUP_ON_EXIT=true # Clean up tmp dirs on exit (default: true)
195
+
196
+ # Key Type
197
+ KEY_TYPE=separate # 'single' or 'separate' (default: separate)
198
+ ```
199
+
200
+ **Example with custom configuration:**
201
+ ```bash
202
+ KEY_TYPE=separate \
203
+ TMP_DIR=/tmp/my_test \
204
+ ruby examples/integration_test_flow.rb
205
+ ```
206
+
207
+ **Note**: By default, the integration test uses localhost URLs for complete isolation:
208
+ - No DNS resolution required
209
+ - No external network dependencies
210
+ - All communication happens on localhost
211
+ - Entity IDs default to `http://localhost:9292` (OP) and `http://localhost:9293` (RP)
212
+
213
+ This ensures the tests work in any environment without network configuration.
214
+
215
+ ### Manual Testing
216
+
217
+ **1. Test Provider Entity Statement:**
218
+ ```bash
219
+ curl http://localhost:9292/.well-known/openid-federation
220
+ ```
221
+
222
+ **2. Test Client Entity Statement:**
223
+ ```bash
224
+ curl http://localhost:9293/.well-known/openid-federation
225
+ ```
226
+
227
+ **3. Test Login Flow:**
228
+ ```bash
229
+ # Initiate login (will redirect to OP)
230
+ curl -L "http://localhost:9293/login?provider=https://op.example.com"
231
+ ```
232
+
233
+ **4. Test Error Scenarios:**
234
+ ```bash
235
+ # Invalid statement
236
+ curl "http://localhost:9292/.well-known/openid-federation?error_mode=invalid_statement"
237
+
238
+ # Wrong keys
239
+ curl "http://localhost:9292/.well-known/jwks.json?error_mode=wrong_keys"
240
+
241
+ # Expired statement
242
+ curl "http://localhost:9292/.well-known/openid-federation?error_mode=expired_statement"
243
+ ```
244
+
245
+ ## Configuration
246
+
247
+ ### OP Server Configuration
248
+
249
+ Create `examples/config/mock_op.yml`:
250
+
251
+ ```yaml
252
+ entity_id: "https://op.example.com"
253
+ server_host: "localhost:9292"
254
+ signing_key: |
255
+ -----BEGIN RSA PRIVATE KEY-----
256
+ ...
257
+ -----END RSA PRIVATE KEY-----
258
+ encryption_key: | # Optional, defaults to signing_key
259
+ -----BEGIN RSA PRIVATE KEY-----
260
+ ...
261
+ -----END RSA PRIVATE KEY-----
262
+ trust_anchors:
263
+ - entity_id: "https://ta.example.com"
264
+ jwks:
265
+ keys:
266
+ - kty: "RSA"
267
+ kid: "ta-key-1"
268
+ use: "sig"
269
+ n: "..."
270
+ e: "AQAB"
271
+ authority_hints: # Optional, if OP is subordinate
272
+ - "https://ta.example.com"
273
+ op_metadata:
274
+ issuer: "https://op.example.com"
275
+ authorization_endpoint: "https://op.example.com/auth"
276
+ token_endpoint: "https://op.example.com/token"
277
+ request_object_encryption_alg_values_supported: ["RSA-OAEP"]
278
+ request_object_encryption_enc_values_supported: ["A128CBC-HS256"]
279
+ require_request_encryption: false # Set to true to require encryption
280
+ validate_request_objects: true # Set to false to skip validation
281
+ ```
282
+
283
+ ### RP Server Configuration
284
+
285
+ Create `examples/config/mock_rp.yml`:
286
+
287
+ ```yaml
288
+ entity_id: "https://rp.example.com"
289
+ server_host: "localhost:9293"
290
+ signing_key: |
291
+ -----BEGIN RSA PRIVATE KEY-----
292
+ ...
293
+ -----END RSA PRIVATE KEY-----
294
+ encryption_key: | # Optional, for decrypting encrypted ID tokens
295
+ -----BEGIN RSA PRIVATE KEY-----
296
+ ...
297
+ -----END RSA PRIVATE KEY-----
298
+ trust_anchors:
299
+ - entity_id: "https://ta.example.com"
300
+ jwks:
301
+ keys: [...]
302
+ authority_hints: # Optional, if RP is subordinate
303
+ - "https://ta.example.com"
304
+ redirect_uris:
305
+ - "https://rp.example.com/callback"
306
+ ```
307
+
308
+ ## Testing Scenarios
309
+
310
+ ### Happy Path
311
+
312
+ 1. Start both servers
313
+ 2. Run integration tests: `ruby examples/integration_test_flow.rb`
314
+ 3. All tests should pass
315
+
316
+ ### Error Scenarios
317
+
318
+ 1. Test invalid entity statement:
319
+ ```bash
320
+ curl "http://localhost:9292/.well-known/openid-federation?error_mode=invalid_statement"
321
+ ```
322
+
323
+ 2. Test wrong keys:
324
+ ```bash
325
+ curl "http://localhost:9292/.well-known/jwks.json?error_mode=wrong_keys"
326
+ ```
327
+
328
+ 3. Test expired statement:
329
+ ```bash
330
+ curl "http://localhost:9292/.well-known/openid-federation?error_mode=expired_statement"
331
+ ```
332
+
333
+ ### Request Object Validation
334
+
335
+ 1. Test with valid signed request object:
336
+ ```bash
337
+ curl "http://localhost:9293/login?provider=https://op.example.com"
338
+ ```
339
+
340
+ 2. Test with invalid request object:
341
+ ```bash
342
+ curl "http://localhost:9292/auth?error_mode=invalid_request&request=invalid.jwt"
343
+ ```
344
+
345
+ ### Trust Chain Resolution
346
+
347
+ 1. Configure trust anchors in both servers
348
+ 2. Ensure RP has authority_hints pointing to trust anchor
349
+ 3. OP will resolve RP's trust chain during authorization
350
+ 4. OP will apply metadata policies from trust chain
351
+
352
+ ## Security Testing
353
+
354
+ The mock servers support comprehensive security testing:
355
+
356
+ - **Algorithm Confusion**: Test rejection of non-RS256 algorithms
357
+ - **Key Confusion**: Test rejection of wrong keys
358
+ - **Signature Verification**: Test rejection of tampered signatures
359
+ - **Path Traversal**: Test rejection of malicious paths
360
+ - **Replay Attacks**: Test nonce validation
361
+ - **Request Object Validation**: Test signature and encryption validation
362
+ - **Trust Chain Validation**: Test authority hints and metadata policy enforcement
363
+
364
+ ## Troubleshooting
365
+
366
+ ### Server Won't Start
367
+
368
+ - Check if ports 9292 (OP) and 9293 (RP) are available
369
+ - Verify all dependencies are installed: `bundle install`
370
+ - Check configuration files exist and are valid YAML
371
+
372
+ ### Entity Statements Not Validating
373
+
374
+ - Verify keys are correctly configured
375
+ - Check entity IDs match between servers
376
+ - Ensure trust anchors are configured if using trust chains
377
+
378
+ ### Request Objects Rejected
379
+
380
+ - Verify RP's entity statement is accessible
381
+ - Check RP's JWKS contains the signing key
382
+ - Ensure request object is properly signed
383
+ - Verify encryption if required by OP
384
+
385
+ ### Trust Chain Resolution Fails
386
+
387
+ - Verify trust anchors are configured
388
+ - Check authority_hints are correct
389
+ - Ensure all entity statements in chain are valid
390
+ - Verify subordinate statements are available
391
+
392
+ ## Next Steps
393
+
394
+ 1. **Add More Error Scenarios**: Extend error injection modes
395
+ 2. **Performance Testing**: Add load testing scenarios
396
+ 3. **Security Testing**: Add fuzzing and penetration tests
397
+ 4. **Trust Mark Testing**: Add trust mark validation
398
+ 5. **Metadata Policy Testing**: Add comprehensive policy merging tests
399
+