omniauth_openid_federation 1.2.2
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 +44 -0
- data/LICENSE.md +22 -0
- data/README.md +922 -0
- data/SECURITY.md +28 -0
- data/app/controllers/omniauth_openid_federation/federation_controller.rb +160 -0
- data/config/routes.rb +17 -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 +37 -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 +131 -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/engine.rb +17 -0
- data/lib/omniauth_openid_federation/entity_statement_reader.rb +129 -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 +399 -0
- data/lib/omniauth_openid_federation/jwks/cache.rb +76 -0
- data/lib/omniauth_openid_federation/jwks/decode.rb +175 -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 +410 -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 +15 -0
- data/lib/omniauth_openid_federation/rate_limiter.rb +55 -0
- data/lib/omniauth_openid_federation/strategy.rb +2114 -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 +168 -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 +99 -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 +361 -0
|
@@ -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
|
+
|
|
@@ -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
|
+
|