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,194 @@
|
|
|
1
|
+
# Background Job Examples for Federation Endpoints
|
|
2
|
+
|
|
3
|
+
This directory contains example background jobs for generating and caching federation files.
|
|
4
|
+
|
|
5
|
+
## Jobs
|
|
6
|
+
|
|
7
|
+
### 1. FederationFilesGenerationJob
|
|
8
|
+
|
|
9
|
+
**Purpose**: Generate and cache federation files (entity statement, JWKS) to disk.
|
|
10
|
+
|
|
11
|
+
**When to use**:
|
|
12
|
+
- Periodic generation (daily, weekly)
|
|
13
|
+
- After key rotation
|
|
14
|
+
- Before deployment
|
|
15
|
+
- To ensure files are always available
|
|
16
|
+
|
|
17
|
+
**What it does**:
|
|
18
|
+
- Generates `config/.federation-entity-statement.jwt`
|
|
19
|
+
- Generates `config/.federation-jwks.json`
|
|
20
|
+
- Clears Rails cache for federation endpoints
|
|
21
|
+
|
|
22
|
+
**Usage**:
|
|
23
|
+
```ruby
|
|
24
|
+
# Schedule in config/schedule.rb (whenever gem)
|
|
25
|
+
every 1.day, at: '2:00 am' do
|
|
26
|
+
runner 'FederationFilesGenerationJob.perform_later'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Or call manually
|
|
30
|
+
FederationFilesGenerationJob.perform_later
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. FederationCacheRefreshJob
|
|
34
|
+
|
|
35
|
+
**Purpose**: Refresh cached federation data without generating files.
|
|
36
|
+
|
|
37
|
+
**When to use**:
|
|
38
|
+
- Periodic cache refresh (hourly, daily)
|
|
39
|
+
- After key rotation
|
|
40
|
+
- When configuration changes
|
|
41
|
+
- To pre-warm caches
|
|
42
|
+
|
|
43
|
+
**What it does**:
|
|
44
|
+
- Clears all federation-related Rails caches
|
|
45
|
+
- Optionally pre-warms caches for immediate availability
|
|
46
|
+
|
|
47
|
+
**Usage**:
|
|
48
|
+
```ruby
|
|
49
|
+
# Schedule in config/schedule.rb (whenever gem)
|
|
50
|
+
every 1.hour do
|
|
51
|
+
runner 'FederationCacheRefreshJob.perform_later'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Or call manually after key rotation
|
|
55
|
+
FederationCacheRefreshJob.perform_later
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Setup Instructions
|
|
59
|
+
|
|
60
|
+
### Step 1: Copy Job Files
|
|
61
|
+
|
|
62
|
+
Copy the example job files to your `app/jobs/` directory:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
cp examples/jobs/federation_files_generation_job.rb.example app/jobs/federation_files_generation_job.rb
|
|
66
|
+
cp examples/jobs/federation_cache_refresh_job.rb.example app/jobs/federation_cache_refresh_job.rb
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Step 2: Configure Scheduling
|
|
70
|
+
|
|
71
|
+
Add to `config/schedule.rb` (if using whenever gem):
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# Generate federation files daily at 2 AM
|
|
75
|
+
every 1.day, at: '2:00 am' do
|
|
76
|
+
runner 'FederationFilesGenerationJob.perform_later'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Refresh caches hourly
|
|
80
|
+
every 1.hour do
|
|
81
|
+
runner 'FederationCacheRefreshJob.perform_later'
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Or use cron directly:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Generate files daily at 2 AM
|
|
89
|
+
0 2 * * * cd /path/to/app && bin/rails runner 'FederationFilesGenerationJob.perform_now'
|
|
90
|
+
|
|
91
|
+
# Refresh caches hourly
|
|
92
|
+
0 * * * * cd /path/to/app && bin/rails runner 'FederationCacheRefreshJob.perform_now'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Step 3: Test Jobs
|
|
96
|
+
|
|
97
|
+
Test the jobs manually:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Test file generation
|
|
101
|
+
bin/rails runner 'FederationFilesGenerationJob.perform_now'
|
|
102
|
+
|
|
103
|
+
# Test cache refresh
|
|
104
|
+
bin/rails runner 'FederationCacheRefreshJob.perform_now'
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## When to Use Each Job
|
|
108
|
+
|
|
109
|
+
### Use FederationFilesGenerationJob when:
|
|
110
|
+
- ✅ You want files on disk for backup/recovery
|
|
111
|
+
- ✅ You want to serve from files instead of generating on-demand
|
|
112
|
+
- ✅ You need files for external tools or scripts
|
|
113
|
+
- ✅ You want to version control generated files (not recommended for production)
|
|
114
|
+
|
|
115
|
+
### Use FederationCacheRefreshJob when:
|
|
116
|
+
- ✅ You only need in-memory caching (Rails.cache)
|
|
117
|
+
- ✅ You want to refresh caches without generating files
|
|
118
|
+
- ✅ You want faster cache refresh cycles
|
|
119
|
+
- ✅ You don't need files on disk
|
|
120
|
+
|
|
121
|
+
### Use Both when:
|
|
122
|
+
- ✅ You want both file generation and cache refresh
|
|
123
|
+
- ✅ You want redundancy (files + cache)
|
|
124
|
+
- ✅ You have different refresh cycles for files vs cache
|
|
125
|
+
|
|
126
|
+
## Key Rotation Workflow
|
|
127
|
+
|
|
128
|
+
When rotating keys:
|
|
129
|
+
|
|
130
|
+
1. **Before rotation**: Generate new files with new keys
|
|
131
|
+
```bash
|
|
132
|
+
bin/rails runner 'FederationFilesGenerationJob.perform_now'
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
2. **After rotation**: Refresh caches
|
|
136
|
+
```bash
|
|
137
|
+
bin/rails runner 'FederationCacheRefreshJob.perform_now'
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
3. **Verify**: Check that endpoints return new keys
|
|
141
|
+
```bash
|
|
142
|
+
curl https://your-app.com/.well-known/jwks.json
|
|
143
|
+
curl https://your-app.com/.well-known/signed-jwks.json
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Monitoring
|
|
147
|
+
|
|
148
|
+
Monitor job execution:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
# In your monitoring system
|
|
152
|
+
FederationFilesGenerationJob.perform_later
|
|
153
|
+
# Check logs for success/failure
|
|
154
|
+
|
|
155
|
+
# Or use ActiveJob monitoring
|
|
156
|
+
# Check GoodJob dashboard or similar
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Error Handling
|
|
160
|
+
|
|
161
|
+
Both jobs include error handling:
|
|
162
|
+
- Logs errors to Rails.logger
|
|
163
|
+
- Raises exceptions for job retry (if configured)
|
|
164
|
+
- Continues execution even if one step fails (where appropriate)
|
|
165
|
+
|
|
166
|
+
## Performance Considerations
|
|
167
|
+
|
|
168
|
+
- **File Generation**: Takes longer (file I/O), but files persist
|
|
169
|
+
- **Cache Refresh**: Faster (in-memory), but data is ephemeral
|
|
170
|
+
- **Pre-warming**: Optional, ensures data is ready immediately
|
|
171
|
+
|
|
172
|
+
## Security Considerations
|
|
173
|
+
|
|
174
|
+
- Generated files contain public keys only (safe to store)
|
|
175
|
+
- Entity statement files are signed JWTs (safe to store)
|
|
176
|
+
- Private keys are never written to files
|
|
177
|
+
- Files should be in `config/` directory (not publicly accessible)
|
|
178
|
+
|
|
179
|
+
## Troubleshooting
|
|
180
|
+
|
|
181
|
+
### Job fails with "Federation endpoint not configured"
|
|
182
|
+
- Ensure `FederationEndpoint.configure` is called in initializer
|
|
183
|
+
- Check that all required configuration is set
|
|
184
|
+
|
|
185
|
+
### Files not generated
|
|
186
|
+
- Check write permissions on `config/` directory
|
|
187
|
+
- Check Rails.logger for error messages
|
|
188
|
+
- Verify configuration is valid
|
|
189
|
+
|
|
190
|
+
### Cache not refreshing
|
|
191
|
+
- Check that Rails.cache is configured
|
|
192
|
+
- Verify cache keys match expected patterns
|
|
193
|
+
- Check cache TTL settings
|
|
194
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Background Job Example: Federation Cache Refresh
|
|
2
|
+
#
|
|
3
|
+
# This job refreshes cached federation data (entity statements, JWKS) without generating files.
|
|
4
|
+
# Useful for periodic cache refresh or after key rotation.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# # Schedule in config/schedule.rb (whenever gem)
|
|
8
|
+
# every 1.hour do
|
|
9
|
+
# runner 'FederationCacheRefreshJob.perform_later'
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# # Or call manually after key rotation
|
|
13
|
+
# FederationCacheRefreshJob.perform_later
|
|
14
|
+
#
|
|
15
|
+
# Requirements:
|
|
16
|
+
# - Rails application with ActiveJob configured
|
|
17
|
+
# - OmniauthOpenidFederation::FederationEndpoint configured
|
|
18
|
+
# - Rails.cache available
|
|
19
|
+
#
|
|
20
|
+
class FederationCacheRefreshJob < ApplicationJob
|
|
21
|
+
queue_as :default
|
|
22
|
+
|
|
23
|
+
# Refresh federation caches
|
|
24
|
+
#
|
|
25
|
+
# Clears all federation-related caches to force fresh data on next request.
|
|
26
|
+
# This is useful for:
|
|
27
|
+
# - Periodic cache refresh
|
|
28
|
+
# - After key rotation
|
|
29
|
+
# - When configuration changes
|
|
30
|
+
def perform
|
|
31
|
+
require "omniauth_openid_federation"
|
|
32
|
+
|
|
33
|
+
Rails.logger.info "[FederationCacheRefreshJob] Starting cache refresh..."
|
|
34
|
+
|
|
35
|
+
# Clear all federation caches
|
|
36
|
+
if Rails.cache.respond_to?(:delete_matched)
|
|
37
|
+
Rails.cache.delete_matched("federation:*")
|
|
38
|
+
Rails.logger.info "[FederationCacheRefreshJob] Cleared federation caches"
|
|
39
|
+
else
|
|
40
|
+
# Fallback: clear known cache keys
|
|
41
|
+
Rails.cache.delete("federation:jwks")
|
|
42
|
+
Rails.cache.delete("federation:signed_jwks")
|
|
43
|
+
Rails.cache.delete("federation:entity_statement")
|
|
44
|
+
Rails.logger.info "[FederationCacheRefreshJob] Cleared federation caches (fallback)"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Optionally: Pre-warm caches by generating data
|
|
48
|
+
# This ensures data is ready immediately after cache clear
|
|
49
|
+
begin
|
|
50
|
+
config = OmniauthOpenidFederation::FederationEndpoint.configuration
|
|
51
|
+
|
|
52
|
+
# Pre-warm JWKS cache
|
|
53
|
+
if config.current_jwks || config.current_jwks_proc
|
|
54
|
+
jwks = OmniauthOpenidFederation::FederationEndpoint.current_jwks
|
|
55
|
+
cache_key = "federation:jwks:#{Digest::SHA256.hexdigest(jwks.to_json)}"
|
|
56
|
+
Rails.cache.write(cache_key, jwks.to_json, expires_in: config.jwks_cache_ttl || 3600)
|
|
57
|
+
Rails.logger.info "[FederationCacheRefreshJob] Pre-warmed JWKS cache"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Pre-warm signed JWKS cache
|
|
61
|
+
signed_jwks = OmniauthOpenidFederation::FederationEndpoint.generate_signed_jwks
|
|
62
|
+
cache_key = "federation:signed_jwks:#{Digest::SHA256.hexdigest(signed_jwks)}"
|
|
63
|
+
config = OmniauthOpenidFederation::FederationEndpoint.configuration
|
|
64
|
+
Rails.cache.write(cache_key, signed_jwks, expires_in: config.jwks_cache_ttl || 3600)
|
|
65
|
+
Rails.logger.info "[FederationCacheRefreshJob] Pre-warmed signed JWKS cache"
|
|
66
|
+
rescue => e
|
|
67
|
+
Rails.logger.warn "[FederationCacheRefreshJob] Failed to pre-warm caches: #{e.message}"
|
|
68
|
+
# Don't fail the job if pre-warming fails - cache will be generated on-demand
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
Rails.logger.info "[FederationCacheRefreshJob] Completed successfully"
|
|
72
|
+
rescue => e
|
|
73
|
+
Rails.logger.error "[FederationCacheRefreshJob] Error: #{e.class} - #{e.message}"
|
|
74
|
+
Rails.logger.error "[FederationCacheRefreshJob] Backtrace: #{e.backtrace.first(5).join("\n")}"
|
|
75
|
+
raise
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Background Job Example: Federation Files Generation
|
|
2
|
+
#
|
|
3
|
+
# This job generates and caches federation files (entity statement, JWKS) for better performance.
|
|
4
|
+
# Schedule this job to run periodically (e.g., daily or when keys rotate).
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# # Schedule in config/schedule.rb (whenever gem)
|
|
8
|
+
# every 1.day, at: '2:00 am' do
|
|
9
|
+
# runner 'FederationFilesGenerationJob.perform_later'
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# # Or call manually
|
|
13
|
+
# FederationFilesGenerationJob.perform_later
|
|
14
|
+
#
|
|
15
|
+
# # Or perform synchronously
|
|
16
|
+
# FederationFilesGenerationJob.perform_now
|
|
17
|
+
#
|
|
18
|
+
# Requirements:
|
|
19
|
+
# - Rails application with ActiveJob configured
|
|
20
|
+
# - OmniauthOpenidFederation::FederationEndpoint configured
|
|
21
|
+
# - Write access to config/ directory
|
|
22
|
+
#
|
|
23
|
+
class FederationFilesGenerationJob < ApplicationJob
|
|
24
|
+
queue_as :default
|
|
25
|
+
|
|
26
|
+
# Generate and cache federation files
|
|
27
|
+
#
|
|
28
|
+
# Generates:
|
|
29
|
+
# - config/federation-entity-statement.jwt - Entity statement JWT file
|
|
30
|
+
# - config/federation-jwks.json - Current JWKS JSON file
|
|
31
|
+
#
|
|
32
|
+
# Also clears Rails cache for federation endpoints to ensure fresh data.
|
|
33
|
+
def perform
|
|
34
|
+
require "omniauth_openid_federation"
|
|
35
|
+
require "json"
|
|
36
|
+
|
|
37
|
+
config = OmniauthOpenidFederation::FederationEndpoint.configuration
|
|
38
|
+
|
|
39
|
+
# Validate configuration
|
|
40
|
+
unless config.issuer && config.private_key && config.jwks && config.metadata
|
|
41
|
+
Rails.logger.error "[FederationFilesGenerationJob] Federation endpoint not configured"
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Rails.logger.info "[FederationFilesGenerationJob] Starting federation files generation..."
|
|
46
|
+
|
|
47
|
+
# Generate entity statement file
|
|
48
|
+
begin
|
|
49
|
+
entity_statement = OmniauthOpenidFederation::FederationEndpoint.generate_entity_statement
|
|
50
|
+
entity_statement_path = Rails.root.join("config", ".federation-entity-statement.jwt")
|
|
51
|
+
File.write(entity_statement_path, entity_statement)
|
|
52
|
+
Rails.logger.info "[FederationFilesGenerationJob] Generated entity statement: #{entity_statement_path}"
|
|
53
|
+
rescue => e
|
|
54
|
+
Rails.logger.error "[FederationFilesGenerationJob] Failed to generate entity statement: #{e.message}"
|
|
55
|
+
raise
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Generate JWKS file
|
|
59
|
+
begin
|
|
60
|
+
jwks = OmniauthOpenidFederation::FederationEndpoint.current_jwks
|
|
61
|
+
jwks_path = Rails.root.join("config", ".federation-jwks.json")
|
|
62
|
+
File.write(jwks_path, JSON.pretty_generate(jwks))
|
|
63
|
+
Rails.logger.info "[FederationFilesGenerationJob] Generated JWKS: #{jwks_path}"
|
|
64
|
+
rescue => e
|
|
65
|
+
Rails.logger.error "[FederationFilesGenerationJob] Failed to generate JWKS: #{e.message}"
|
|
66
|
+
raise
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Clear federation caches to ensure fresh data on next request
|
|
70
|
+
if Rails.cache.respond_to?(:delete_matched)
|
|
71
|
+
Rails.cache.delete_matched("federation:*")
|
|
72
|
+
Rails.logger.info "[FederationFilesGenerationJob] Cleared federation caches"
|
|
73
|
+
else
|
|
74
|
+
# Fallback: clear known cache keys
|
|
75
|
+
Rails.cache.delete("federation:jwks")
|
|
76
|
+
Rails.cache.delete("federation:signed_jwks")
|
|
77
|
+
Rails.logger.info "[FederationFilesGenerationJob] Cleared federation caches (fallback)"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
Rails.logger.info "[FederationFilesGenerationJob] Completed successfully"
|
|
81
|
+
rescue => e
|
|
82
|
+
Rails.logger.error "[FederationFilesGenerationJob] Error: #{e.class} - #{e.message}"
|
|
83
|
+
Rails.logger.error "[FederationFilesGenerationJob] Backtrace: #{e.backtrace.first(5).join("\n")}"
|
|
84
|
+
raise
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|