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,83 @@
1
+ # Mock OpenID Provider (OP) Server Configuration
2
+ #
3
+ # Copy this file to config/mock_op.yml and customize for your testing needs
4
+
5
+ # Entity Identifier (Entity ID) for the OP
6
+ entity_id: "https://op.example.com"
7
+
8
+ # Server host and port
9
+ server_host: "localhost:9292"
10
+
11
+ # Private key for signing entity statements and ID tokens
12
+ # Can be:
13
+ # - PEM format (with BEGIN/END markers)
14
+ # - Base64 encoded PEM
15
+ # - Path to key file (not recommended for production)
16
+ # If not provided, a new key will be generated (for testing only)
17
+ signing_key: |
18
+ -----BEGIN RSA PRIVATE KEY-----
19
+ MIIEpAIBAAKCAQEA...
20
+ -----END RSA PRIVATE KEY-----
21
+
22
+ # Trust Anchors for validating incoming RPs
23
+ # Each trust anchor must have:
24
+ # - entity_id: Trust Anchor Entity Identifier
25
+ # - jwks: Trust Anchor JWKS for validation
26
+ trust_anchors:
27
+ - entity_id: "https://ta.example.com"
28
+ jwks:
29
+ keys:
30
+ - kty: "RSA"
31
+ use: "sig"
32
+ kid: "ta-key-1"
33
+ n: "..."
34
+ e: "AQAB"
35
+
36
+ # Authority hints (for OP's Entity Configuration)
37
+ # List of Immediate Superiors' Entity Identifiers
38
+ # Leave empty if this OP is a Trust Anchor
39
+ authority_hints:
40
+ - "https://federation.example.com"
41
+
42
+ # OP Metadata (OpenID Provider metadata)
43
+ op_metadata:
44
+ issuer: "https://op.example.com"
45
+ authorization_endpoint: "https://op.example.com/auth"
46
+ token_endpoint: "https://op.example.com/token"
47
+ userinfo_endpoint: "https://op.example.com/userinfo"
48
+ jwks_uri: "https://op.example.com/.well-known/jwks.json"
49
+ signed_jwks_uri: "https://op.example.com/.well-known/signed-jwks.json"
50
+ client_registration_types_supported:
51
+ - "automatic"
52
+ - "explicit"
53
+ response_types_supported:
54
+ - "code"
55
+ grant_types_supported:
56
+ - "authorization_code"
57
+ id_token_signing_alg_values_supported:
58
+ - "RS256"
59
+ scopes_supported:
60
+ - "openid"
61
+ - "profile"
62
+ - "email"
63
+
64
+ # Subordinate Statements (for Fetch Endpoint)
65
+ # Maps subject Entity IDs to their Subordinate Statement configuration
66
+ subordinate_statements:
67
+ "https://rp.example.com":
68
+ metadata:
69
+ openid_relying_party:
70
+ redirect_uris:
71
+ - "https://rp.example.com/callback"
72
+ client_registration_types:
73
+ - "automatic"
74
+ application_type: "web"
75
+ metadata_policy:
76
+ openid_relying_party:
77
+ grant_types:
78
+ subset_of:
79
+ - "authorization_code"
80
+ response_types:
81
+ subset_of:
82
+ - "code"
83
+
@@ -0,0 +1,210 @@
1
+ # OpenID Connect Configuration Class
2
+ # This example shows how to structure OpenID Federation configuration
3
+ # using a configuration class pattern (similar to Anyway::Config)
4
+ #
5
+ # Usage:
6
+ # 1. Copy this file to config/configs/open_id_connect_config.rb
7
+ # 2. Customize the configuration attributes for your needs
8
+ # 3. Use it in your initializers (see federation_endpoint.rb.example)
9
+ #
10
+ # Environment Variables:
11
+ # OPEN_ID_CONNECT_ENABLED=true
12
+ # OPEN_ID_CONNECT_CLIENT_ID=your-client-id
13
+ # OPEN_ID_CONNECT_PRIVATE_KEY_PATH=config/client-private-key.pem
14
+ # OPEN_ID_CONNECT_SIGNING_KEY_PATH=config/client-signing-private-key.pem
15
+ # OPEN_ID_CONNECT_ENCRYPTION_KEY_PATH=config/client-encryption-private-key.pem
16
+ # OPEN_ID_CONNECT_REDIRECT_URI=https://your-app.example.com/users/auth/openid_federation/callback
17
+ # OPEN_ID_CONNECT_ENTITY_STATEMENT_URL=https://provider.example.com/.well-known/openid-federation
18
+ # OPEN_ID_CONNECT_CLIENT_ENTITY_STATEMENT_PATH=config/client-entity-statement.jwt
19
+ # OPEN_ID_CONNECT_CLIENT_ENTITY_STATEMENT_URL=https://your-app.example.com/.well-known/openid-federation
20
+ # OPEN_ID_CONNECT_ORGANIZATION_NAME=Your Organization Name
21
+
22
+ require "openssl"
23
+ require "json"
24
+
25
+ # Base configuration class (if using Anyway::Config gem)
26
+ # If not using Anyway::Config, you can use a simpler pattern:
27
+ #
28
+ # class ApplicationConfig
29
+ # class << self
30
+ # delegate_missing_to :instance
31
+ #
32
+ # private
33
+ #
34
+ # def instance
35
+ # @instance ||= new
36
+ # end
37
+ # end
38
+ #
39
+ # def enabled?
40
+ # ["true", "1"].include?(ENV["#{config_name.to_s.upcase}_ENABLED"]&.to_s&.downcase)
41
+ # end
42
+ # end
43
+
44
+ class OpenIdConnectConfig < ApplicationConfig
45
+ config_name :open_id_connect
46
+
47
+ # Configuration attributes
48
+ # These can be set via environment variables with OPEN_ID_CONNECT_ prefix
49
+ # or via YAML files (if using Anyway::Config)
50
+ attr_config :enabled,
51
+ :client_id,
52
+ :private_key_path,
53
+ :private_key_base64,
54
+ :signing_key_path,
55
+ :signing_key_base64,
56
+ :encryption_key_path,
57
+ :encryption_key_base64,
58
+ :redirect_uri,
59
+ :entity_statement_path,
60
+ :entity_statement_url,
61
+ :entity_statement_fingerprint,
62
+ :client_entity_statement_path,
63
+ :client_entity_statement_url,
64
+ :client_entity_identifier,
65
+ :ftn_spname,
66
+ :acr_values,
67
+ :organization_name
68
+
69
+ # Load private key from file or base64 encoded string
70
+ # Used for both signing and encryption (DEV/TESTING ONLY)
71
+ # For production, use separate signing_key and encryption_key
72
+ def private_key
73
+ @private_key ||= begin
74
+ private_key_pem = if private_key_base64.present?
75
+ Base64.decode64(private_key_base64)
76
+ elsif private_key_path.present?
77
+ File.read(Rails.root.join(private_key_path))
78
+ end
79
+
80
+ raise "Private key not found. Set OPEN_ID_CONNECT_PRIVATE_KEY_PATH or OPEN_ID_CONNECT_PRIVATE_KEY_BASE64" if private_key_pem.blank?
81
+
82
+ OpenSSL::PKey::RSA.new(private_key_pem)
83
+ end
84
+ end
85
+
86
+ # Load signing key from file or base64 encoded string
87
+ # RECOMMENDED for production: Use separate signing key
88
+ def signing_key
89
+ return nil if signing_key_path.blank? && signing_key_base64.blank?
90
+
91
+ @signing_key ||= begin
92
+ signing_key_pem = if signing_key_base64.present?
93
+ Base64.decode64(signing_key_base64)
94
+ elsif signing_key_path.present?
95
+ File.read(Rails.root.join(signing_key_path))
96
+ end
97
+
98
+ return nil if signing_key_pem.blank?
99
+
100
+ OpenSSL::PKey::RSA.new(signing_key_pem)
101
+ end
102
+ end
103
+
104
+ # Load encryption key from file or base64 encoded string
105
+ # RECOMMENDED for production: Use separate encryption key
106
+ def encryption_key
107
+ return nil if encryption_key_path.blank? && encryption_key_base64.blank?
108
+
109
+ @encryption_key ||= begin
110
+ encryption_key_pem = if encryption_key_base64.present?
111
+ Base64.decode64(encryption_key_base64)
112
+ elsif encryption_key_path.present?
113
+ File.read(Rails.root.join(encryption_key_path))
114
+ end
115
+
116
+ return nil if encryption_key_pem.blank?
117
+
118
+ OpenSSL::PKey::RSA.new(encryption_key_pem)
119
+ end
120
+ end
121
+
122
+ # Note: client_jwk_signing_key is now automatically extracted by the
123
+ # omniauth_openid_federation gem from client_entity_statement_path or
124
+ # client_entity_statement_url when provided.
125
+ # No manual extraction needed - the library handles this automatically.
126
+
127
+ # OpenID Federation 1.0 Section 9: Entity Configuration endpoint
128
+ # Entity statements MUST be available at /.well-known/openid-federation
129
+ # URL is the source of truth per OpenID Federation spec
130
+ def entity_statement_absolute_path
131
+ return nil if entity_statement_path.blank?
132
+ @entity_statement_absolute_path ||= Rails.root.join(entity_statement_path)
133
+ end
134
+
135
+ # Get provider entity statement URL
136
+ # OpenID Federation 1.0 Section 9: MUST be at /.well-known/openid-federation
137
+ # URL is always required per OpenID Federation specification
138
+ def entity_statement_url
139
+ return values[:entity_statement_url] if values[:entity_statement_url].present?
140
+ nil # Return nil if not configured - must be provided via environment variable
141
+ end
142
+
143
+ # Get client entity statement absolute path
144
+ def client_entity_statement_absolute_path
145
+ return nil if client_entity_statement_path.blank?
146
+ @client_entity_statement_absolute_path ||= Rails.root.join(client_entity_statement_path)
147
+ end
148
+
149
+ # Get client entity statement path for strategy configuration
150
+ # Prefers URL over path - only returns path if URL is not available
151
+ def client_entity_statement_path_for_strategy
152
+ return nil if client_entity_statement_url.present?
153
+ client_entity_statement_absolute_path
154
+ end
155
+
156
+ # Get client registration type based on entity statement configuration
157
+ # :automatic - Uses entity statement for automatic client registration
158
+ # :explicit - Uses explicit client_id and manual registration
159
+ def client_registration_type
160
+ (values[:client_entity_statement_url].present? || values[:client_entity_statement_path].present?) ? :automatic : :explicit
161
+ end
162
+
163
+ # Generate client entity statement URL
164
+ # URL is for external consumers (providers) - we never access it ourselves
165
+ # Uses standard path /.well-known/openid-federation (mounted by library)
166
+ def client_entity_statement_url
167
+ return values[:client_entity_statement_url] if values[:client_entity_statement_url].present?
168
+ # Default to standard federation endpoint if not explicitly configured
169
+ app_url = ENV["APP_URL"] || "https://your-app.example.com"
170
+ "#{app_url}/.well-known/openid-federation"
171
+ end
172
+ end
173
+
174
+ # Usage in initializer (config/initializers/omniauth_openid_federation.rb):
175
+ #
176
+ # open_id_config = OpenIdConnectConfig.new
177
+ # if open_id_config.enabled? && open_id_config.private_key.present?
178
+ # app_url = ENV["APP_URL"] || "https://your-app.example.com"
179
+ #
180
+ # OmniauthOpenidFederation::FederationEndpoint.auto_configure(
181
+ # issuer: app_url,
182
+ # # Production (RECOMMENDED): Use separate keys
183
+ # signing_key: open_id_config.signing_key,
184
+ # encryption_key: open_id_config.encryption_key,
185
+ # # Development/Testing (NOT RECOMMENDED FOR PRODUCTION): Single key
186
+ # # private_key: open_id_config.private_key,
187
+ # entity_statement_path: open_id_config.client_entity_statement_absolute_path,
188
+ # metadata: {
189
+ # openid_relying_party: {
190
+ # redirect_uris: [
191
+ # open_id_config.redirect_uri
192
+ # ],
193
+ # client_registration_types: ["automatic"],
194
+ # application_type: "web",
195
+ # grant_types: ["authorization_code"],
196
+ # response_types: ["code"],
197
+ # token_endpoint_auth_method: "private_key_jwt",
198
+ # token_endpoint_auth_signing_alg: "RS256",
199
+ # request_object_signing_alg: "RS256",
200
+ # id_token_encrypted_response_alg: "RSA-OAEP",
201
+ # id_token_encrypted_response_enc: "A128CBC-HS256",
202
+ # organization_name: open_id_config.organization_name
203
+ # }
204
+ # },
205
+ # expiration_seconds: (ENV["FEDERATION_EXPIRATION_SECONDS"] || 86400).to_i,
206
+ # jwks_cache_ttl: (ENV["FEDERATION_JWKS_CACHE_TTL"] || 3600).to_i,
207
+ # auto_provision_keys: true
208
+ # )
209
+ # end
210
+
@@ -0,0 +1,12 @@
1
+ # Example routes.rb showing how to mount the federation endpoint
2
+ Rails.application.routes.draw do
3
+ # Mount the federation endpoint (required for serving entity statements)
4
+ # This enables the /.well-known/openid-federation endpoint
5
+ OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)
6
+
7
+ # Or mount manually:
8
+ # get "/.well-known/openid-federation", to: "omniauth_openid_federation/federation#show", as: :openid_federation
9
+
10
+ # Your other routes...
11
+ end
12
+
@@ -0,0 +1,16 @@
1
+ # Example migration for adding OmniAuth fields to users table
2
+ # Copy this to db/migrate/XXXXXX_add_omniauth_to_users.rb
3
+
4
+ class AddOmniauthToUsers < ActiveRecord::Migration[7.0]
5
+ def change
6
+ add_column :users, :provider, :string
7
+ add_column :users, :uid, :string
8
+ add_index :users, [:provider, :uid], unique: true
9
+
10
+ # Optional: add user info fields if not already present
11
+ add_column :users, :name, :string unless column_exists?(:users, :name)
12
+ add_column :users, :first_name, :string unless column_exists?(:users, :first_name)
13
+ add_column :users, :last_name, :string unless column_exists?(:users, :last_name)
14
+ end
15
+ end
16
+