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,126 @@
|
|
|
1
|
+
# Input validation utilities for omniauth_openid_federation
|
|
2
|
+
module OmniauthOpenidFederation
|
|
3
|
+
module Validators
|
|
4
|
+
# Validate that a private key is present and valid
|
|
5
|
+
#
|
|
6
|
+
# @param private_key [OpenSSL::PKey::RSA, String, nil] The private key to validate
|
|
7
|
+
# @raise [ConfigurationError] If private key is missing or invalid
|
|
8
|
+
def self.validate_private_key!(private_key)
|
|
9
|
+
if private_key.nil?
|
|
10
|
+
raise ConfigurationError, "Private key is required for signed request objects"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Try to parse if it's a string
|
|
14
|
+
if private_key.is_a?(String)
|
|
15
|
+
begin
|
|
16
|
+
OpenSSL::PKey::RSA.new(private_key)
|
|
17
|
+
rescue => e
|
|
18
|
+
raise ConfigurationError, "Invalid private key format: #{e.message}"
|
|
19
|
+
end
|
|
20
|
+
elsif !private_key.is_a?(OpenSSL::PKey::RSA)
|
|
21
|
+
raise ConfigurationError, "Private key must be an OpenSSL::PKey::RSA instance or PEM string"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Validate that a URI is valid
|
|
28
|
+
#
|
|
29
|
+
# @param uri [String, URI, nil] The URI to validate
|
|
30
|
+
# @param required [Boolean] Whether the URI is required
|
|
31
|
+
# @raise [ConfigurationError] If URI is invalid or missing when required
|
|
32
|
+
def self.validate_uri!(uri, required: false)
|
|
33
|
+
if StringHelpers.blank?(uri)
|
|
34
|
+
if required
|
|
35
|
+
raise ConfigurationError, "URI is required"
|
|
36
|
+
end
|
|
37
|
+
return false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
parsed = URI.parse(uri.to_s)
|
|
42
|
+
unless parsed.is_a?(URI::HTTP) || parsed.is_a?(URI::HTTPS)
|
|
43
|
+
raise ConfigurationError, "URI must be HTTP or HTTPS: #{uri}"
|
|
44
|
+
end
|
|
45
|
+
true
|
|
46
|
+
rescue URI::InvalidURIError => e
|
|
47
|
+
raise ConfigurationError, "Invalid URI format: #{e.message}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Validate that a file path exists
|
|
52
|
+
#
|
|
53
|
+
# @param path [String, nil] The file path to validate
|
|
54
|
+
# @param required [Boolean] Whether the file is required
|
|
55
|
+
# @raise [ConfigurationError] If file is missing when required
|
|
56
|
+
def self.validate_file_path!(path, required: false)
|
|
57
|
+
if StringHelpers.blank?(path)
|
|
58
|
+
if required
|
|
59
|
+
raise ConfigurationError, "File path is required"
|
|
60
|
+
end
|
|
61
|
+
return false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
unless File.exist?(path)
|
|
65
|
+
if required
|
|
66
|
+
raise ConfigurationError, "File not found: #{path}"
|
|
67
|
+
end
|
|
68
|
+
return false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Validate client options hash
|
|
75
|
+
#
|
|
76
|
+
# @param client_options [Hash] The client options to validate
|
|
77
|
+
# @raise [ConfigurationError] If required options are missing
|
|
78
|
+
def self.validate_client_options!(client_options)
|
|
79
|
+
client_options ||= {}
|
|
80
|
+
|
|
81
|
+
# Normalize hash keys to symbols
|
|
82
|
+
normalized = normalize_hash(client_options)
|
|
83
|
+
|
|
84
|
+
# Validate required fields
|
|
85
|
+
if StringHelpers.blank?(normalized[:identifier])
|
|
86
|
+
raise ConfigurationError, "Client identifier is required"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if StringHelpers.blank?(normalized[:redirect_uri])
|
|
90
|
+
raise ConfigurationError, "Redirect URI is required"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Validate redirect URI format
|
|
94
|
+
validate_uri!(normalized[:redirect_uri], required: true)
|
|
95
|
+
|
|
96
|
+
# Validate private key
|
|
97
|
+
validate_private_key!(normalized[:private_key])
|
|
98
|
+
|
|
99
|
+
# Validate endpoints if provided
|
|
100
|
+
%i[authorization_endpoint token_endpoint jwks_uri].each do |endpoint|
|
|
101
|
+
if normalized.key?(endpoint) && !StringHelpers.blank?(normalized[endpoint])
|
|
102
|
+
# Endpoints can be paths or full URLs
|
|
103
|
+
endpoint_value = normalized[endpoint]
|
|
104
|
+
unless endpoint_value.to_s.start_with?("/", "http://", "https://")
|
|
105
|
+
raise ConfigurationError, "Invalid endpoint format for #{endpoint}: #{endpoint_value}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
normalized
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Normalize hash keys to symbols
|
|
114
|
+
#
|
|
115
|
+
# @param hash [Hash] The hash to normalize
|
|
116
|
+
# @return [Hash] Hash with symbol keys
|
|
117
|
+
def self.normalize_hash(hash)
|
|
118
|
+
return {} if hash.nil?
|
|
119
|
+
|
|
120
|
+
hash.each_with_object({}) do |(k, v), result|
|
|
121
|
+
key = k.is_a?(String) ? k.to_sym : k
|
|
122
|
+
result[key] = v
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# OmniAuth OpenID Federation
|
|
2
|
+
#
|
|
3
|
+
# Custom OmniAuth strategy for OpenID Federation providers using openid_connect gem
|
|
4
|
+
# supporting signed request objects, ID token encryption, and OpenID Federation.
|
|
5
|
+
#
|
|
6
|
+
# @see https://openid.net/specs/openid-federation-1_0.html
|
|
7
|
+
module OmniauthOpenidFederation
|
|
8
|
+
# Configure the gem
|
|
9
|
+
#
|
|
10
|
+
# @yield [config] Yields the configuration object
|
|
11
|
+
# @example
|
|
12
|
+
# OmniauthOpenidFederation.configure do |config|
|
|
13
|
+
# config.verify_ssl = false # Only for development
|
|
14
|
+
# config.cache_ttl = 3600
|
|
15
|
+
# end
|
|
16
|
+
def self.configure
|
|
17
|
+
yield(Configuration.config) if block_given?
|
|
18
|
+
Configuration.config
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Get the global configuration
|
|
22
|
+
#
|
|
23
|
+
# @return [Configuration] The configuration instance
|
|
24
|
+
def self.config
|
|
25
|
+
Configuration.config
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
require_relative "omniauth_openid_federation/version"
|
|
30
|
+
require_relative "omniauth_openid_federation/string_helpers"
|
|
31
|
+
require_relative "omniauth_openid_federation/logger"
|
|
32
|
+
require_relative "omniauth_openid_federation/configuration"
|
|
33
|
+
require_relative "omniauth_openid_federation/errors"
|
|
34
|
+
require_relative "omniauth_openid_federation/instrumentation"
|
|
35
|
+
require_relative "omniauth_openid_federation/validators"
|
|
36
|
+
require_relative "omniauth_openid_federation/key_extractor"
|
|
37
|
+
require_relative "omniauth_openid_federation/constants"
|
|
38
|
+
require_relative "omniauth_openid_federation/cache"
|
|
39
|
+
require_relative "omniauth_openid_federation/cache_adapter"
|
|
40
|
+
require_relative "omniauth_openid_federation/utils"
|
|
41
|
+
require_relative "omniauth_openid_federation/jws"
|
|
42
|
+
require_relative "omniauth_openid_federation/jwks/normalizer"
|
|
43
|
+
require_relative "omniauth_openid_federation/jwks/fetch"
|
|
44
|
+
require_relative "omniauth_openid_federation/jwks/decode"
|
|
45
|
+
require_relative "omniauth_openid_federation/jwks/cache"
|
|
46
|
+
require_relative "omniauth_openid_federation/jwks/selector"
|
|
47
|
+
require_relative "omniauth_openid_federation/jwks/rotate"
|
|
48
|
+
require_relative "omniauth_openid_federation/federation/entity_statement"
|
|
49
|
+
require_relative "omniauth_openid_federation/federation/entity_statement_fetcher"
|
|
50
|
+
require_relative "omniauth_openid_federation/federation/entity_statement_parser"
|
|
51
|
+
require_relative "omniauth_openid_federation/federation/entity_statement_validator"
|
|
52
|
+
require_relative "omniauth_openid_federation/federation/entity_statement_helper"
|
|
53
|
+
require_relative "omniauth_openid_federation/federation/entity_statement_builder"
|
|
54
|
+
require_relative "omniauth_openid_federation/federation/trust_chain_resolver"
|
|
55
|
+
require_relative "omniauth_openid_federation/federation/metadata_policy_merger"
|
|
56
|
+
require_relative "omniauth_openid_federation/federation/signed_jwks"
|
|
57
|
+
require_relative "omniauth_openid_federation/federation_endpoint"
|
|
58
|
+
require_relative "omniauth_openid_federation/rack_endpoint"
|
|
59
|
+
require_relative "omniauth_openid_federation/entity_statement_reader"
|
|
60
|
+
require_relative "omniauth_openid_federation/tasks_helper"
|
|
61
|
+
require_relative "omniauth_openid_federation/endpoint_resolver"
|
|
62
|
+
require_relative "omniauth_openid_federation/strategy"
|
|
63
|
+
require_relative "omniauth_openid_federation/access_token"
|
|
64
|
+
|
|
65
|
+
module OmniauthOpenidFederation
|
|
66
|
+
# Rotate JWKS cache for a provider
|
|
67
|
+
# This is useful for background jobs to proactively refresh keys
|
|
68
|
+
#
|
|
69
|
+
# @param jwks_uri [String] The JWKS URI to refresh
|
|
70
|
+
# @param entity_statement_path [String, nil] Path to entity statement file (optional)
|
|
71
|
+
# @return [Hash] The refreshed JWKS hash
|
|
72
|
+
# @raise [FetchError] If fetching fails
|
|
73
|
+
# @raise [ValidationError] If validation fails
|
|
74
|
+
# @example
|
|
75
|
+
# # Rotate JWKS for a provider
|
|
76
|
+
# OmniauthOpenidFederation.rotate_jwks(
|
|
77
|
+
# "https://provider.example.com/.well-known/jwks.json",
|
|
78
|
+
# entity_statement_path: "config/provider-entity-statement.jwt"
|
|
79
|
+
# )
|
|
80
|
+
def self.rotate_jwks(jwks_uri, entity_statement_path: nil)
|
|
81
|
+
Jwks::Rotate.run(jwks_uri, entity_statement_path: entity_statement_path)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Load Railtie for Rails integration (rake tasks, etc.)
|
|
86
|
+
if defined?(Rails)
|
|
87
|
+
require_relative "omniauth_openid_federation/railtie"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Create an alias for Devise's autoload mechanism
|
|
91
|
+
# Devise tries to autoload OpenidFederation (camelCase) from :openid_federation
|
|
92
|
+
# but our class is OpenIDFederation (with capital ID)
|
|
93
|
+
module OmniAuth
|
|
94
|
+
module Strategies
|
|
95
|
+
# Alias for Devise autoload compatibility
|
|
96
|
+
OpenidFederation = OpenIDFederation unless defined?(OpenidFederation)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# Rake tasks for OmniAuth OpenID Federation
|
|
2
|
+
# Thin wrappers around TasksHelper for CLI interface
|
|
3
|
+
|
|
4
|
+
namespace :openid_federation do
|
|
5
|
+
desc "Fetch entity statement from OpenID Federation provider"
|
|
6
|
+
task :fetch_entity_statement, [:url, :fingerprint, :output_file] => :environment do |_t, args|
|
|
7
|
+
require "omniauth_openid_federation"
|
|
8
|
+
|
|
9
|
+
url = args[:url] || ENV["ENTITY_STATEMENT_URL"]
|
|
10
|
+
fingerprint = args[:fingerprint] || ENV["ENTITY_STATEMENT_FINGERPRINT"]
|
|
11
|
+
output_file = args[:output_file] || ENV["ENTITY_STATEMENT_OUTPUT"] || "config/provider-entity-statement.jwt"
|
|
12
|
+
|
|
13
|
+
unless url
|
|
14
|
+
puts "â Entity statement URL is required"
|
|
15
|
+
puts " Usage: rake openid_federation:fetch_entity_statement[URL,FINGERPRINT,OUTPUT_FILE]"
|
|
16
|
+
puts " Or set: ENTITY_STATEMENT_URL, ENTITY_STATEMENT_FINGERPRINT, ENTITY_STATEMENT_OUTPUT"
|
|
17
|
+
exit 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
puts "Fetching entity statement from #{url}..."
|
|
21
|
+
puts "Output file: #{OmniauthOpenidFederation::TasksHelper.resolve_path(output_file)}"
|
|
22
|
+
|
|
23
|
+
if fingerprint
|
|
24
|
+
puts "Expected fingerprint: #{fingerprint}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
result = OmniauthOpenidFederation::TasksHelper.fetch_entity_statement(
|
|
29
|
+
url: url,
|
|
30
|
+
fingerprint: fingerprint,
|
|
31
|
+
output_file: output_file
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
puts "â
Entity statement saved to: #{result[:output_path]}"
|
|
35
|
+
puts "â
Fingerprint: #{result[:fingerprint]}"
|
|
36
|
+
|
|
37
|
+
metadata = result[:metadata]
|
|
38
|
+
puts "\nđ Entity Statement Metadata:"
|
|
39
|
+
puts " Issuer: #{metadata[:issuer]}"
|
|
40
|
+
puts " Authorization Endpoint: #{metadata[:metadata][:openid_provider][:authorization_endpoint]}"
|
|
41
|
+
puts " Token Endpoint: #{metadata[:metadata][:openid_provider][:token_endpoint]}"
|
|
42
|
+
puts " UserInfo Endpoint: #{metadata[:metadata][:openid_provider][:userinfo_endpoint]}"
|
|
43
|
+
puts " JWKS URI: #{metadata[:metadata][:openid_provider][:jwks_uri]}"
|
|
44
|
+
if metadata[:metadata][:openid_provider][:signed_jwks_uri]
|
|
45
|
+
puts " Signed JWKS URI: #{metadata[:metadata][:openid_provider][:signed_jwks_uri]}"
|
|
46
|
+
end
|
|
47
|
+
rescue OmniauthOpenidFederation::Federation::EntityStatement::FetchError => e
|
|
48
|
+
puts "â Error fetching entity statement: #{e.message}"
|
|
49
|
+
exit 1
|
|
50
|
+
rescue OmniauthOpenidFederation::Federation::EntityStatement::ValidationError => e
|
|
51
|
+
puts "â Validation error: #{e.message}"
|
|
52
|
+
puts " â ī¸ Entity statement fingerprint mismatch. Check provider documentation."
|
|
53
|
+
exit 1
|
|
54
|
+
rescue => e
|
|
55
|
+
puts "â Unexpected error: #{e.message}"
|
|
56
|
+
puts " #{e.class}: #{e.backtrace.first}"
|
|
57
|
+
exit 1
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
desc "Validate existing entity statement file"
|
|
62
|
+
task :validate_entity_statement, [:file_path, :fingerprint] => :environment do |_t, args|
|
|
63
|
+
require "omniauth_openid_federation"
|
|
64
|
+
|
|
65
|
+
file_path = args[:file_path] || ENV["ENTITY_STATEMENT_PATH"] || "config/provider-entity-statement.jwt"
|
|
66
|
+
expected_fingerprint = args[:fingerprint] || ENV["ENTITY_STATEMENT_FINGERPRINT"]
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
result = OmniauthOpenidFederation::TasksHelper.validate_entity_statement(
|
|
70
|
+
file_path: file_path,
|
|
71
|
+
expected_fingerprint: expected_fingerprint
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if expected_fingerprint
|
|
75
|
+
puts "â
Fingerprint matches: #{result[:fingerprint]}"
|
|
76
|
+
else
|
|
77
|
+
puts "đ Entity statement fingerprint: #{result[:fingerprint]}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
metadata = result[:metadata]
|
|
81
|
+
puts "\nđ Entity Statement Metadata:"
|
|
82
|
+
puts " Issuer: #{metadata[:issuer]}"
|
|
83
|
+
puts " Authorization Endpoint: #{metadata[:metadata][:openid_provider][:authorization_endpoint]}"
|
|
84
|
+
puts " Token Endpoint: #{metadata[:metadata][:openid_provider][:token_endpoint]}"
|
|
85
|
+
puts " UserInfo Endpoint: #{metadata[:metadata][:openid_provider][:userinfo_endpoint]}"
|
|
86
|
+
puts " JWKS URI: #{metadata[:metadata][:openid_provider][:jwks_uri]}"
|
|
87
|
+
if metadata[:metadata][:openid_provider][:signed_jwks_uri]
|
|
88
|
+
puts " Signed JWKS URI: #{metadata[:metadata][:openid_provider][:signed_jwks_uri]}"
|
|
89
|
+
end
|
|
90
|
+
rescue OmniauthOpenidFederation::ConfigurationError => e
|
|
91
|
+
puts "â #{e.message}"
|
|
92
|
+
exit 1
|
|
93
|
+
rescue OmniauthOpenidFederation::ValidationError => e
|
|
94
|
+
puts "â Validation error: #{e.message}"
|
|
95
|
+
exit 1
|
|
96
|
+
rescue => e
|
|
97
|
+
puts "â Error: #{e.message}"
|
|
98
|
+
puts " #{e.class}: #{e.backtrace.first}"
|
|
99
|
+
exit 1
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
desc "Fetch JWKS from provider"
|
|
104
|
+
task :fetch_jwks, [:jwks_uri, :output_file] => :environment do |_t, args|
|
|
105
|
+
require "omniauth_openid_federation"
|
|
106
|
+
|
|
107
|
+
jwks_uri = args[:jwks_uri] || ENV["JWKS_URI"]
|
|
108
|
+
output_file = args[:output_file] || ENV["JWKS_OUTPUT"] || "config/provider-jwks.json"
|
|
109
|
+
|
|
110
|
+
unless jwks_uri
|
|
111
|
+
puts "â JWKS URI is required"
|
|
112
|
+
puts " Usage: rake openid_federation:fetch_jwks[JWKS_URI,OUTPUT_FILE]"
|
|
113
|
+
puts " Or set: JWKS_URI, JWKS_OUTPUT"
|
|
114
|
+
exit 1
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
puts "Fetching JWKS from #{jwks_uri}..."
|
|
118
|
+
puts "Output file: #{OmniauthOpenidFederation::TasksHelper.resolve_path(output_file)}"
|
|
119
|
+
|
|
120
|
+
begin
|
|
121
|
+
result = OmniauthOpenidFederation::TasksHelper.fetch_jwks(
|
|
122
|
+
jwks_uri: jwks_uri,
|
|
123
|
+
output_file: output_file
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
jwks = result[:jwks]
|
|
127
|
+
puts "â
JWKS saved to: #{result[:output_path]}"
|
|
128
|
+
puts "â
Keys found: #{jwks&.[]("keys")&.length || 0}"
|
|
129
|
+
|
|
130
|
+
jwks&.[]("keys")&.each_with_index do |key, index|
|
|
131
|
+
puts " Key #{index + 1}:"
|
|
132
|
+
puts " - kid: #{key["kid"]}"
|
|
133
|
+
puts " - kty: #{key["kty"]}"
|
|
134
|
+
puts " - use: #{key["use"] || "not specified"}"
|
|
135
|
+
end
|
|
136
|
+
rescue => e
|
|
137
|
+
puts "â Error fetching JWKS: #{e.message}"
|
|
138
|
+
puts " #{e.class}: #{e.backtrace.first}"
|
|
139
|
+
exit 1
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
desc "Parse entity statement and display endpoints"
|
|
144
|
+
task :parse_entity_statement, [:file_path] => :environment do |_t, args|
|
|
145
|
+
require "omniauth_openid_federation"
|
|
146
|
+
require "json"
|
|
147
|
+
|
|
148
|
+
file_path = args[:file_path] || ENV["ENTITY_STATEMENT_PATH"] || "config/provider-entity-statement.jwt"
|
|
149
|
+
|
|
150
|
+
begin
|
|
151
|
+
metadata = OmniauthOpenidFederation::TasksHelper.parse_entity_statement(file_path: file_path)
|
|
152
|
+
|
|
153
|
+
puts "đ Entity Statement Metadata:"
|
|
154
|
+
puts JSON.pretty_generate(metadata)
|
|
155
|
+
rescue OmniauthOpenidFederation::ConfigurationError, OmniauthOpenidFederation::ValidationError => e
|
|
156
|
+
puts "â Error: #{e.message}"
|
|
157
|
+
exit 1
|
|
158
|
+
rescue => e
|
|
159
|
+
puts "â Error parsing entity statement: #{e.message}"
|
|
160
|
+
puts " #{e.class}: #{e.backtrace.first}"
|
|
161
|
+
exit 1
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
desc "Generate client keys and prepare public JWKS for provider registration"
|
|
166
|
+
task :prepare_client_keys, [:key_type, :output_dir] => :environment do |_t, args|
|
|
167
|
+
require "omniauth_openid_federation"
|
|
168
|
+
require "json"
|
|
169
|
+
require "fileutils"
|
|
170
|
+
|
|
171
|
+
key_type = (args[:key_type] || ENV["KEY_TYPE"] || "single").to_s.downcase
|
|
172
|
+
output_dir = args[:output_dir] || ENV["KEYS_OUTPUT_DIR"] || "config"
|
|
173
|
+
|
|
174
|
+
output_path = OmniauthOpenidFederation::TasksHelper.resolve_path(output_dir)
|
|
175
|
+
|
|
176
|
+
unless File.directory?(output_path)
|
|
177
|
+
FileUtils.mkdir_p(output_path)
|
|
178
|
+
puts "Created output directory: #{output_path}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
puts "Generating client keys..."
|
|
182
|
+
puts "Key type: #{key_type}"
|
|
183
|
+
puts "Output directory: #{output_path}"
|
|
184
|
+
|
|
185
|
+
begin
|
|
186
|
+
result = OmniauthOpenidFederation::TasksHelper.prepare_client_keys(
|
|
187
|
+
key_type: key_type,
|
|
188
|
+
output_dir: output_dir
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
puts "\nâ
Keys generated successfully:"
|
|
192
|
+
if key_type == "single"
|
|
193
|
+
puts " Private key: #{result[:private_key_path]}"
|
|
194
|
+
else
|
|
195
|
+
puts " Signing private key: #{result[:signing_key_path]}"
|
|
196
|
+
puts " Encryption private key: #{result[:encryption_key_path]}"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
puts " Public JWKS: #{result[:public_jwks_path]}"
|
|
200
|
+
puts "\nđ Send this JWKS to your provider for client registration:"
|
|
201
|
+
separator = "=" * 70
|
|
202
|
+
puts separator
|
|
203
|
+
puts JSON.pretty_generate(result[:jwks])
|
|
204
|
+
puts separator
|
|
205
|
+
|
|
206
|
+
puts "\nâ ī¸ SECURITY WARNING:"
|
|
207
|
+
puts " - Keep private keys secure! Never commit them to version control."
|
|
208
|
+
puts " - Add to .gitignore: config/*-private-key.pem"
|
|
209
|
+
puts " - Prefer storing secrets in secret vaults (1Password, HashiCorp Vault, etc.)"
|
|
210
|
+
puts " - Only send the public JWKS (client-jwks.json) to your provider"
|
|
211
|
+
rescue ArgumentError => e
|
|
212
|
+
puts "â #{e.message}"
|
|
213
|
+
puts " Valid options: 'single' (one key for both signing/encryption) or 'separate' (two keys)"
|
|
214
|
+
exit 1
|
|
215
|
+
rescue => e
|
|
216
|
+
puts "â Error generating keys: #{e.message}"
|
|
217
|
+
puts " #{e.class}: #{e.backtrace.first}"
|
|
218
|
+
exit 1
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
desc "Test local entity statement endpoint and all linked endpoints"
|
|
223
|
+
task :test_local_endpoint, [:base_url] => :environment do |_t, args|
|
|
224
|
+
require "omniauth_openid_federation"
|
|
225
|
+
|
|
226
|
+
base_url = args[:base_url] || ENV["BASE_URL"] || "http://localhost:3000"
|
|
227
|
+
|
|
228
|
+
puts "=" * 80
|
|
229
|
+
puts "Testing Local Entity Statement Endpoint"
|
|
230
|
+
puts "=" * 80
|
|
231
|
+
puts
|
|
232
|
+
puts "Base URL: #{base_url}"
|
|
233
|
+
puts "Entity Statement URL: #{base_url}/.well-known/openid-federation"
|
|
234
|
+
puts
|
|
235
|
+
|
|
236
|
+
begin
|
|
237
|
+
result = OmniauthOpenidFederation::TasksHelper.test_local_endpoint(base_url: base_url)
|
|
238
|
+
|
|
239
|
+
entity_statement = result[:entity_statement]
|
|
240
|
+
metadata = result[:metadata]
|
|
241
|
+
results = result[:results]
|
|
242
|
+
key_status = result[:key_status]
|
|
243
|
+
validation_warnings = result[:validation_warnings] || []
|
|
244
|
+
|
|
245
|
+
puts "đĨ Step 1: Fetching entity statement..."
|
|
246
|
+
if validation_warnings.any? { |w| w.include?("fetch") || w.include?("Fetch") }
|
|
247
|
+
puts "â ī¸ Entity statement fetched with warnings"
|
|
248
|
+
else
|
|
249
|
+
puts "â
Entity statement fetched successfully"
|
|
250
|
+
end
|
|
251
|
+
puts " Fingerprint: #{entity_statement.fingerprint}"
|
|
252
|
+
puts
|
|
253
|
+
|
|
254
|
+
puts "đ Step 2: Parsing entity statement..."
|
|
255
|
+
if validation_warnings.any?
|
|
256
|
+
puts "â ī¸ Entity statement parsed with validation warnings:"
|
|
257
|
+
validation_warnings.each do |warning|
|
|
258
|
+
puts " â ī¸ #{warning}"
|
|
259
|
+
end
|
|
260
|
+
else
|
|
261
|
+
puts "â
Entity statement parsed successfully"
|
|
262
|
+
end
|
|
263
|
+
puts " Issuer: #{metadata[:issuer]}"
|
|
264
|
+
puts " Subject: #{metadata[:sub]}"
|
|
265
|
+
puts " Expires: #{Time.at(metadata[:exp])}" if metadata[:exp]
|
|
266
|
+
puts " Issued At: #{Time.at(metadata[:iat])}" if metadata[:iat]
|
|
267
|
+
puts
|
|
268
|
+
|
|
269
|
+
# Key status information
|
|
270
|
+
if key_status
|
|
271
|
+
puts "đ Key Configuration:"
|
|
272
|
+
case key_status[:type]
|
|
273
|
+
when :single
|
|
274
|
+
puts " Status: Single key detected (#{key_status[:count]} key(s))"
|
|
275
|
+
when :separate
|
|
276
|
+
puts " Status: Separate keys detected (#{key_status[:count]} key(s))"
|
|
277
|
+
else
|
|
278
|
+
puts " Status: #{key_status[:type]} (#{key_status[:count]} key(s))"
|
|
279
|
+
end
|
|
280
|
+
puts " #{key_status[:recommendation]}"
|
|
281
|
+
puts
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
puts "đ Step 3: Testing endpoints from entity statement..."
|
|
285
|
+
puts " Found #{results.length} endpoint(s) to test"
|
|
286
|
+
puts
|
|
287
|
+
|
|
288
|
+
results.each do |name, result_data|
|
|
289
|
+
puts " Testing: #{name}"
|
|
290
|
+
case result_data[:status]
|
|
291
|
+
when :success
|
|
292
|
+
if result_data[:keys]
|
|
293
|
+
puts " â
JWKS fetched successfully (#{result_data[:keys]} key(s))"
|
|
294
|
+
else
|
|
295
|
+
puts " â
Endpoint accessible (HTTP #{result_data[:code]})"
|
|
296
|
+
end
|
|
297
|
+
when :warning
|
|
298
|
+
warning_msg = "HTTP #{result_data[:code]}"
|
|
299
|
+
if result_data[:code] == "404" && result_data[:body]
|
|
300
|
+
body_text = result_data[:body].strip
|
|
301
|
+
warning_msg += " - #{body_text}" if body_text.length > 0
|
|
302
|
+
end
|
|
303
|
+
puts " â ī¸ Endpoint returned #{warning_msg}"
|
|
304
|
+
when :error
|
|
305
|
+
puts " â Error: #{result_data[:message]}"
|
|
306
|
+
end
|
|
307
|
+
puts
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Summary
|
|
311
|
+
puts "=" * 80
|
|
312
|
+
puts "Test Summary"
|
|
313
|
+
puts "=" * 80
|
|
314
|
+
puts
|
|
315
|
+
|
|
316
|
+
success_count = results.values.count { |r| r[:status] == :success }
|
|
317
|
+
warning_count = results.values.count { |r| r[:status] == :warning }
|
|
318
|
+
error_count = results.values.count { |r| r[:status] == :error }
|
|
319
|
+
|
|
320
|
+
results.each do |name, result_data|
|
|
321
|
+
case result_data[:status]
|
|
322
|
+
when :success
|
|
323
|
+
puts "â
#{name}: #{result_data[:keys] ? "#{result_data[:keys]} key(s)" : "OK"}"
|
|
324
|
+
when :warning
|
|
325
|
+
warning_msg = "HTTP #{result_data[:code]}"
|
|
326
|
+
if result_data[:code] == "404" && result_data[:body]
|
|
327
|
+
body_text = result_data[:body].strip
|
|
328
|
+
warning_msg += " - #{body_text}" if body_text.length > 0
|
|
329
|
+
end
|
|
330
|
+
puts "â ī¸ #{name}: #{warning_msg}"
|
|
331
|
+
when :error
|
|
332
|
+
puts "â #{name}: #{result_data[:message]}"
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
puts
|
|
337
|
+
if results.empty?
|
|
338
|
+
puts "No endpoints found to test in entity statement."
|
|
339
|
+
if validation_warnings.any?
|
|
340
|
+
puts "Validation warnings: #{validation_warnings.length}"
|
|
341
|
+
end
|
|
342
|
+
puts
|
|
343
|
+
if validation_warnings.any?
|
|
344
|
+
puts "â ī¸ Entity statement has validation warnings (see above)."
|
|
345
|
+
puts " Review the warnings and fix any issues before deploying to production."
|
|
346
|
+
else
|
|
347
|
+
puts "âšī¸ Entity statement parsed successfully, but no testable endpoints found."
|
|
348
|
+
end
|
|
349
|
+
else
|
|
350
|
+
puts "Total: #{success_count} successful, #{warning_count} warning(s), #{error_count} error(s)"
|
|
351
|
+
if validation_warnings.any?
|
|
352
|
+
puts "Validation warnings: #{validation_warnings.length}"
|
|
353
|
+
end
|
|
354
|
+
puts
|
|
355
|
+
|
|
356
|
+
if error_count > 0
|
|
357
|
+
puts "â ī¸ Some endpoints failed. Check the errors above."
|
|
358
|
+
exit 1
|
|
359
|
+
elsif validation_warnings.any?
|
|
360
|
+
puts "â ī¸ Entity statement has validation warnings (see above), but endpoints were tested."
|
|
361
|
+
puts " Review the warnings and fix any issues before deploying to production."
|
|
362
|
+
else
|
|
363
|
+
puts "â
All endpoints tested successfully!"
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
rescue OmniauthOpenidFederation::Federation::EntityStatement::FetchError => e
|
|
367
|
+
puts "â Error fetching entity statement: #{e.message}"
|
|
368
|
+
puts " Make sure the server is running and the endpoint is accessible"
|
|
369
|
+
exit 1
|
|
370
|
+
rescue => e
|
|
371
|
+
puts "â Unexpected error: #{e.message}"
|
|
372
|
+
puts " #{e.class}: #{e.backtrace.first(3).join("\n ")}"
|
|
373
|
+
exit 1
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|