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,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
|
+
|