omniauth_openid_federation 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +16 -0
- data/LICENSE.md +22 -0
- data/README.md +822 -0
- data/SECURITY.md +129 -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 +33 -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 +97 -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/entity_statement_reader.rb +122 -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 +383 -0
- data/lib/omniauth_openid_federation/jwks/cache.rb +76 -0
- data/lib/omniauth_openid_federation/jwks/decode.rb +174 -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 +416 -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 +29 -0
- data/lib/omniauth_openid_federation/rate_limiter.rb +55 -0
- data/lib/omniauth_openid_federation/strategy.rb +2029 -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 +166 -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 +98 -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 +352 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Mock OpenID Provider (OP) Server
|
|
2
|
+
|
|
3
|
+
A standalone Rack/Sinatra application for testing OpenID Federation flows.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ Entity Configuration endpoint (`/.well-known/openid-federation`)
|
|
8
|
+
- ✅ Fetch Endpoint (`/.well-known/openid-federation/fetch`) for Subordinate Statements
|
|
9
|
+
- ✅ Authorization Endpoint (`/auth`) with trust chain resolution
|
|
10
|
+
- ✅ Token Endpoint (`/token`) with ID Token signing
|
|
11
|
+
- ✅ JWKS endpoints (standard and signed)
|
|
12
|
+
- ✅ UserInfo endpoint (mock)
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### 1. Install Dependencies
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bundle install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 2. Configure
|
|
23
|
+
|
|
24
|
+
Copy the example configuration:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cp examples/config/mock_op.yml.example examples/config/mock_op.yml
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Edit `examples/config/mock_op.yml` with your settings:
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
entity_id: "https://op.example.com"
|
|
34
|
+
server_host: "localhost:9292"
|
|
35
|
+
signing_key: |
|
|
36
|
+
-----BEGIN RSA PRIVATE KEY-----
|
|
37
|
+
...
|
|
38
|
+
-----END RSA PRIVATE KEY-----
|
|
39
|
+
trust_anchors:
|
|
40
|
+
- entity_id: "https://ta.example.com"
|
|
41
|
+
jwks:
|
|
42
|
+
keys: [...]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 3. Generate Keys (if needed)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Generate OP signing key
|
|
49
|
+
openssl genrsa -out op-private-key.pem 2048
|
|
50
|
+
openssl rsa -in op-private-key.pem -pubout -out op-public-key.pem
|
|
51
|
+
|
|
52
|
+
# Extract JWKS from public key (or use the rake task)
|
|
53
|
+
rake openid_federation:prepare_client_keys
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 4. Run Server
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Option 1: Direct Ruby execution
|
|
60
|
+
ruby examples/mock_op_server.rb
|
|
61
|
+
|
|
62
|
+
# Option 2: Using Rack
|
|
63
|
+
rackup examples/mock_op_server.ru
|
|
64
|
+
|
|
65
|
+
# Option 3: With specific port
|
|
66
|
+
rackup -p 9292 examples/mock_op_server.ru
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Configuration Options
|
|
70
|
+
|
|
71
|
+
### Environment Variables
|
|
72
|
+
|
|
73
|
+
Instead of YAML, you can use environment variables:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
export OP_ENTITY_ID="https://op.example.com"
|
|
77
|
+
export OP_SERVER_HOST="localhost:9292"
|
|
78
|
+
export OP_SIGNING_KEY="$(cat op-private-key.pem)"
|
|
79
|
+
export OP_TRUST_ANCHORS='[{"entity_id":"https://ta.example.com","jwks":{"keys":[...]}}]'
|
|
80
|
+
export OP_AUTHORITY_HINTS="https://federation.example.com"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### YAML Configuration
|
|
84
|
+
|
|
85
|
+
See `examples/config/mock_op.yml.example` for full configuration options.
|
|
86
|
+
|
|
87
|
+
## Testing Scenarios
|
|
88
|
+
|
|
89
|
+
### 1. Direct Entity Statement (No Trust Chain)
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Fetch OP's Entity Configuration
|
|
93
|
+
curl http://localhost:9292/.well-known/openid-federation
|
|
94
|
+
|
|
95
|
+
# Use in RP configuration
|
|
96
|
+
config.omniauth :openid_federation,
|
|
97
|
+
issuer: "https://op.example.com",
|
|
98
|
+
entity_statement_url: "http://localhost:9292/.well-known/openid-federation",
|
|
99
|
+
client_options: { ... }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2. Trust Chain Resolution
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Configure trust anchors in mock_op.yml
|
|
106
|
+
trust_anchors:
|
|
107
|
+
- entity_id: "https://ta.example.com"
|
|
108
|
+
jwks: {...}
|
|
109
|
+
|
|
110
|
+
# RP with trust chain
|
|
111
|
+
# The OP will resolve the RP's trust chain automatically
|
|
112
|
+
curl "http://localhost:9292/auth?client_id=https://rp.example.com&redirect_uri=https://rp.example.com/callback"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 3. Subordinate Statements
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Configure subordinate statements in mock_op.yml
|
|
119
|
+
subordinate_statements:
|
|
120
|
+
"https://rp.example.com":
|
|
121
|
+
metadata: {...}
|
|
122
|
+
metadata_policy: {...}
|
|
123
|
+
|
|
124
|
+
# Fetch Subordinate Statement
|
|
125
|
+
curl "http://localhost:9292/.well-known/openid-federation/fetch?sub=https://rp.example.com"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Endpoints
|
|
129
|
+
|
|
130
|
+
| Endpoint | Method | Description |
|
|
131
|
+
|----------|--------|-------------|
|
|
132
|
+
| `/.well-known/openid-federation` | GET | Entity Configuration (JWT) |
|
|
133
|
+
| `/.well-known/openid-federation/fetch` | GET | Fetch Subordinate Statement (requires `sub` parameter) |
|
|
134
|
+
| `/.well-known/jwks.json` | GET | Standard JWKS (JSON) |
|
|
135
|
+
| `/.well-known/signed-jwks.json` | GET | Signed JWKS (JWT) |
|
|
136
|
+
| `/auth` | GET | Authorization Endpoint (requires `client_id`, `redirect_uri`) |
|
|
137
|
+
| `/token` | POST | Token Endpoint (requires `code`, `grant_type=authorization_code`) |
|
|
138
|
+
| `/userinfo` | GET | UserInfo Endpoint (mock data) |
|
|
139
|
+
| `/` | GET | Health check and endpoint list |
|
|
140
|
+
|
|
141
|
+
## Example Flow
|
|
142
|
+
|
|
143
|
+
### 1. RP Discovers OP
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# RP fetches OP's Entity Configuration
|
|
147
|
+
curl http://localhost:9292/.well-known/openid-federation
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 2. RP Initiates Authentication
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# RP redirects user to authorization endpoint
|
|
154
|
+
# client_id is the RP's Entity ID (for automatic registration)
|
|
155
|
+
curl "http://localhost:9292/auth?client_id=https://rp.example.com&redirect_uri=https://rp.example.com/callback&state=xyz&nonce=abc"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 3. OP Resolves RP's Trust Chain
|
|
159
|
+
|
|
160
|
+
The OP automatically:
|
|
161
|
+
- Resolves RP's trust chain using `TrustChainResolver`
|
|
162
|
+
- Merges metadata policies using `MetadataPolicyMerger`
|
|
163
|
+
- Validates RP's effective metadata
|
|
164
|
+
- Redirects back to RP with authorization code
|
|
165
|
+
|
|
166
|
+
### 4. RP Exchanges Code for Tokens
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
curl -X POST http://localhost:9292/token \
|
|
170
|
+
-d "grant_type=authorization_code" \
|
|
171
|
+
-d "code=<authorization_code>" \
|
|
172
|
+
-d "redirect_uri=https://rp.example.com/callback" \
|
|
173
|
+
-d "client_id=https://rp.example.com"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 5. RP Validates ID Token
|
|
177
|
+
|
|
178
|
+
The RP validates the ID Token using the OP's JWKS from the effective metadata.
|
|
179
|
+
|
|
180
|
+
## Integration with Real RP
|
|
181
|
+
|
|
182
|
+
To test with a real RP application:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
# In RP's config/initializers/devise.rb
|
|
186
|
+
config.omniauth :openid_federation,
|
|
187
|
+
issuer: "http://localhost:9292",
|
|
188
|
+
entity_statement_url: "http://localhost:9292/.well-known/openid-federation",
|
|
189
|
+
trust_anchors: [
|
|
190
|
+
{
|
|
191
|
+
entity_id: "https://ta.example.com",
|
|
192
|
+
jwks: trust_anchor_jwks
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
client_options: {
|
|
196
|
+
identifier: "https://rp.example.com", # RP's Entity ID
|
|
197
|
+
redirect_uri: "http://localhost:3000/users/auth/openid_federation/callback",
|
|
198
|
+
private_key: rp_private_key
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Limitations
|
|
203
|
+
|
|
204
|
+
This is a **mock server for testing only**:
|
|
205
|
+
|
|
206
|
+
- ⚠️ No real user authentication (always returns mock user)
|
|
207
|
+
- ⚠️ Authorization codes stored in memory (lost on restart)
|
|
208
|
+
- ⚠️ No database persistence
|
|
209
|
+
- ⚠️ No production security hardening
|
|
210
|
+
- ⚠️ ID Tokens contain mock user data
|
|
211
|
+
|
|
212
|
+
## Production Considerations
|
|
213
|
+
|
|
214
|
+
For production use, you would need:
|
|
215
|
+
|
|
216
|
+
- Real user authentication system
|
|
217
|
+
- Database for authorization codes and tokens
|
|
218
|
+
- Proper session management
|
|
219
|
+
- Security hardening (rate limiting, CSRF protection, etc.)
|
|
220
|
+
- Real user data in ID Tokens
|
|
221
|
+
- Proper error handling and logging
|
|
222
|
+
|
|
223
|
+
## Troubleshooting
|
|
224
|
+
|
|
225
|
+
**"Federation endpoint not configured"**
|
|
226
|
+
- Ensure `signing_key` is provided in config
|
|
227
|
+
- Check that `entity_id` is set
|
|
228
|
+
|
|
229
|
+
**"Trust chain resolution failed"**
|
|
230
|
+
- Verify `trust_anchors` are correctly configured
|
|
231
|
+
- Ensure trust anchor JWKS are valid
|
|
232
|
+
- Check that RP's Entity ID is resolvable
|
|
233
|
+
|
|
234
|
+
**"Subordinate Statement not found"**
|
|
235
|
+
- Configure `subordinate_statements` in `mock_op.yml`
|
|
236
|
+
- Ensure subject Entity ID matches exactly
|
|
237
|
+
|
|
238
|
+
## See Also
|
|
239
|
+
|
|
240
|
+
- [OpenID Federation 1.0 Specification](https://openid.net/specs/openid-federation-1_0.html)
|
|
241
|
+
- [Main README](../README.md)
|
|
242
|
+
- [Federation Endpoint Documentation](../README.md#publishing-federation-endpoint)
|
|
243
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Example OmniAuth callback controller for Devise
|
|
2
|
+
# Copy this to app/controllers/users/omniauth_callbacks_controller.rb
|
|
3
|
+
|
|
4
|
+
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|
5
|
+
skip_before_action :authenticate_user!, only: [:openid_federation, :failure]
|
|
6
|
+
|
|
7
|
+
def openid_federation
|
|
8
|
+
auth = request.env["omniauth.auth"]
|
|
9
|
+
|
|
10
|
+
@user = User.find_or_create_from_omniauth(auth)
|
|
11
|
+
|
|
12
|
+
if @user&.persisted?
|
|
13
|
+
sign_in_and_redirect @user, event: :authentication
|
|
14
|
+
else
|
|
15
|
+
redirect_to root_path, alert: "Authentication failed"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def failure
|
|
20
|
+
error_type = request.env["omniauth.error.type"] || :unknown
|
|
21
|
+
error_message = request.env["omniauth.error"]&.message || "Authentication failed"
|
|
22
|
+
|
|
23
|
+
Rails.logger.error({
|
|
24
|
+
message: "OmniAuth authentication failure",
|
|
25
|
+
error_type: error_type,
|
|
26
|
+
error_message: error_message,
|
|
27
|
+
strategy: request.env["omniauth.strategy"]&.name
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
redirect_to root_path, alert: "Authentication failed: #{error_type}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Background job for proactive JWKS key rotation
|
|
2
|
+
# This job runs periodically to refresh JWKS cache before expiration,
|
|
3
|
+
# ensuring keys are always up-to-date without blocking client requests.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# 1. Schedule this job to run periodically (e.g., every 12 hours)
|
|
7
|
+
# 2. Configure cache TTL to be longer than job frequency (e.g., 24 hours)
|
|
8
|
+
# 3. This ensures keys are refreshed proactively, not reactively
|
|
9
|
+
#
|
|
10
|
+
# Example scheduling (using GoodJob, Sidekiq, or similar):
|
|
11
|
+
#
|
|
12
|
+
# # config/initializers/schedule.rb (for GoodJob)
|
|
13
|
+
# GoodJob::Cron::Schedule.add("jwks_rotation", {
|
|
14
|
+
# cron: "0 */12 * * *", # Every 12 hours
|
|
15
|
+
# class: "JwksRotationJob"
|
|
16
|
+
# })
|
|
17
|
+
#
|
|
18
|
+
# # Or with Sidekiq-Cron
|
|
19
|
+
# Sidekiq::Cron::Job.create(
|
|
20
|
+
# name: "JWKS Rotation",
|
|
21
|
+
# cron: "0 */12 * * *",
|
|
22
|
+
# class: "JwksRotationJob"
|
|
23
|
+
# )
|
|
24
|
+
class JwksRotationJob < ApplicationJob
|
|
25
|
+
queue_as :default
|
|
26
|
+
|
|
27
|
+
# Rotate JWKS for a specific provider
|
|
28
|
+
#
|
|
29
|
+
# @param jwks_uri [String] The JWKS URI to refresh
|
|
30
|
+
# @param entity_statement_path [String, nil] Path to entity statement for signed JWKS
|
|
31
|
+
def perform(jwks_uri, entity_statement_path: nil)
|
|
32
|
+
OmniauthOpenidFederation.rotate_jwks(jwks_uri, entity_statement_path: entity_statement_path)
|
|
33
|
+
rescue => e
|
|
34
|
+
# Log error but don't fail - request-level rotation will handle it
|
|
35
|
+
Rails.logger.error("[JwksRotationJob] Failed to rotate JWKS for #{jwks_uri}: #{e.class} - #{e.message}")
|
|
36
|
+
# Optionally, send to error tracking service (Sentry, Rollbar, etc.)
|
|
37
|
+
# Sentry.capture_exception(e) if defined?(Sentry)
|
|
38
|
+
raise # Re-raise to allow job retry if configured
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Rotate JWKS for all configured providers
|
|
42
|
+
# This is useful when you have multiple providers configured
|
|
43
|
+
def self.rotate_all
|
|
44
|
+
# Example: Get all providers from Devise config
|
|
45
|
+
providers = Devise.omniauth_configs.keys.select { |k| k.to_s.start_with?("openid") }
|
|
46
|
+
|
|
47
|
+
providers.each do |provider_name|
|
|
48
|
+
config = Devise.omniauth_configs[provider_name]
|
|
49
|
+
options = config.options
|
|
50
|
+
client_options = options[:client_options] || options["client_options"] || {}
|
|
51
|
+
jwks_uri = client_options[:jwks_uri] || client_options["jwks_uri"]
|
|
52
|
+
entity_statement_path = options[:entity_statement_path] || options["entity_statement_path"]
|
|
53
|
+
|
|
54
|
+
if jwks_uri
|
|
55
|
+
perform_later(jwks_uri, entity_statement_path: entity_statement_path)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Example User model with OmniAuth integration
|
|
2
|
+
# Add these methods to your existing User model
|
|
3
|
+
|
|
4
|
+
class User < ApplicationRecord
|
|
5
|
+
# Required columns (add via migration):
|
|
6
|
+
# - provider (string)
|
|
7
|
+
# - uid (string)
|
|
8
|
+
# - email (string)
|
|
9
|
+
# - name (string)
|
|
10
|
+
# - first_name (string, optional)
|
|
11
|
+
# - last_name (string, optional)
|
|
12
|
+
|
|
13
|
+
def self.find_or_create_from_omniauth(auth)
|
|
14
|
+
user = find_by(provider: auth.provider, uid: auth.uid)
|
|
15
|
+
|
|
16
|
+
if user
|
|
17
|
+
# Update existing user info
|
|
18
|
+
user.update(
|
|
19
|
+
email: auth.info.email,
|
|
20
|
+
name: auth.info.name,
|
|
21
|
+
first_name: auth.info.first_name,
|
|
22
|
+
last_name: auth.info.last_name
|
|
23
|
+
)
|
|
24
|
+
else
|
|
25
|
+
# Create new user
|
|
26
|
+
user = create(
|
|
27
|
+
provider: auth.provider,
|
|
28
|
+
uid: auth.uid,
|
|
29
|
+
email: auth.info.email,
|
|
30
|
+
name: auth.info.name,
|
|
31
|
+
first_name: auth.info.first_name,
|
|
32
|
+
last_name: auth.info.last_name
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
user
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Example Devise configuration for OmniAuth OpenID Federation
|
|
2
|
+
# Copy this to config/initializers/devise.rb and customize for your provider
|
|
3
|
+
|
|
4
|
+
require "omniauth_openid_federation"
|
|
5
|
+
|
|
6
|
+
# Configure global settings (optional but recommended)
|
|
7
|
+
OmniauthOpenidFederation.configure do |config|
|
|
8
|
+
# Security instrumentation - get notified about security events, MITM attacks, etc.
|
|
9
|
+
# Example with Sentry:
|
|
10
|
+
# config.instrumentation = ->(event, data) do
|
|
11
|
+
# Sentry.capture_message(
|
|
12
|
+
# "OpenID Federation: #{event}",
|
|
13
|
+
# level: data[:severity] == :error ? :error : :warning,
|
|
14
|
+
# extra: data
|
|
15
|
+
# )
|
|
16
|
+
# end
|
|
17
|
+
|
|
18
|
+
# Example with Honeybadger:
|
|
19
|
+
# config.instrumentation = ->(event, data) do
|
|
20
|
+
# Honeybadger.notify("OpenID Federation: #{event}", context: data)
|
|
21
|
+
# end
|
|
22
|
+
|
|
23
|
+
# Example with custom logger:
|
|
24
|
+
# config.instrumentation = ->(event, data) do
|
|
25
|
+
# Rails.logger.warn("[Security] #{event}: #{data.inspect}")
|
|
26
|
+
# end
|
|
27
|
+
|
|
28
|
+
# Cache configuration (optional)
|
|
29
|
+
# config.cache_ttl = 3600 # Refresh provider keys every hour
|
|
30
|
+
# config.rotate_on_errors = true # Auto-handle provider key rotation
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Provider configuration
|
|
34
|
+
provider_issuer = ENV["OPENID_PROVIDER_ISSUER"] || "https://provider.example.com"
|
|
35
|
+
client_id = ENV["OPENID_CLIENT_ID"] || "your-client-id"
|
|
36
|
+
redirect_uri = "#{ENV["APP_URL"] || "https://your-app.com"}/users/auth/openid_federation/callback"
|
|
37
|
+
|
|
38
|
+
# File paths
|
|
39
|
+
private_key_path = Rails.root.join("config", "client-private-key.pem")
|
|
40
|
+
entity_statement_path = Rails.root.join("config", "provider-entity-statement.jwt")
|
|
41
|
+
|
|
42
|
+
# Load private key
|
|
43
|
+
unless File.exist?(private_key_path)
|
|
44
|
+
raise "Private key not found at #{private_key_path}. Generate it first using the example in README."
|
|
45
|
+
end
|
|
46
|
+
private_key = OpenSSL::PKey::RSA.new(File.read(private_key_path))
|
|
47
|
+
|
|
48
|
+
# Resolve endpoints from entity statement or manual configuration
|
|
49
|
+
endpoints = if File.exist?(entity_statement_path)
|
|
50
|
+
# Use entity statement if available (recommended for OpenID Federation)
|
|
51
|
+
OmniauthOpenidFederation::EndpointResolver.resolve(
|
|
52
|
+
entity_statement_path: entity_statement_path.to_s,
|
|
53
|
+
config: {}
|
|
54
|
+
)
|
|
55
|
+
else
|
|
56
|
+
# Fallback to manual configuration
|
|
57
|
+
{
|
|
58
|
+
authorization_endpoint: ENV["OPENID_AUTHORIZATION_ENDPOINT"] || "/oauth2/authorize",
|
|
59
|
+
token_endpoint: ENV["OPENID_TOKEN_ENDPOINT"] || "/oauth2/token",
|
|
60
|
+
userinfo_endpoint: ENV["OPENID_USERINFO_ENDPOINT"] || "/oauth2/userinfo",
|
|
61
|
+
jwks_uri: ENV["OPENID_JWKS_URI"] || "/.well-known/jwks.json",
|
|
62
|
+
audience: provider_issuer
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Validate endpoints
|
|
67
|
+
OmniauthOpenidFederation::EndpointResolver.validate_and_build_audience(
|
|
68
|
+
endpoints,
|
|
69
|
+
issuer_uri: URI.parse(provider_issuer)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
Devise.setup do |config|
|
|
73
|
+
# ... your other Devise configuration ...
|
|
74
|
+
|
|
75
|
+
config.omniauth :openid_federation,
|
|
76
|
+
name: :openid_federation,
|
|
77
|
+
scope: [:openid],
|
|
78
|
+
response_type: "code",
|
|
79
|
+
discovery: true,
|
|
80
|
+
issuer: provider_issuer,
|
|
81
|
+
client_auth_method: :jwt_bearer,
|
|
82
|
+
client_signing_alg: :RS256,
|
|
83
|
+
audience: endpoints[:audience],
|
|
84
|
+
entity_statement_path: entity_statement_path.to_s,
|
|
85
|
+
client_options: {
|
|
86
|
+
identifier: client_id,
|
|
87
|
+
redirect_uri: redirect_uri,
|
|
88
|
+
private_key: private_key,
|
|
89
|
+
scheme: URI.parse(provider_issuer).scheme,
|
|
90
|
+
host: URI.parse(provider_issuer).host,
|
|
91
|
+
authorization_endpoint: endpoints[:authorization_endpoint],
|
|
92
|
+
token_endpoint: endpoints[:token_endpoint],
|
|
93
|
+
userinfo_endpoint: endpoints[:userinfo_endpoint],
|
|
94
|
+
jwks_uri: endpoints[:jwks_uri]
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Federation Endpoint Configuration
|
|
2
|
+
# This enables publishing an entity statement at /.well-known/openid-federation
|
|
3
|
+
# Required for OpenID Federation 1.0 compliance with signed JWKS support
|
|
4
|
+
#
|
|
5
|
+
# The entity statement is a self-signed JWT that contains:
|
|
6
|
+
# - Entity metadata (endpoints, configuration)
|
|
7
|
+
# - JWKS for signature validation (both signing and encryption keys)
|
|
8
|
+
# - Issuer and subject information
|
|
9
|
+
#
|
|
10
|
+
# Supports two entity types:
|
|
11
|
+
# - openid_relying_party (RP): For clients/relying parties (PRIMARY USE CASE)
|
|
12
|
+
# - openid_provider (OP): For providers/servers (secondary use case)
|
|
13
|
+
#
|
|
14
|
+
# Automatic Key Provisioning:
|
|
15
|
+
# - Extracts JWKS from entity_statement_path if provided (cached, supports key rotation)
|
|
16
|
+
# - Supports separate signing_key and encryption_key (RECOMMENDED for production)
|
|
17
|
+
# - Falls back to single private_key (DEV/TESTING ONLY - not recommended for production)
|
|
18
|
+
# - Automatically generates both signing and encryption keys from provided keys
|
|
19
|
+
|
|
20
|
+
require "omniauth_openid_federation"
|
|
21
|
+
|
|
22
|
+
# ============================================================================
|
|
23
|
+
# Global Configuration (Optional but Recommended)
|
|
24
|
+
# ============================================================================
|
|
25
|
+
OmniauthOpenidFederation.configure do |config|
|
|
26
|
+
# Security instrumentation - get notified about security events, MITM attacks, etc.
|
|
27
|
+
# Example with Sentry:
|
|
28
|
+
# config.instrumentation = ->(event, data) do
|
|
29
|
+
# Sentry.capture_message(
|
|
30
|
+
# "OpenID Federation: #{event}",
|
|
31
|
+
# level: data[:severity] == :error ? :error : :warning,
|
|
32
|
+
# extra: data
|
|
33
|
+
# )
|
|
34
|
+
# end
|
|
35
|
+
|
|
36
|
+
# Example with Honeybadger:
|
|
37
|
+
# config.instrumentation = ->(event, data) do
|
|
38
|
+
# Honeybadger.notify("OpenID Federation: #{event}", context: data)
|
|
39
|
+
# end
|
|
40
|
+
|
|
41
|
+
# Example with custom logger:
|
|
42
|
+
# config.instrumentation = ->(event, data) do
|
|
43
|
+
# Rails.logger.warn("[Security] #{event}: #{data.inspect}")
|
|
44
|
+
# end
|
|
45
|
+
|
|
46
|
+
# Cache configuration (optional)
|
|
47
|
+
# config.cache_ttl = 3600 # Refresh provider keys every hour
|
|
48
|
+
# config.rotate_on_errors = true # Auto-handle provider key rotation
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# ============================================================================
|
|
52
|
+
# EXAMPLE 1: Relying Party (RP) Configuration (PRIMARY USE CASE)
|
|
53
|
+
# ============================================================================
|
|
54
|
+
# For client applications that authenticate users via OpenID Federation
|
|
55
|
+
|
|
56
|
+
app_url = ENV["APP_URL"] || "https://your-app.example.com"
|
|
57
|
+
|
|
58
|
+
# Production Setup (RECOMMENDED): Separate signing and encryption keys
|
|
59
|
+
signing_key_path = Rails.root.join("config", "client-signing-private-key.pem")
|
|
60
|
+
encryption_key_path = Rails.root.join("config", "client-encryption-private-key.pem")
|
|
61
|
+
|
|
62
|
+
if File.exist?(signing_key_path) && File.exist?(encryption_key_path)
|
|
63
|
+
# Production: Use separate keys
|
|
64
|
+
signing_key = OpenSSL::PKey::RSA.new(File.read(signing_key_path))
|
|
65
|
+
encryption_key = OpenSSL::PKey::RSA.new(File.read(encryption_key_path))
|
|
66
|
+
|
|
67
|
+
OmniauthOpenidFederation::FederationEndpoint.auto_configure(
|
|
68
|
+
issuer: app_url,
|
|
69
|
+
signing_key: signing_key,
|
|
70
|
+
encryption_key: encryption_key,
|
|
71
|
+
entity_statement_path: Rails.root.join("config", "client-entity-statement.jwt"), # Cache for key rotation
|
|
72
|
+
metadata: {
|
|
73
|
+
openid_relying_party: {
|
|
74
|
+
redirect_uris: [
|
|
75
|
+
"#{app_url}/users/auth/openid_federation/callback"
|
|
76
|
+
],
|
|
77
|
+
client_registration_types: ["automatic"],
|
|
78
|
+
application_type: "web",
|
|
79
|
+
grant_types: ["authorization_code"],
|
|
80
|
+
response_types: ["code"],
|
|
81
|
+
token_endpoint_auth_method: "private_key_jwt",
|
|
82
|
+
token_endpoint_auth_signing_alg: "RS256",
|
|
83
|
+
request_object_signing_alg: "RS256",
|
|
84
|
+
id_token_encrypted_response_alg: "RSA-OAEP",
|
|
85
|
+
id_token_encrypted_response_enc: "A128CBC-HS256"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
expiration_seconds: (ENV["FEDERATION_EXPIRATION_SECONDS"] || 86400).to_i,
|
|
89
|
+
jwks_cache_ttl: (ENV["FEDERATION_JWKS_CACHE_TTL"] || 3600).to_i,
|
|
90
|
+
auto_provision_keys: true
|
|
91
|
+
)
|
|
92
|
+
else
|
|
93
|
+
# Development/Testing (NOT RECOMMENDED FOR PRODUCTION): Single private key
|
|
94
|
+
private_key_path = Rails.root.join("config", "client-private-key.pem")
|
|
95
|
+
unless File.exist?(private_key_path)
|
|
96
|
+
Rails.logger.warn "[FederationEndpoint] Private key not found at #{private_key_path}. Generate it first."
|
|
97
|
+
Rails.logger.warn " Run: bundle exec rake omniauth_openid_federation:prepare_client_keys"
|
|
98
|
+
next
|
|
99
|
+
end
|
|
100
|
+
private_key = OpenSSL::PKey::RSA.new(File.read(private_key_path))
|
|
101
|
+
|
|
102
|
+
OmniauthOpenidFederation::FederationEndpoint.auto_configure(
|
|
103
|
+
issuer: app_url,
|
|
104
|
+
private_key: private_key, # DEV/TESTING ONLY - not recommended for production
|
|
105
|
+
entity_statement_path: Rails.root.join("config", "client-entity-statement.jwt"),
|
|
106
|
+
metadata: {
|
|
107
|
+
openid_relying_party: {
|
|
108
|
+
redirect_uris: [
|
|
109
|
+
"#{app_url}/users/auth/openid_federation/callback"
|
|
110
|
+
],
|
|
111
|
+
client_registration_types: ["automatic"],
|
|
112
|
+
application_type: "web",
|
|
113
|
+
grant_types: ["authorization_code"],
|
|
114
|
+
response_types: ["code"],
|
|
115
|
+
token_endpoint_auth_method: "private_key_jwt",
|
|
116
|
+
token_endpoint_auth_signing_alg: "RS256",
|
|
117
|
+
request_object_signing_alg: "RS256",
|
|
118
|
+
id_token_encrypted_response_alg: "RSA-OAEP",
|
|
119
|
+
id_token_encrypted_response_enc: "A128CBC-HS256"
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
expiration_seconds: (ENV["FEDERATION_EXPIRATION_SECONDS"] || 86400).to_i,
|
|
123
|
+
jwks_cache_ttl: (ENV["FEDERATION_JWKS_CACHE_TTL"] || 3600).to_i,
|
|
124
|
+
auto_provision_keys: true
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# ============================================================================
|
|
129
|
+
# EXAMPLE 2: OpenID Provider (OP) Configuration (SECONDARY USE CASE)
|
|
130
|
+
# ============================================================================
|
|
131
|
+
# For provider/server applications that serve authentication
|
|
132
|
+
# Uncomment and configure if you're building a provider:
|
|
133
|
+
|
|
134
|
+
# Production Setup (RECOMMENDED): Separate signing and encryption keys
|
|
135
|
+
# provider_url = ENV["PROVIDER_URL"] || "https://provider.example.com"
|
|
136
|
+
#
|
|
137
|
+
# signing_key = OpenSSL::PKey::RSA.new(File.read("config/provider-signing-key.pem"))
|
|
138
|
+
# encryption_key = OpenSSL::PKey::RSA.new(File.read("config/provider-encryption-key.pem"))
|
|
139
|
+
#
|
|
140
|
+
# OmniauthOpenidFederation::FederationEndpoint.auto_configure(
|
|
141
|
+
# issuer: provider_url,
|
|
142
|
+
# signing_key: signing_key,
|
|
143
|
+
# encryption_key: encryption_key,
|
|
144
|
+
# entity_statement_path: Rails.root.join("config", "provider-entity-statement.jwt"),
|
|
145
|
+
# metadata: {
|
|
146
|
+
# openid_provider: {
|
|
147
|
+
# issuer: provider_url,
|
|
148
|
+
# authorization_endpoint: "#{provider_url}/oauth2/authorize",
|
|
149
|
+
# token_endpoint: "#{provider_url}/oauth2/token",
|
|
150
|
+
# userinfo_endpoint: "#{provider_url}/oauth2/userinfo",
|
|
151
|
+
# jwks_uri: "#{provider_url}/.well-known/jwks.json",
|
|
152
|
+
# signed_jwks_uri: "#{provider_url}/.well-known/signed-jwks.json",
|
|
153
|
+
# federation_fetch_endpoint: "#{provider_url}/.well-known/openid-federation/fetch" # Auto-added for OPs
|
|
154
|
+
# }
|
|
155
|
+
# },
|
|
156
|
+
# expiration_seconds: (ENV["FEDERATION_EXPIRATION_SECONDS"] || 86400).to_i,
|
|
157
|
+
# jwks_cache_ttl: (ENV["FEDERATION_JWKS_CACHE_TTL"] || 3600).to_i,
|
|
158
|
+
# auto_provision_keys: true
|
|
159
|
+
# )
|
|
160
|
+
|
|
161
|
+
# Development/Testing (NOT RECOMMENDED FOR PRODUCTION): Single private key
|
|
162
|
+
# provider_url = ENV["PROVIDER_URL"] || "https://provider.example.com"
|
|
163
|
+
# private_key = OpenSSL::PKey::RSA.new(File.read("config/provider-private-key.pem"))
|
|
164
|
+
#
|
|
165
|
+
# OmniauthOpenidFederation::FederationEndpoint.auto_configure(
|
|
166
|
+
# issuer: provider_url,
|
|
167
|
+
# private_key: private_key, # DEV/TESTING ONLY - not recommended for production
|
|
168
|
+
# entity_statement_path: Rails.root.join("config", "provider-entity-statement.jwt"),
|
|
169
|
+
# metadata: {
|
|
170
|
+
# openid_provider: {
|
|
171
|
+
# issuer: provider_url,
|
|
172
|
+
# authorization_endpoint: "#{provider_url}/oauth2/authorize",
|
|
173
|
+
# token_endpoint: "#{provider_url}/oauth2/token",
|
|
174
|
+
# userinfo_endpoint: "#{provider_url}/oauth2/userinfo",
|
|
175
|
+
# jwks_uri: "#{provider_url}/.well-known/jwks.json",
|
|
176
|
+
# signed_jwks_uri: "#{provider_url}/.well-known/signed-jwks.json",
|
|
177
|
+
# federation_fetch_endpoint: "#{provider_url}/.well-known/openid-federation/fetch" # Auto-added for OPs
|
|
178
|
+
# }
|
|
179
|
+
# },
|
|
180
|
+
# expiration_seconds: (ENV["FEDERATION_EXPIRATION_SECONDS"] || 86400).to_i,
|
|
181
|
+
# jwks_cache_ttl: (ENV["FEDERATION_JWKS_CACHE_TTL"] || 3600).to_i,
|
|
182
|
+
# auto_provision_keys: true
|
|
183
|
+
# )
|
|
184
|
+
|
|
185
|
+
# ============================================================================
|
|
186
|
+
# Routes Configuration
|
|
187
|
+
# ============================================================================
|
|
188
|
+
# Add to config/routes.rb:
|
|
189
|
+
#
|
|
190
|
+
# OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)
|
|
191
|
+
#
|
|
192
|
+
# This mounts all endpoints:
|
|
193
|
+
# - GET /.well-known/openid-federation (entity statement)
|
|
194
|
+
# - GET /.well-known/openid-federation/fetch (fetch endpoint - OPs only)
|
|
195
|
+
# - GET /.well-known/jwks.json (standard JWKS)
|
|
196
|
+
# - GET /.well-known/signed-jwks.json (signed JWKS)
|
|
197
|
+
#
|
|
198
|
+
# Or manually:
|
|
199
|
+
# get "/.well-known/openid-federation", to: "omniauth_openid_federation/federation#show"
|
|
200
|
+
# get "/.well-known/openid-federation/fetch", to: "omniauth_openid_federation/federation#fetch"
|
|
201
|
+
# get "/.well-known/jwks.json", to: "omniauth_openid_federation/federation#jwks"
|
|
202
|
+
# get "/.well-known/signed-jwks.json", to: "omniauth_openid_federation/federation#signed_jwks"
|
|
203
|
+
|
|
204
|
+
Rails.logger.info "[FederationEndpoint] Configured. Add the route in config/routes.rb:"
|
|
205
|
+
Rails.logger.info " OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)"
|
|
206
|
+
|