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.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +16 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +822 -0
  5. data/SECURITY.md +129 -0
  6. data/examples/README_INTEGRATION_TESTING.md +399 -0
  7. data/examples/README_MOCK_OP.md +243 -0
  8. data/examples/app/controllers/users/omniauth_callbacks_controller.rb.example +33 -0
  9. data/examples/app/jobs/jwks_rotation_job.rb.example +60 -0
  10. data/examples/app/models/user.rb.example +39 -0
  11. data/examples/config/initializers/devise.rb.example +97 -0
  12. data/examples/config/initializers/federation_endpoint.rb.example +206 -0
  13. data/examples/config/mock_op.yml.example +83 -0
  14. data/examples/config/open_id_connect_config.rb.example +210 -0
  15. data/examples/config/routes.rb.example +12 -0
  16. data/examples/db/migrate/add_omniauth_to_users.rb.example +16 -0
  17. data/examples/integration_test_flow.rb +1334 -0
  18. data/examples/jobs/README.md +194 -0
  19. data/examples/jobs/federation_cache_refresh_job.rb.example +78 -0
  20. data/examples/jobs/federation_files_generation_job.rb.example +87 -0
  21. data/examples/mock_op_server.rb +775 -0
  22. data/examples/mock_rp_server.rb +435 -0
  23. data/lib/omniauth_openid_federation/access_token.rb +504 -0
  24. data/lib/omniauth_openid_federation/cache.rb +39 -0
  25. data/lib/omniauth_openid_federation/cache_adapter.rb +173 -0
  26. data/lib/omniauth_openid_federation/configuration.rb +135 -0
  27. data/lib/omniauth_openid_federation/constants.rb +13 -0
  28. data/lib/omniauth_openid_federation/endpoint_resolver.rb +168 -0
  29. data/lib/omniauth_openid_federation/entity_statement_reader.rb +122 -0
  30. data/lib/omniauth_openid_federation/errors.rb +52 -0
  31. data/lib/omniauth_openid_federation/federation/entity_statement.rb +331 -0
  32. data/lib/omniauth_openid_federation/federation/entity_statement_builder.rb +188 -0
  33. data/lib/omniauth_openid_federation/federation/entity_statement_fetcher.rb +142 -0
  34. data/lib/omniauth_openid_federation/federation/entity_statement_helper.rb +87 -0
  35. data/lib/omniauth_openid_federation/federation/entity_statement_parser.rb +198 -0
  36. data/lib/omniauth_openid_federation/federation/entity_statement_validator.rb +502 -0
  37. data/lib/omniauth_openid_federation/federation/metadata_policy_merger.rb +276 -0
  38. data/lib/omniauth_openid_federation/federation/signed_jwks.rb +210 -0
  39. data/lib/omniauth_openid_federation/federation/trust_chain_resolver.rb +225 -0
  40. data/lib/omniauth_openid_federation/federation_endpoint.rb +949 -0
  41. data/lib/omniauth_openid_federation/http_client.rb +70 -0
  42. data/lib/omniauth_openid_federation/instrumentation.rb +383 -0
  43. data/lib/omniauth_openid_federation/jwks/cache.rb +76 -0
  44. data/lib/omniauth_openid_federation/jwks/decode.rb +174 -0
  45. data/lib/omniauth_openid_federation/jwks/fetch.rb +153 -0
  46. data/lib/omniauth_openid_federation/jwks/normalizer.rb +49 -0
  47. data/lib/omniauth_openid_federation/jwks/rotate.rb +97 -0
  48. data/lib/omniauth_openid_federation/jwks/selector.rb +101 -0
  49. data/lib/omniauth_openid_federation/jws.rb +416 -0
  50. data/lib/omniauth_openid_federation/key_extractor.rb +173 -0
  51. data/lib/omniauth_openid_federation/logger.rb +99 -0
  52. data/lib/omniauth_openid_federation/rack_endpoint.rb +187 -0
  53. data/lib/omniauth_openid_federation/railtie.rb +29 -0
  54. data/lib/omniauth_openid_federation/rate_limiter.rb +55 -0
  55. data/lib/omniauth_openid_federation/strategy.rb +2029 -0
  56. data/lib/omniauth_openid_federation/string_helpers.rb +30 -0
  57. data/lib/omniauth_openid_federation/tasks_helper.rb +428 -0
  58. data/lib/omniauth_openid_federation/utils.rb +166 -0
  59. data/lib/omniauth_openid_federation/validators.rb +126 -0
  60. data/lib/omniauth_openid_federation/version.rb +3 -0
  61. data/lib/omniauth_openid_federation.rb +98 -0
  62. data/lib/tasks/omniauth_openid_federation.rake +376 -0
  63. data/sig/federation.rbs +218 -0
  64. data/sig/jwks.rbs +63 -0
  65. data/sig/omniauth_openid_federation.rbs +254 -0
  66. data/sig/strategy.rbs +60 -0
  67. 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
+