omniauth_openid_federation 1.2.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.
Potentially problematic release.
This version of omniauth_openid_federation might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/CHANGELOG.md +36 -0
- data/LICENSE.md +22 -0
- data/README.md +922 -0
- data/SECURITY.md +28 -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 +21 -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 +359 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Background job for proactive JWKS key rotation
|
|
2
|
+
# This job runs periodically to refresh JWKS cache before expiration,
|
|
3
|
+
# ensuring keys are always up-to-date without blocking client requests.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# 1. Schedule this job to run periodically (e.g., every 12 hours)
|
|
7
|
+
# 2. Configure cache TTL to be longer than job frequency (e.g., 24 hours)
|
|
8
|
+
# 3. This ensures keys are refreshed proactively, not reactively
|
|
9
|
+
#
|
|
10
|
+
# Example scheduling (using GoodJob, Sidekiq, or similar):
|
|
11
|
+
#
|
|
12
|
+
# # config/initializers/schedule.rb (for GoodJob)
|
|
13
|
+
# GoodJob::Cron::Schedule.add("jwks_rotation", {
|
|
14
|
+
# cron: "0 */12 * * *", # Every 12 hours
|
|
15
|
+
# class: "JwksRotationJob"
|
|
16
|
+
# })
|
|
17
|
+
#
|
|
18
|
+
# # Or with Sidekiq-Cron
|
|
19
|
+
# Sidekiq::Cron::Job.create(
|
|
20
|
+
# name: "JWKS Rotation",
|
|
21
|
+
# cron: "0 */12 * * *",
|
|
22
|
+
# class: "JwksRotationJob"
|
|
23
|
+
# )
|
|
24
|
+
class JwksRotationJob < ApplicationJob
|
|
25
|
+
queue_as :default
|
|
26
|
+
|
|
27
|
+
# Rotate JWKS for a specific provider
|
|
28
|
+
#
|
|
29
|
+
# @param jwks_uri [String] The JWKS URI to refresh
|
|
30
|
+
# @param entity_statement_path [String, nil] Path to entity statement for signed JWKS
|
|
31
|
+
def perform(jwks_uri, entity_statement_path: nil)
|
|
32
|
+
OmniauthOpenidFederation.rotate_jwks(jwks_uri, entity_statement_path: entity_statement_path)
|
|
33
|
+
rescue => e
|
|
34
|
+
# Log error but don't fail - request-level rotation will handle it
|
|
35
|
+
Rails.logger.error("[JwksRotationJob] Failed to rotate JWKS for #{jwks_uri}: #{e.class} - #{e.message}")
|
|
36
|
+
# Optionally, send to error tracking service (Sentry, Rollbar, etc.)
|
|
37
|
+
# Sentry.capture_exception(e) if defined?(Sentry)
|
|
38
|
+
raise # Re-raise to allow job retry if configured
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Rotate JWKS for all configured providers
|
|
42
|
+
# This is useful when you have multiple providers configured
|
|
43
|
+
def self.rotate_all
|
|
44
|
+
# Example: Get all providers from Devise config
|
|
45
|
+
providers = Devise.omniauth_configs.keys.select { |k| k.to_s.start_with?("openid") }
|
|
46
|
+
|
|
47
|
+
providers.each do |provider_name|
|
|
48
|
+
config = Devise.omniauth_configs[provider_name]
|
|
49
|
+
options = config.options
|
|
50
|
+
client_options = options[:client_options] || options["client_options"] || {}
|
|
51
|
+
jwks_uri = client_options[:jwks_uri] || client_options["jwks_uri"]
|
|
52
|
+
entity_statement_path = options[:entity_statement_path] || options["entity_statement_path"]
|
|
53
|
+
|
|
54
|
+
if jwks_uri
|
|
55
|
+
perform_later(jwks_uri, entity_statement_path: entity_statement_path)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Example User model with OmniAuth integration
|
|
2
|
+
# Add these methods to your existing User model
|
|
3
|
+
|
|
4
|
+
class User < ApplicationRecord
|
|
5
|
+
# Required columns (add via migration):
|
|
6
|
+
# - provider (string)
|
|
7
|
+
# - uid (string)
|
|
8
|
+
# - email (string)
|
|
9
|
+
# - name (string)
|
|
10
|
+
# - first_name (string, optional)
|
|
11
|
+
# - last_name (string, optional)
|
|
12
|
+
|
|
13
|
+
def self.find_or_create_from_omniauth(auth)
|
|
14
|
+
user = find_by(provider: auth.provider, uid: auth.uid)
|
|
15
|
+
|
|
16
|
+
if user
|
|
17
|
+
# Update existing user info
|
|
18
|
+
user.update(
|
|
19
|
+
email: auth.info.email,
|
|
20
|
+
name: auth.info.name,
|
|
21
|
+
first_name: auth.info.first_name,
|
|
22
|
+
last_name: auth.info.last_name
|
|
23
|
+
)
|
|
24
|
+
else
|
|
25
|
+
# Create new user
|
|
26
|
+
user = create(
|
|
27
|
+
provider: auth.provider,
|
|
28
|
+
uid: auth.uid,
|
|
29
|
+
email: auth.info.email,
|
|
30
|
+
name: auth.info.name,
|
|
31
|
+
first_name: auth.info.first_name,
|
|
32
|
+
last_name: auth.info.last_name
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
user
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Example Devise configuration for OmniAuth OpenID Federation
|
|
2
|
+
# Copy this to config/initializers/devise.rb and customize for your provider
|
|
3
|
+
|
|
4
|
+
require "omniauth_openid_federation"
|
|
5
|
+
|
|
6
|
+
# Configure global settings (optional but recommended)
|
|
7
|
+
OmniauthOpenidFederation.configure do |config|
|
|
8
|
+
# Security instrumentation - get notified about security events, MITM attacks, etc.
|
|
9
|
+
# Example with Sentry:
|
|
10
|
+
# config.instrumentation = ->(event, data) do
|
|
11
|
+
# Sentry.capture_message(
|
|
12
|
+
# "OpenID Federation: #{event}",
|
|
13
|
+
# level: data[:severity] == :error ? :error : :warning,
|
|
14
|
+
# extra: data
|
|
15
|
+
# )
|
|
16
|
+
# end
|
|
17
|
+
|
|
18
|
+
# Example with Honeybadger:
|
|
19
|
+
# config.instrumentation = ->(event, data) do
|
|
20
|
+
# Honeybadger.notify("OpenID Federation: #{event}", context: data)
|
|
21
|
+
# end
|
|
22
|
+
|
|
23
|
+
# Example with custom logger:
|
|
24
|
+
# config.instrumentation = ->(event, data) do
|
|
25
|
+
# Rails.logger.warn("[Security] #{event}: #{data.inspect}")
|
|
26
|
+
# end
|
|
27
|
+
|
|
28
|
+
# Cache configuration (optional)
|
|
29
|
+
# config.cache_ttl = 3600 # Refresh provider keys every hour
|
|
30
|
+
# config.rotate_on_errors = true # Auto-handle provider key rotation
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Provider configuration
|
|
34
|
+
provider_issuer = ENV["OPENID_PROVIDER_ISSUER"] || "https://provider.example.com"
|
|
35
|
+
client_id = ENV["OPENID_CLIENT_ID"] || "your-client-id"
|
|
36
|
+
redirect_uri = "#{ENV["APP_URL"] || "https://your-app.com"}/users/auth/openid_federation/callback"
|
|
37
|
+
|
|
38
|
+
# File paths
|
|
39
|
+
private_key_path = Rails.root.join("config", "client-private-key.pem")
|
|
40
|
+
entity_statement_path = Rails.root.join("config", "provider-entity-statement.jwt")
|
|
41
|
+
|
|
42
|
+
# Load private key
|
|
43
|
+
unless File.exist?(private_key_path)
|
|
44
|
+
raise "Private key not found at #{private_key_path}. Generate it first using the example in README."
|
|
45
|
+
end
|
|
46
|
+
private_key = OpenSSL::PKey::RSA.new(File.read(private_key_path))
|
|
47
|
+
|
|
48
|
+
# Resolve endpoints from entity statement or manual configuration
|
|
49
|
+
endpoints = if File.exist?(entity_statement_path)
|
|
50
|
+
# Use entity statement if available (recommended for OpenID Federation)
|
|
51
|
+
OmniauthOpenidFederation::EndpointResolver.resolve(
|
|
52
|
+
entity_statement_path: entity_statement_path.to_s,
|
|
53
|
+
config: {}
|
|
54
|
+
)
|
|
55
|
+
else
|
|
56
|
+
# Fallback to manual configuration
|
|
57
|
+
{
|
|
58
|
+
authorization_endpoint: ENV["OPENID_AUTHORIZATION_ENDPOINT"] || "/oauth2/authorize",
|
|
59
|
+
token_endpoint: ENV["OPENID_TOKEN_ENDPOINT"] || "/oauth2/token",
|
|
60
|
+
userinfo_endpoint: ENV["OPENID_USERINFO_ENDPOINT"] || "/oauth2/userinfo",
|
|
61
|
+
jwks_uri: ENV["OPENID_JWKS_URI"] || "/.well-known/jwks.json",
|
|
62
|
+
audience: provider_issuer
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Validate endpoints
|
|
67
|
+
OmniauthOpenidFederation::EndpointResolver.validate_and_build_audience(
|
|
68
|
+
endpoints,
|
|
69
|
+
issuer_uri: URI.parse(provider_issuer)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
Devise.setup do |config|
|
|
73
|
+
# ... your other Devise configuration ...
|
|
74
|
+
|
|
75
|
+
# OmniAuth 2.0+ defaults to POST only for CSRF protection (CVE-2015-9284)
|
|
76
|
+
# Always use POST for security - forms must include CSRF token
|
|
77
|
+
if defined?(OmniAuth)
|
|
78
|
+
OmniAuth.config.allowed_request_methods = [:post]
|
|
79
|
+
OmniAuth.config.silence_get_warning = false
|
|
80
|
+
|
|
81
|
+
# Configure CSRF validation to check tokens only for request phase (initiating OAuth)
|
|
82
|
+
# Callback phase uses OAuth state parameter for CSRF protection (validated in strategy)
|
|
83
|
+
# This ensures:
|
|
84
|
+
# - Request phase: Forms must include Rails CSRF tokens (standard Rails protection)
|
|
85
|
+
# - Callback phase: OAuth state parameter provides CSRF protection (external providers can't include Rails tokens)
|
|
86
|
+
OmniAuth.config.request_validation_phase = lambda do |env|
|
|
87
|
+
request = Rack::Request.new(env)
|
|
88
|
+
path = request.path
|
|
89
|
+
|
|
90
|
+
# Skip CSRF validation for callback paths (external providers can't include Rails CSRF tokens)
|
|
91
|
+
# OAuth state parameter provides CSRF protection for callbacks (validated in OpenIDFederation strategy)
|
|
92
|
+
return true if path.end_with?("/callback")
|
|
93
|
+
|
|
94
|
+
# For request phase, use Rails' standard CSRF token validation
|
|
95
|
+
# This ensures forms must include valid CSRF tokens when initiating OAuth
|
|
96
|
+
session = env["rack.session"] || {}
|
|
97
|
+
token = request.params["authenticity_token"] || request.get_header("X-CSRF-Token")
|
|
98
|
+
expected_token = session[:_csrf_token] || session["_csrf_token"]
|
|
99
|
+
|
|
100
|
+
# Validate CSRF token using constant-time comparison
|
|
101
|
+
if token.present? && expected_token.present?
|
|
102
|
+
ActiveSupport::SecurityUtils.secure_compare(token.to_s, expected_token.to_s)
|
|
103
|
+
else
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
config.omniauth :openid_federation,
|
|
110
|
+
name: :openid_federation,
|
|
111
|
+
scope: [:openid],
|
|
112
|
+
response_type: "code",
|
|
113
|
+
discovery: true,
|
|
114
|
+
issuer: provider_issuer,
|
|
115
|
+
client_auth_method: :jwt_bearer,
|
|
116
|
+
client_signing_alg: :RS256,
|
|
117
|
+
audience: endpoints[:audience],
|
|
118
|
+
entity_statement_path: entity_statement_path.to_s,
|
|
119
|
+
client_options: {
|
|
120
|
+
identifier: client_id,
|
|
121
|
+
redirect_uri: redirect_uri,
|
|
122
|
+
private_key: private_key,
|
|
123
|
+
scheme: URI.parse(provider_issuer).scheme,
|
|
124
|
+
host: URI.parse(provider_issuer).host,
|
|
125
|
+
authorization_endpoint: endpoints[:authorization_endpoint],
|
|
126
|
+
token_endpoint: endpoints[:token_endpoint],
|
|
127
|
+
userinfo_endpoint: endpoints[:userinfo_endpoint],
|
|
128
|
+
jwks_uri: endpoints[:jwks_uri]
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
|
|
@@ -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
|
+
|