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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +44 -0
- data/LICENSE.md +22 -0
- data/README.md +922 -0
- data/SECURITY.md +28 -0
- data/app/controllers/omniauth_openid_federation/federation_controller.rb +160 -0
- data/config/routes.rb +17 -0
- data/examples/README_INTEGRATION_TESTING.md +399 -0
- data/examples/README_MOCK_OP.md +243 -0
- data/examples/app/controllers/users/omniauth_callbacks_controller.rb.example +37 -0
- data/examples/app/jobs/jwks_rotation_job.rb.example +60 -0
- data/examples/app/models/user.rb.example +39 -0
- data/examples/config/initializers/devise.rb.example +131 -0
- data/examples/config/initializers/federation_endpoint.rb.example +206 -0
- data/examples/config/mock_op.yml.example +83 -0
- data/examples/config/open_id_connect_config.rb.example +210 -0
- data/examples/config/routes.rb.example +12 -0
- data/examples/db/migrate/add_omniauth_to_users.rb.example +16 -0
- data/examples/integration_test_flow.rb +1334 -0
- data/examples/jobs/README.md +194 -0
- data/examples/jobs/federation_cache_refresh_job.rb.example +78 -0
- data/examples/jobs/federation_files_generation_job.rb.example +87 -0
- data/examples/mock_op_server.rb +775 -0
- data/examples/mock_rp_server.rb +435 -0
- data/lib/omniauth_openid_federation/access_token.rb +504 -0
- data/lib/omniauth_openid_federation/cache.rb +39 -0
- data/lib/omniauth_openid_federation/cache_adapter.rb +173 -0
- data/lib/omniauth_openid_federation/configuration.rb +135 -0
- data/lib/omniauth_openid_federation/constants.rb +13 -0
- data/lib/omniauth_openid_federation/endpoint_resolver.rb +168 -0
- data/lib/omniauth_openid_federation/engine.rb +17 -0
- data/lib/omniauth_openid_federation/entity_statement_reader.rb +129 -0
- data/lib/omniauth_openid_federation/errors.rb +52 -0
- data/lib/omniauth_openid_federation/federation/entity_statement.rb +331 -0
- data/lib/omniauth_openid_federation/federation/entity_statement_builder.rb +188 -0
- data/lib/omniauth_openid_federation/federation/entity_statement_fetcher.rb +142 -0
- data/lib/omniauth_openid_federation/federation/entity_statement_helper.rb +87 -0
- data/lib/omniauth_openid_federation/federation/entity_statement_parser.rb +198 -0
- data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +502 -0
- data/lib/omniauth_openid_federation/federation/metadata_policy_merger.rb +276 -0
- data/lib/omniauth_openid_federation/federation/signed_jwks.rb +210 -0
- data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +225 -0
- data/lib/omniauth_openid_federation/federation_endpoint.rb +949 -0
- data/lib/omniauth_openid_federation/http_client.rb +70 -0
- data/lib/omniauth_openid_federation/instrumentation.rb +399 -0
- data/lib/omniauth_openid_federation/jwks/cache.rb +76 -0
- data/lib/omniauth_openid_federation/jwks/decode.rb +175 -0
- data/lib/omniauth_openid_federation/jwks/fetch.rb +153 -0
- data/lib/omniauth_openid_federation/jwks/normalizer.rb +49 -0
- data/lib/omniauth_openid_federation/jwks/rotate.rb +97 -0
- data/lib/omniauth_openid_federation/jwks/selector.rb +101 -0
- data/lib/omniauth_openid_federation/jws.rb +410 -0
- data/lib/omniauth_openid_federation/key_extractor.rb +173 -0
- data/lib/omniauth_openid_federation/logger.rb +99 -0
- data/lib/omniauth_openid_federation/rack_endpoint.rb +187 -0
- data/lib/omniauth_openid_federation/railtie.rb +15 -0
- data/lib/omniauth_openid_federation/rate_limiter.rb +55 -0
- data/lib/omniauth_openid_federation/strategy.rb +2114 -0
- data/lib/omniauth_openid_federation/string_helpers.rb +30 -0
- data/lib/omniauth_openid_federation/tasks_helper.rb +428 -0
- data/lib/omniauth_openid_federation/utils.rb +168 -0
- data/lib/omniauth_openid_federation/validators.rb +126 -0
- data/lib/omniauth_openid_federation/version.rb +3 -0
- data/lib/omniauth_openid_federation.rb +99 -0
- data/lib/tasks/omniauth_openid_federation.rake +376 -0
- data/sig/federation.rbs +218 -0
- data/sig/jwks.rbs +63 -0
- data/sig/omniauth_openid_federation.rbs +254 -0
- data/sig/strategy.rbs +60 -0
- 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
|
+
|