omniauth_openid_federation 1.2.2 → 1.3.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 +4 -4
- data/CHANGELOG.md +7 -1
- data/README.md +210 -708
- data/app/controllers/omniauth_openid_federation/federation_controller.rb +13 -0
- data/examples/config/initializers/devise.rb.example +44 -55
- data/examples/config/initializers/federation_endpoint.rb.example +2 -2
- data/examples/config/open_id_connect_config.rb.example +12 -15
- data/examples/config/routes.rb.example +9 -5
- data/lib/omniauth_openid_federation/configuration.rb +8 -0
- data/lib/omniauth_openid_federation/constants.rb +5 -0
- data/lib/omniauth_openid_federation/federation_endpoint.rb +0 -22
- data/lib/omniauth_openid_federation/jwks/decode.rb +0 -15
- data/lib/omniauth_openid_federation/jws.rb +21 -19
- data/lib/omniauth_openid_federation/rack_endpoint.rb +13 -0
- data/lib/omniauth_openid_federation/strategy.rb +143 -194
- data/lib/omniauth_openid_federation/tasks_helper.rb +482 -1
- data/lib/omniauth_openid_federation/validators.rb +316 -6
- data/lib/omniauth_openid_federation/version.rb +1 -1
- data/lib/tasks/omniauth_openid_federation.rake +298 -0
- data/sig/federation.rbs +0 -8
- data/sig/jwks.rbs +0 -6
- data/sig/omniauth_openid_federation.rbs +0 -1
- data/sig/strategy.rbs +0 -2
- metadata +1 -1
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# omniauth_openid_federation
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/rb/omniauth_openid_federation) [](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml) [](https://codecov.io/gh/amkisko/omniauth_openid_federation.rb)
|
|
3
|
+
[](https://badge.fury.io/rb/omniauth_openid_federation) [](https://github.com/amkisko/omniauth_openid_federation.rb/actions/workflows/test.yml) [](https://codecov.io/gh/amkisko/omniauth_openid_federation.rb/graph/badge.svg)
|
|
4
4
|
|
|
5
5
|
OmniAuth strategy for OpenID Federation providers with comprehensive security features, supporting signed request objects, ID token encryption, and full OpenID Federation 1.0 compliance.
|
|
6
6
|
|
|
@@ -10,7 +10,6 @@ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
|
|
|
10
10
|
<img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
|
|
11
11
|
</a>
|
|
12
12
|
|
|
13
|
-
|
|
14
13
|
## Installation
|
|
15
14
|
|
|
16
15
|
```ruby
|
|
@@ -24,31 +23,25 @@ bundle install
|
|
|
24
23
|
|
|
25
24
|
## Features
|
|
26
25
|
|
|
27
|
-
- ✅ **Signed Request Objects (RFC 9101)** - RS256 signing of authorization requests
|
|
28
|
-
- ✅ **Optional Request Object Encryption** -
|
|
26
|
+
- ✅ **Signed Request Objects (RFC 9101)** - RS256 signing of authorization requests
|
|
27
|
+
- ✅ **Optional Request Object Encryption** - RSA-OAEP encryption when provider requires it
|
|
29
28
|
- ✅ **ID Token Encryption/Decryption** - RSA-OAEP encryption and A128CBC-HS256 decryption
|
|
30
29
|
- ✅ **OpenID Federation 1.0** - Full entity statement support and federation metadata
|
|
31
30
|
- ✅ **Federation Endpoint** - Publish entity statements at `/.well-known/openid-federation`
|
|
32
|
-
- ✅ **Automatic Key Provisioning** - Automatic extraction/generation of signing and encryption keys
|
|
31
|
+
- ✅ **Automatic Key Provisioning** - Automatic extraction/generation of signing and encryption keys
|
|
33
32
|
- ✅ **Separate Key Support** - Production-ready support for separate signing and encryption keys
|
|
34
|
-
- ✅ **Entity Type Support** - Full support for both `openid_relying_party` (RP) and `openid_provider` (OP) entity types
|
|
35
|
-
- ✅ **Signed JWKS Support** - Automatic validation for key rotation compliance
|
|
36
|
-
- ✅ **Automatic Provider Key Rotation** - Handles external provider key rotation automatically via Signed JWKS (client key rotation is manual)
|
|
37
33
|
- ✅ **Client Assertion (private_key_jwt)** - Secure client authentication
|
|
38
|
-
- ✅ **Security Hardened** - OWASP compliant,
|
|
39
|
-
- ✅ **Production Ready** - Thread-safe, comprehensive error handling
|
|
34
|
+
- ✅ **Security Hardened** - OWASP compliant, input validation, rate limiting
|
|
40
35
|
|
|
41
36
|
## Quick Start
|
|
42
37
|
|
|
43
|
-
The library relies on **URLs and fingerprint verification** for security. Always fetch entity statements from provider URLs - local files are cached copies for configuration use. Everything is automated via discovery.
|
|
44
|
-
|
|
45
38
|
### Step 1: Get Provider Information
|
|
46
39
|
|
|
47
40
|
Your provider will provide:
|
|
48
41
|
- **Entity statement URL**: `https://provider.example.com/.well-known/openid-federation`
|
|
49
|
-
- **Expected fingerprint hash**: For verification
|
|
42
|
+
- **Expected fingerprint hash**: For verification
|
|
50
43
|
|
|
51
|
-
|
|
44
|
+
Fetch and cache the entity statement:
|
|
52
45
|
|
|
53
46
|
```bash
|
|
54
47
|
rake openid_federation:fetch_entity_statement[
|
|
@@ -58,13 +51,9 @@ rake openid_federation:fetch_entity_statement[
|
|
|
58
51
|
]
|
|
59
52
|
```
|
|
60
53
|
|
|
61
|
-
This fetches from the URL, verifies the fingerprint, and stores locally. The local file is a cached copy of the URL - always use the URL as the source of truth.
|
|
62
|
-
|
|
63
54
|
### Step 2: Generate Client Keys
|
|
64
55
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
```bash
|
|
56
|
+
```bash
|
|
68
57
|
rake openid_federation:prepare_client_keys
|
|
69
58
|
```
|
|
70
59
|
|
|
@@ -72,9 +61,13 @@ This generates:
|
|
|
72
61
|
- Private key: `config/client-private-key.pem` (keep secure, never commit)
|
|
73
62
|
- Public JWKS: `config/client-jwks.json` (send to provider for explicit registration)
|
|
74
63
|
|
|
75
|
-
**Security**:
|
|
64
|
+
**Security Warning**:
|
|
65
|
+
- **NEVER commit production private keys to your repository**
|
|
66
|
+
- For production: Use environment variables (`OPENID_CLIENT_PRIVATE_KEY_BASE64`) or secure key management systems
|
|
67
|
+
- For development: Add private key files to `.gitignore`:
|
|
76
68
|
```
|
|
77
|
-
|
|
69
|
+
.federation*
|
|
70
|
+
*.pem
|
|
78
71
|
```
|
|
79
72
|
|
|
80
73
|
### Step 3: Register Client
|
|
@@ -85,215 +78,137 @@ config/*-private-key.pem
|
|
|
85
78
|
|
|
86
79
|
**Automatic Registration** (if provider supports it):
|
|
87
80
|
- No pre-registration needed
|
|
88
|
-
- Client entity statement is auto-generated via `FederationEndpoint` (see Step 5)
|
|
89
81
|
- Set `client_entity_statement_url` to `https://your-app.com/.well-known/openid-federation`
|
|
90
82
|
|
|
91
83
|
### Step 4: Configure OmniAuth Strategy
|
|
92
84
|
|
|
93
|
-
#### For Devise (Rails)
|
|
94
|
-
|
|
95
85
|
```ruby
|
|
96
86
|
# config/initializers/devise.rb
|
|
97
87
|
require "omniauth_openid_federation"
|
|
98
88
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# rake openid_federation:fetch_entity_statement[entity_statement_url, entity_statement_fingerprint, "config/provider-entity-statement.jwt"]
|
|
107
|
-
|
|
108
|
-
config.omniauth :openid_federation,
|
|
109
|
-
discovery: true, # Enables automatic endpoint discovery
|
|
110
|
-
# Option 1: Provide URL (recommended - library fetches and caches automatically)
|
|
111
|
-
entity_statement_url: entity_statement_url, # Always provide URL as source of truth
|
|
112
|
-
entity_statement_fingerprint: entity_statement_fingerprint, # Fingerprint for verification
|
|
113
|
-
# Option 2: Provide issuer (library builds URL from issuer + /.well-known/openid-federation)
|
|
114
|
-
# issuer: "https://provider.example.com",
|
|
115
|
-
# Option 3: Provide cached path (optional - for offline development)
|
|
116
|
-
# entity_statement_path: "config/provider-entity-statement.jwt", # Cached copy from URL
|
|
117
|
-
client_options: {
|
|
118
|
-
identifier: ENV["OPENID_CLIENT_ID"],
|
|
119
|
-
redirect_uri: "#{ENV["APP_URL"]}/users/auth/openid_federation/callback",
|
|
120
|
-
private_key: private_key
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
**Key Points**:
|
|
125
|
-
- `entity_statement_url` is recommended - library automatically fetches and caches
|
|
126
|
-
- `entity_statement_fingerprint` is used for verification when fetching from URL
|
|
127
|
-
- `issuer` can be used instead - library builds URL from issuer + `/.well-known/openid-federation`
|
|
128
|
-
- `entity_statement_path` is optional - only for offline development (cached copy)
|
|
129
|
-
- `discovery: true` automatically discovers all endpoints from entity statement
|
|
130
|
-
|
|
131
|
-
**Important**: Don't forget to configure CSRF protection (see [Step 7: Configure CSRF Protection](#step-7-configure-csrf-protection)) to ensure proper security for both request and callback phases.
|
|
89
|
+
# Global settings (optional)
|
|
90
|
+
OmniauthOpenidFederation.configure do |config|
|
|
91
|
+
config.cache_ttl = 24 * 60 * 60
|
|
92
|
+
config.rotate_on_errors = true
|
|
93
|
+
config.http_timeout = 10
|
|
94
|
+
config.max_retries = 3
|
|
95
|
+
end
|
|
132
96
|
|
|
133
|
-
|
|
97
|
+
if ENV["OPENID_ENABLED"] == "true"
|
|
98
|
+
# Load private key from environment variable (recommended for production)
|
|
99
|
+
private_key = if ENV["OPENID_CLIENT_PRIVATE_KEY_BASE64"]
|
|
100
|
+
OpenSSL::PKey::RSA.new(Base64.decode64(ENV["OPENID_CLIENT_PRIVATE_KEY_BASE64"]))
|
|
101
|
+
elsif ENV["OPENID_CLIENT_PRIVATE_KEY_PATH"]
|
|
102
|
+
OpenSSL::PKey::RSA.new(File.read(Rails.root.join(ENV["OPENID_CLIENT_PRIVATE_KEY_PATH"])))
|
|
103
|
+
else
|
|
104
|
+
OpenSSL::PKey::RSA.new(File.read(Rails.root.join("config", "client-private-key.pem")))
|
|
105
|
+
end
|
|
134
106
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
107
|
+
entity_statement_path = ENV["OPENID_ENTITY_STATEMENT_PATH"] ||
|
|
108
|
+
Rails.root.join("config", ".federation-entity-statement.jwt").to_s
|
|
109
|
+
|
|
110
|
+
# Configure CSRF protection
|
|
111
|
+
if defined?(OmniAuth)
|
|
112
|
+
OmniAuth.config.allowed_request_methods = [:post]
|
|
113
|
+
OmniAuth.config.request_validation_phase = lambda do |env|
|
|
114
|
+
request = Rack::Request.new(env)
|
|
115
|
+
return true if request.path.end_with?("/callback")
|
|
116
|
+
|
|
117
|
+
session = env["rack.session"] || {}
|
|
118
|
+
token = request.params["authenticity_token"] || request.get_header("X-CSRF-Token")
|
|
119
|
+
expected_token = session[:_csrf_token] || session["_csrf_token"]
|
|
120
|
+
|
|
121
|
+
if token.present? && expected_token.present?
|
|
122
|
+
ActiveSupport::SecurityUtils.secure_compare(token.to_s, expected_token.to_s)
|
|
123
|
+
else
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
138
128
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
129
|
+
Devise.setup do |config|
|
|
130
|
+
config.omniauth :openid_federation,
|
|
131
|
+
strategy_class: OmniAuth::Strategies::OpenIDFederation,
|
|
132
|
+
name: :openid_federation,
|
|
133
|
+
scope: [:openid],
|
|
134
|
+
response_type: "code",
|
|
135
|
+
discovery: true,
|
|
136
|
+
client_auth_method: :jwt_bearer,
|
|
137
|
+
client_signing_alg: :RS256,
|
|
138
|
+
entity_statement_path: entity_statement_path,
|
|
139
|
+
always_encrypt_request_object: true,
|
|
140
|
+
client_options: {
|
|
141
|
+
identifier: ENV["OPENID_CLIENT_ID"],
|
|
142
|
+
redirect_uri: ENV["OPENID_REDIRECT_URI"] || "#{ENV["APP_URL"]}/users/auth/openid_federation/callback",
|
|
143
|
+
private_key: private_key
|
|
144
|
+
}
|
|
145
|
+
end
|
|
153
146
|
end
|
|
154
147
|
```
|
|
155
148
|
|
|
156
149
|
### Step 5: Configure Federation Endpoint (For Automatic Registration)
|
|
157
150
|
|
|
158
|
-
If using automatic registration, publish your client entity statement:
|
|
159
|
-
|
|
160
151
|
```ruby
|
|
161
152
|
# config/initializers/omniauth_openid_federation.rb
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
private_key: private_key,
|
|
165
|
-
entity_statement_path: "config/client-entity-statement.jwt", # Optional: cached copy for offline dev
|
|
166
|
-
metadata: {
|
|
167
|
-
openid_provider: {
|
|
168
|
-
issuer: ENV["APP_URL"],
|
|
169
|
-
authorization_endpoint: "#{ENV["APP_URL"]}/users/auth/openid_federation",
|
|
170
|
-
token_endpoint: "#{ENV["APP_URL"]}/users/auth/openid_federation",
|
|
171
|
-
userinfo_endpoint: "#{ENV["APP_URL"]}/users/auth/openid_federation",
|
|
172
|
-
jwks_uri: "#{ENV["APP_URL"]}/.well-known/jwks.json",
|
|
173
|
-
signed_jwks_uri: "#{ENV["APP_URL"]}/.well-known/signed-jwks.json"
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
)
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
```ruby
|
|
180
|
-
# config/routes.rb
|
|
181
|
-
# RECOMMENDED: Mount the Engine (Rails-idiomatic way)
|
|
182
|
-
mount OmniauthOpenidFederation::Engine => "/"
|
|
183
|
-
|
|
184
|
-
# ALTERNATIVE: Use mount_routes helper (for backward compatibility)
|
|
185
|
-
# OmniauthOpenidFederation::FederationEndpoint.mount_routes(self)
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
**Key Points**:
|
|
189
|
-
- `auto_configure` automatically extracts/generates JWKS from keys
|
|
190
|
-
- Only application-specific endpoints need to be provided in metadata
|
|
191
|
-
- Well-known endpoints are auto-generated
|
|
192
|
-
|
|
193
|
-
### Step 6: Add Routes
|
|
194
|
-
|
|
195
|
-
#### Mount the Engine (Required for Federation Endpoints)
|
|
196
|
-
|
|
197
|
-
The gem provides a Rails Engine that serves the well-known OpenID Federation endpoints. Mount it in your routes:
|
|
198
|
-
|
|
199
|
-
```ruby
|
|
200
|
-
# config/routes.rb
|
|
201
|
-
Rails.application.routes.draw do
|
|
202
|
-
# Mount the Engine to enable /.well-known/openid-federation endpoint
|
|
203
|
-
mount OmniauthOpenidFederation::Engine => "/"
|
|
153
|
+
if ENV["OPENID_ENABLED"] == "true"
|
|
154
|
+
app_url = ENV["APP_URL"] || "https://your-app.example.com"
|
|
204
155
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
156
|
+
private_key = if ENV["OPENID_CLIENT_PRIVATE_KEY_BASE64"]
|
|
157
|
+
OpenSSL::PKey::RSA.new(Base64.decode64(ENV["OPENID_CLIENT_PRIVATE_KEY_BASE64"]))
|
|
158
|
+
elsif ENV["OPENID_CLIENT_PRIVATE_KEY_PATH"]
|
|
159
|
+
OpenSSL::PKey::RSA.new(File.read(Rails.root.join(ENV["OPENID_CLIENT_PRIVATE_KEY_PATH"])))
|
|
160
|
+
else
|
|
161
|
+
OpenSSL::PKey::RSA.new(File.read(Rails.root.join("config", "client-private-key.pem")))
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
client_entity_statement_path = ENV["OPENID_CLIENT_ENTITY_STATEMENT_PATH"] ||
|
|
165
|
+
Rails.root.join("config", "client-entity-statement.jwt").to_s
|
|
166
|
+
|
|
167
|
+
OmniauthOpenidFederation::FederationEndpoint.auto_configure(
|
|
168
|
+
issuer: app_url,
|
|
169
|
+
private_key: private_key,
|
|
170
|
+
entity_statement_path: client_entity_statement_path,
|
|
171
|
+
metadata: {
|
|
172
|
+
openid_relying_party: {
|
|
173
|
+
redirect_uris: [
|
|
174
|
+
ENV["OPENID_REDIRECT_URI"] || "#{app_url}/users/auth/openid_federation/callback"
|
|
175
|
+
],
|
|
176
|
+
client_registration_types: ["automatic"],
|
|
177
|
+
application_type: "web",
|
|
178
|
+
grant_types: ["authorization_code"],
|
|
179
|
+
response_types: ["code"],
|
|
180
|
+
token_endpoint_auth_method: "private_key_jwt",
|
|
181
|
+
token_endpoint_auth_signing_alg: "RS256",
|
|
182
|
+
request_object_signing_alg: "RS256",
|
|
183
|
+
id_token_encrypted_response_alg: "RSA-OAEP",
|
|
184
|
+
id_token_encrypted_response_enc: "A128CBC-HS256",
|
|
185
|
+
organization_name: ENV["OPENID_ORGANIZATION_NAME"]
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
)
|
|
209
189
|
end
|
|
210
190
|
```
|
|
211
191
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
#### For OmniAuth (Non-Devise)
|
|
192
|
+
### Step 6: Add Routes
|
|
215
193
|
|
|
216
194
|
```ruby
|
|
217
195
|
# config/routes.rb
|
|
218
|
-
|
|
196
|
+
if ENV["OPENID_ENABLED"] == "true"
|
|
219
197
|
mount OmniauthOpenidFederation::Engine => "/"
|
|
220
|
-
|
|
221
|
-
get "/auth/:provider/callback", to: "sessions#create"
|
|
222
|
-
get "/auth/failure", to: "sessions#failure"
|
|
223
198
|
end
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
#### Alternative: Manual Route Mounting (Backward Compatibility)
|
|
227
|
-
|
|
228
|
-
If you need custom paths or prefer manual route definition, you can use the `mount_routes` helper (deprecated):
|
|
229
199
|
|
|
230
|
-
```ruby
|
|
231
|
-
# config/routes.rb
|
|
232
200
|
Rails.application.routes.draw do
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
end
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
### Step 7: Configure CSRF Protection
|
|
240
|
-
|
|
241
|
-
OmniAuth requires CSRF protection configuration to handle both the request phase (initiating OAuth) and callback phase (external provider redirect).
|
|
242
|
-
|
|
243
|
-
**Important**: The request phase uses Rails CSRF tokens (forms must include them), while the callback phase uses OAuth state parameter for CSRF protection (external providers cannot include Rails CSRF tokens).
|
|
244
|
-
|
|
245
|
-
#### For Devise (Rails)
|
|
246
|
-
|
|
247
|
-
```ruby
|
|
248
|
-
# config/initializers/devise.rb
|
|
249
|
-
if defined?(OmniAuth)
|
|
250
|
-
OmniAuth.config.allowed_request_methods = [:post]
|
|
251
|
-
OmniAuth.config.silence_get_warning = false
|
|
252
|
-
|
|
253
|
-
# Configure CSRF validation to check tokens only for request phase (initiating OAuth)
|
|
254
|
-
# Callback phase uses OAuth state parameter for CSRF protection (validated in strategy)
|
|
255
|
-
# This ensures:
|
|
256
|
-
# - Request phase: Forms must include Rails CSRF tokens (standard Rails protection)
|
|
257
|
-
# - Callback phase: OAuth state parameter provides CSRF protection (external providers can't include Rails tokens)
|
|
258
|
-
OmniAuth.config.request_validation_phase = lambda do |env|
|
|
259
|
-
request = Rack::Request.new(env)
|
|
260
|
-
path = request.path
|
|
261
|
-
|
|
262
|
-
# Skip CSRF validation for callback paths (external providers can't include Rails CSRF tokens)
|
|
263
|
-
# OAuth state parameter provides CSRF protection for callbacks (validated in OpenIDFederation strategy)
|
|
264
|
-
return true if path.end_with?("/callback")
|
|
265
|
-
|
|
266
|
-
# For request phase, use Rails' standard CSRF token validation
|
|
267
|
-
# This ensures forms must include valid CSRF tokens when initiating OAuth
|
|
268
|
-
session = env["rack.session"] || {}
|
|
269
|
-
token = request.params["authenticity_token"] || request.get_header("X-CSRF-Token")
|
|
270
|
-
expected_token = session[:_csrf_token] || session["_csrf_token"]
|
|
271
|
-
|
|
272
|
-
# Validate CSRF token using constant-time comparison
|
|
273
|
-
if token.present? && expected_token.present?
|
|
274
|
-
ActiveSupport::SecurityUtils.secure_compare(token.to_s, expected_token.to_s)
|
|
275
|
-
else
|
|
276
|
-
false
|
|
277
|
-
end
|
|
278
|
-
end
|
|
201
|
+
devise_for :users, controllers: {
|
|
202
|
+
omniauth_callbacks: "users/omniauth_callbacks"
|
|
203
|
+
}
|
|
279
204
|
end
|
|
280
205
|
```
|
|
281
206
|
|
|
282
|
-
|
|
283
|
-
- **Request phase** (initiating OAuth): Forms must include Rails CSRF tokens via `button_to` or `form_with` helpers
|
|
284
|
-
- **Callback phase** (external provider redirect): OAuth `state` parameter provides CSRF protection (automatically validated in `OpenIDFederation` strategy using constant-time comparison)
|
|
285
|
-
- Both layers provide equivalent security - Rails CSRF tokens for request phase, OAuth state parameter for callbacks
|
|
286
|
-
|
|
287
|
-
### Step 8: Create Callback Controller
|
|
288
|
-
|
|
289
|
-
#### For Devise
|
|
207
|
+
### Step 7: Create Callback Controller
|
|
290
208
|
|
|
291
209
|
```ruby
|
|
292
210
|
# app/controllers/users/omniauth_callbacks_controller.rb
|
|
293
211
|
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|
294
|
-
# Skip Rails CSRF protection for OAuth callbacks
|
|
295
|
-
# OAuth callbacks from external providers cannot include Rails CSRF tokens
|
|
296
|
-
# CSRF protection is handled by OAuth state parameter validation in the strategy
|
|
297
212
|
skip_before_action :verify_authenticity_token, only: [:openid_federation, :failure]
|
|
298
213
|
skip_before_action :authenticate_user!, only: [:openid_federation, :failure]
|
|
299
214
|
|
|
@@ -314,9 +229,7 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|
|
314
229
|
end
|
|
315
230
|
```
|
|
316
231
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
### Step 9: Create User Model Method
|
|
232
|
+
### Step 8: Create User Model Method
|
|
320
233
|
|
|
321
234
|
```ruby
|
|
322
235
|
# app/models/user.rb
|
|
@@ -327,18 +240,14 @@ class User < ApplicationRecord
|
|
|
327
240
|
if user
|
|
328
241
|
user.update(
|
|
329
242
|
email: auth.info.email,
|
|
330
|
-
name: auth.info.name
|
|
331
|
-
first_name: auth.info.first_name,
|
|
332
|
-
last_name: auth.info.last_name
|
|
243
|
+
name: auth.info.name
|
|
333
244
|
)
|
|
334
245
|
else
|
|
335
246
|
user = create(
|
|
336
247
|
provider: auth.provider,
|
|
337
248
|
uid: auth.uid,
|
|
338
249
|
email: auth.info.email,
|
|
339
|
-
name: auth.info.name
|
|
340
|
-
first_name: auth.info.first_name,
|
|
341
|
-
last_name: auth.info.last_name
|
|
250
|
+
name: auth.info.name
|
|
342
251
|
)
|
|
343
252
|
end
|
|
344
253
|
|
|
@@ -347,576 +256,169 @@ class User < ApplicationRecord
|
|
|
347
256
|
end
|
|
348
257
|
```
|
|
349
258
|
|
|
350
|
-
##
|
|
351
|
-
|
|
352
|
-
### Prepare Client Keys
|
|
353
|
-
|
|
354
|
-
```bash
|
|
355
|
-
rake openid_federation:prepare_client_keys
|
|
356
|
-
rake openid_federation:prepare_client_keys[separate,config] # Separate signing/encryption keys
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
### Fetch Entity Statement
|
|
360
|
-
|
|
361
|
-
Fetches entity statement from provider URL, verifies fingerprint, and caches locally:
|
|
362
|
-
|
|
363
|
-
```bash
|
|
364
|
-
rake openid_federation:fetch_entity_statement[
|
|
365
|
-
"https://provider.example.com/.well-known/openid-federation",
|
|
366
|
-
"expected-fingerprint-hash",
|
|
367
|
-
"config/provider-entity-statement.jwt"
|
|
368
|
-
]
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
**Note**: Always use the URL as the source of truth - the local file is just a cached copy.
|
|
372
|
-
|
|
373
|
-
### Parse Entity Statement
|
|
374
|
-
|
|
375
|
-
```bash
|
|
376
|
-
rake openid_federation:parse_entity_statement["config/provider-entity-statement.jwt"]
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
### Test Local Entity Statement Endpoint
|
|
380
|
-
|
|
381
|
-
Validates your local entity statement endpoint and tests all linked endpoints. Useful for verifying your federation endpoint implementation:
|
|
382
|
-
|
|
383
|
-
```bash
|
|
384
|
-
# Default (localhost:3000)
|
|
385
|
-
rake openid_federation:test_local_endpoint
|
|
386
|
-
|
|
387
|
-
# Custom base URL
|
|
388
|
-
rake openid_federation:test_local_endpoint[http://localhost:3000]
|
|
389
|
-
|
|
390
|
-
# Via environment variable
|
|
391
|
-
BASE_URL=http://localhost:3000 rake openid_federation:test_local_endpoint
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
This task:
|
|
395
|
-
- Fetches and validates the entity statement from `/.well-known/openid-federation`
|
|
396
|
-
- Shows key configuration status (single vs separate keys) with recommendations
|
|
397
|
-
- Tests all endpoints mentioned in the entity statement
|
|
398
|
-
- Displays validation warnings without blocking execution
|
|
399
|
-
|
|
400
|
-
See all tasks: `rake -T openid_federation`
|
|
259
|
+
## Passing Custom Parameters
|
|
401
260
|
|
|
402
|
-
###
|
|
261
|
+
### Using `request_object_params` (Allow-List)
|
|
403
262
|
|
|
404
|
-
|
|
263
|
+
Pass custom parameters via `request_object_params` allow-list:
|
|
405
264
|
|
|
406
265
|
```ruby
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
end
|
|
266
|
+
config.omniauth :openid_federation,
|
|
267
|
+
request_object_params: ["custom_param", "another_param"],
|
|
268
|
+
# ... other options
|
|
411
269
|
```
|
|
412
270
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
Configure custom instrumentation for security events, MITM attack detection, and authentication mismatches:
|
|
271
|
+
Parameters in the allow-list are automatically included in the JWT request object if present in the HTTP request.
|
|
416
272
|
|
|
417
|
-
|
|
418
|
-
OmniauthOpenidFederation.configure do |config|
|
|
419
|
-
# Configure with Sentry
|
|
420
|
-
config.instrumentation = ->(event, data) do
|
|
421
|
-
Sentry.capture_message(
|
|
422
|
-
"OpenID Federation: #{event}",
|
|
423
|
-
level: data[:severity] == :error ? :error : :warning,
|
|
424
|
-
extra: data
|
|
425
|
-
)
|
|
426
|
-
end
|
|
427
|
-
end
|
|
428
|
-
```
|
|
273
|
+
### Using `prepare_request_object_params` (Proc)
|
|
429
274
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
Honeybadger.notify("OpenID Federation: #{event}", context: data)
|
|
435
|
-
end
|
|
436
|
-
end
|
|
437
|
-
```
|
|
275
|
+
Use `prepare_request_object_params` proc to modify parameters before they're added to the signed request object. This is useful for:
|
|
276
|
+
- Combining config values with form values (e.g., base `acr_values` + provider-specific)
|
|
277
|
+
- Adding config-based parameters (e.g., `ftn_spname` from config)
|
|
278
|
+
- Transforming or validating parameters
|
|
438
279
|
|
|
439
|
-
**With custom logger**:
|
|
440
280
|
```ruby
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
- `missing_required_claims` - Token missing required claims
|
|
461
|
-
|
|
462
|
-
**Note**: All blocking exceptions are automatically reported through instrumentation, including:
|
|
463
|
-
- OmniAuth middleware errors (like `AuthenticityTokenProtection` blocking requests)
|
|
464
|
-
- Strategy-level errors (CSRF detected, missing code, token exchange failures)
|
|
465
|
-
- Unknown error types (reported as `unexpected_authentication_break`)
|
|
466
|
-
|
|
467
|
-
**Security Note**: All sensitive data (tokens, keys, fingerprints) is automatically sanitized before being sent to your instrumentation callback.
|
|
468
|
-
|
|
469
|
-
**Key Rotation Types**:
|
|
470
|
-
- **Provider Keys** (from external providers): ✅ Automatic via Signed JWKS - library automatically detects and uses new provider keys
|
|
471
|
-
- **Client Keys** (your own keys): ⚠️ **Manual rotation required** - you must generate new RSA keys and update entity statement
|
|
472
|
-
|
|
473
|
-
**Client Key Rotation Process** (Manual Steps Required):
|
|
474
|
-
1. **Generate new RSA keys** (manual):
|
|
475
|
-
```bash
|
|
476
|
-
bundle exec rake omniauth_openid_federation:prepare_client_keys[key_type=separate]
|
|
477
|
-
```
|
|
478
|
-
2. **Update entity statement file** (manual): Update `entity_statement_path` with new keys, or let the library regenerate it
|
|
479
|
-
3. **Library automatically uses new keys** (automatic): Library extracts JWKS from updated entity statement file on next cache refresh
|
|
480
|
-
|
|
481
|
-
**Note**: The library automatically generates JWKS from your RSA keys, but you must manually generate new RSA keys when rotating. The library then automatically uses the new keys from the updated entity statement file. See [Automatic Key Provisioning](#automatic-key-provisioning) for details.
|
|
482
|
-
|
|
483
|
-
### Publishing Federation Endpoint
|
|
484
|
-
|
|
485
|
-
Publish your entity statement at `/.well-known/openid-federation` using `auto_configure`.
|
|
486
|
-
|
|
487
|
-
The library supports two entity types:
|
|
488
|
-
- **openid_relying_party (RP)**: For clients/relying parties (PRIMARY USE CASE)
|
|
489
|
-
- **openid_provider (OP)**: For providers/servers (secondary use case)
|
|
490
|
-
|
|
491
|
-
#### Relying Party (RP) Configuration (Primary Use Case)
|
|
492
|
-
|
|
493
|
-
**First, generate your RSA keys** (if not already generated):
|
|
494
|
-
|
|
495
|
-
```bash
|
|
496
|
-
# Generate separate signing and encryption keys (RECOMMENDED for production)
|
|
497
|
-
bundle exec rake omniauth_openid_federation:prepare_client_keys[key_type=separate]
|
|
498
|
-
|
|
499
|
-
# Or generate single key for dev/testing (NOT RECOMMENDED for production)
|
|
500
|
-
bundle exec rake omniauth_openid_federation:prepare_client_keys[key_type=single]
|
|
281
|
+
config.omniauth :openid_federation,
|
|
282
|
+
request_object_params: [:ftn_spname], # Allow-list for custom params
|
|
283
|
+
prepare_request_object_params: proc do |params|
|
|
284
|
+
# Combine config acr_values with form acr_values
|
|
285
|
+
form_acr_values = params["acr_values"]&.to_s&.strip
|
|
286
|
+
config_acr_values = ENV["OPENID_ACR_VALUES"].to_s.strip
|
|
287
|
+
|
|
288
|
+
if config_acr_values.present? && form_acr_values.present?
|
|
289
|
+
params["acr_values"] = "#{config_acr_values} #{form_acr_values}".strip
|
|
290
|
+
elsif config_acr_values.present?
|
|
291
|
+
params["acr_values"] = config_acr_values
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Add custom parameter from config
|
|
295
|
+
params["ftn_spname"] = ENV["OPENID_FTN_SPNAME"] if ENV["OPENID_FTN_SPNAME"].present?
|
|
296
|
+
|
|
297
|
+
params
|
|
298
|
+
end,
|
|
299
|
+
# ... other options
|
|
501
300
|
```
|
|
502
301
|
|
|
503
|
-
|
|
504
|
-
- `config/client-signing-private-key.pem` and `config/client-encryption-private-key.pem` (separate keys)
|
|
505
|
-
- OR `config/client-private-key.pem` (single key for dev/testing)
|
|
506
|
-
|
|
507
|
-
**Then configure the federation endpoint** - the library automatically generates JWKS from your keys:
|
|
302
|
+
**Form Example** (pass clean values, proc handles combining):
|
|
508
303
|
|
|
509
304
|
```ruby
|
|
510
|
-
#
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
issuer: "https://your-app.com",
|
|
515
|
-
signing_key: OpenSSL::PKey::RSA.new(File.read("config/client-signing-private-key.pem")),
|
|
516
|
-
encryption_key: OpenSSL::PKey::RSA.new(File.read("config/client-encryption-private-key.pem")),
|
|
517
|
-
entity_statement_path: "config/client-entity-statement.jwt", # Cache for automatic key rotation
|
|
518
|
-
metadata: {
|
|
519
|
-
openid_relying_party: {
|
|
520
|
-
redirect_uris: ["https://your-app.com/users/auth/openid_federation/callback"],
|
|
521
|
-
client_registration_types: ["automatic"],
|
|
522
|
-
application_type: "web",
|
|
523
|
-
grant_types: ["authorization_code"],
|
|
524
|
-
response_types: ["code"],
|
|
525
|
-
token_endpoint_auth_method: "private_key_jwt",
|
|
526
|
-
token_endpoint_auth_signing_alg: "RS256",
|
|
527
|
-
request_object_signing_alg: "RS256",
|
|
528
|
-
id_token_encrypted_response_alg: "RSA-OAEP",
|
|
529
|
-
id_token_encrypted_response_enc: "A128CBC-HS256"
|
|
530
|
-
}
|
|
531
|
-
},
|
|
532
|
-
auto_provision_keys: true # Library automatically generates JWKS from provided keys
|
|
533
|
-
)
|
|
305
|
+
# In your form - pass only provider-specific value
|
|
306
|
+
<%= button_to "Login", user_openid_federation_omniauth_authorize_path,
|
|
307
|
+
method: :post,
|
|
308
|
+
params: { acr_values: "provider_specific_level" } %>
|
|
534
309
|
```
|
|
535
310
|
|
|
536
|
-
|
|
537
|
-
```ruby
|
|
538
|
-
OmniauthOpenidFederation::FederationEndpoint.auto_configure(
|
|
539
|
-
issuer: "https://your-app.com",
|
|
540
|
-
private_key: private_key, # DEV/TESTING ONLY - single key for both signing and encryption
|
|
541
|
-
entity_statement_path: "config/client-entity-statement.jwt",
|
|
542
|
-
metadata: {
|
|
543
|
-
openid_relying_party: { ... }
|
|
544
|
-
},
|
|
545
|
-
auto_provision_keys: true
|
|
546
|
-
)
|
|
547
|
-
```
|
|
311
|
+
The proc will combine this with config values before adding to the signed JWT.
|
|
548
312
|
|
|
549
|
-
|
|
313
|
+
## Rake Tasks
|
|
550
314
|
|
|
551
|
-
|
|
315
|
+
### Prepare Client Keys
|
|
552
316
|
|
|
553
317
|
```bash
|
|
554
|
-
|
|
555
|
-
bundle exec rake omniauth_openid_federation:prepare_client_keys[key_type=separate,output_dir=config]
|
|
556
|
-
|
|
557
|
-
# Or generate single key for dev/testing (NOT RECOMMENDED for production)
|
|
558
|
-
bundle exec rake omniauth_openid_federation:prepare_client_keys[key_type=single,output_dir=config]
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
**Then configure the federation endpoint** - the library automatically generates JWKS from your keys:
|
|
562
|
-
|
|
563
|
-
```ruby
|
|
564
|
-
# For provider/server applications
|
|
565
|
-
# Production Setup (RECOMMENDED): Separate signing and encryption keys
|
|
566
|
-
# The library automatically generates JWKS from these keys
|
|
567
|
-
signing_key = OpenSSL::PKey::RSA.new(File.read("config/client-signing-private-key.pem"))
|
|
568
|
-
encryption_key = OpenSSL::PKey::RSA.new(File.read("config/client-encryption-private-key.pem"))
|
|
569
|
-
|
|
570
|
-
OmniauthOpenidFederation::FederationEndpoint.auto_configure(
|
|
571
|
-
issuer: "https://provider.example.com",
|
|
572
|
-
signing_key: signing_key,
|
|
573
|
-
encryption_key: encryption_key,
|
|
574
|
-
entity_statement_path: "config/provider-entity-statement.jwt",
|
|
575
|
-
metadata: {
|
|
576
|
-
openid_provider: {
|
|
577
|
-
issuer: "https://provider.example.com",
|
|
578
|
-
authorization_endpoint: "https://provider.example.com/oauth2/authorize",
|
|
579
|
-
token_endpoint: "https://provider.example.com/oauth2/token",
|
|
580
|
-
userinfo_endpoint: "https://provider.example.com/oauth2/userinfo",
|
|
581
|
-
jwks_uri: "https://provider.example.com/.well-known/jwks.json",
|
|
582
|
-
signed_jwks_uri: "https://provider.example.com/.well-known/signed-jwks.json"
|
|
583
|
-
# federation_fetch_endpoint is automatically added for OPs
|
|
584
|
-
}
|
|
585
|
-
},
|
|
586
|
-
auto_provision_keys: true # Library automatically generates JWKS from provided keys
|
|
587
|
-
)
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
**Development/Testing** (NOT RECOMMENDED FOR PRODUCTION):
|
|
591
|
-
```ruby
|
|
592
|
-
# Single private key for both signing and encryption (DEV/TESTING ONLY)
|
|
593
|
-
OmniauthOpenidFederation::FederationEndpoint.auto_configure(
|
|
594
|
-
issuer: "https://provider.example.com",
|
|
595
|
-
private_key: private_key, # DEV/TESTING ONLY - not recommended for production
|
|
596
|
-
entity_statement_path: "config/provider-entity-statement.jwt",
|
|
597
|
-
metadata: {
|
|
598
|
-
openid_provider: {
|
|
599
|
-
issuer: "https://provider.example.com",
|
|
600
|
-
authorization_endpoint: "https://provider.example.com/oauth2/authorize",
|
|
601
|
-
token_endpoint: "https://provider.example.com/oauth2/token",
|
|
602
|
-
userinfo_endpoint: "https://provider.example.com/oauth2/userinfo",
|
|
603
|
-
jwks_uri: "https://provider.example.com/.well-known/jwks.json",
|
|
604
|
-
signed_jwks_uri: "https://provider.example.com/.well-known/signed-jwks.json"
|
|
605
|
-
}
|
|
606
|
-
},
|
|
607
|
-
auto_provision_keys: true
|
|
608
|
-
)
|
|
318
|
+
rake openid_federation:prepare_client_keys
|
|
609
319
|
```
|
|
610
320
|
|
|
611
|
-
|
|
612
|
-
# config/routes.rb
|
|
613
|
-
# RECOMMENDED: Mount the Engine (Rails-idiomatic way)
|
|
614
|
-
mount OmniauthOpenidFederation::Engine => "/"
|
|
321
|
+
### Fetch Entity Statement
|
|
615
322
|
|
|
616
|
-
|
|
617
|
-
|
|
323
|
+
```bash
|
|
324
|
+
rake openid_federation:fetch_entity_statement[
|
|
325
|
+
"https://provider.example.com/.well-known/openid-federation",
|
|
326
|
+
"expected-fingerprint-hash",
|
|
327
|
+
"config/provider-entity-statement.jwt"
|
|
328
|
+
]
|
|
618
329
|
```
|
|
619
330
|
|
|
620
|
-
|
|
621
|
-
- Extracts JWKS from entity statement file or generates from provided keys
|
|
622
|
-
- Supports separate signing/encryption keys (RECOMMENDED) or single key (dev/testing)
|
|
623
|
-
- Auto-detects entity type and generates well-known endpoints
|
|
624
|
-
- Uses `entity_statement_path` as cache for key rotation
|
|
625
|
-
|
|
626
|
-
**Manual Configuration** (advanced, not recommended):
|
|
331
|
+
### Test Authentication Flow
|
|
627
332
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
config.signing_key = signing_key # RECOMMENDED: Separate signing key
|
|
635
|
-
config.encryption_key = encryption_key # RECOMMENDED: Separate encryption key
|
|
636
|
-
config.jwks = jwks # Must provide manually
|
|
637
|
-
config.metadata = { ... }
|
|
638
|
-
end
|
|
333
|
+
```bash
|
|
334
|
+
rake openid_federation:test_authentication_flow[
|
|
335
|
+
"https://provider.example.com/login",
|
|
336
|
+
"https://your-app.com",
|
|
337
|
+
"urn:mace:incommon:iap:silver"
|
|
338
|
+
]
|
|
639
339
|
```
|
|
640
340
|
|
|
641
|
-
### Automatic Key Provisioning
|
|
642
|
-
|
|
643
|
-
The `auto_configure` method automatically generates JWKS from your RSA keys (generate keys first using the rake task).
|
|
644
|
-
|
|
645
|
-
**Priority Order**:
|
|
646
|
-
1. Extracts JWKS from `entity_statement_path` if file exists (supports key rotation)
|
|
647
|
-
2. Generates JWKS from separate `signing_key` and `encryption_key` (RECOMMENDED)
|
|
648
|
-
3. Generates JWKS from single `private_key` (dev/testing only)
|
|
649
|
-
|
|
650
|
-
**Key Rotation** (Semi-Automatic):
|
|
651
|
-
1. **Manual**: Generate new RSA keys using `rake omniauth_openid_federation:prepare_client_keys`
|
|
652
|
-
2. **Manual**: Update entity statement file at `entity_statement_path` with new keys
|
|
653
|
-
3. **Automatic**: Library extracts and uses new keys from updated file on next cache refresh
|
|
654
|
-
|
|
655
341
|
## Configuration Options
|
|
656
342
|
|
|
657
343
|
### Required
|
|
658
344
|
|
|
659
|
-
- `client_options
|
|
660
|
-
- `client_options
|
|
661
|
-
- `client_options
|
|
662
|
-
-
|
|
663
|
-
- `entity_statement_url` - Provider entity statement URL (recommended - library fetches and caches automatically)
|
|
664
|
-
- `issuer` - Provider issuer URI (library builds entity statement URL from issuer + `/.well-known/openid-federation`)
|
|
665
|
-
- `entity_statement_path` - Provider entity statement path (optional - for offline development)
|
|
345
|
+
- `client_options.identifier` - Client ID from provider
|
|
346
|
+
- `client_options.redirect_uri` - Callback URL
|
|
347
|
+
- `client_options.private_key` - RSA private key for signing
|
|
348
|
+
- `entity_statement_path` - Path to cached entity statement file
|
|
666
349
|
|
|
667
350
|
### Optional
|
|
668
351
|
|
|
669
|
-
- `
|
|
670
|
-
- `entity_statement_fingerprint` -
|
|
671
|
-
- `
|
|
672
|
-
- `
|
|
673
|
-
- `
|
|
674
|
-
- `
|
|
675
|
-
- `
|
|
676
|
-
- `
|
|
677
|
-
- `scope` - OAuth scopes (default: `[:openid]`)
|
|
678
|
-
- `response_type` - Response type (default: `"code"`)
|
|
679
|
-
- `client_auth_method` - Client authentication (default: `:jwt_bearer`)
|
|
680
|
-
- `client_signing_alg` - Signing algorithm (default: `:RS256`)
|
|
681
|
-
- `fetch_userinfo` - Whether to fetch userinfo endpoint (default: `true`)
|
|
682
|
-
- `acr_values` - Authentication Context Class Reference values (provider-specific)
|
|
683
|
-
- `key_source` - `:local` (default) or `:federation` (advanced)
|
|
684
|
-
|
|
685
|
-
### Global Configuration
|
|
686
|
-
|
|
687
|
-
Configure global settings via `OmniauthOpenidFederation.configure`:
|
|
688
|
-
|
|
689
|
-
```ruby
|
|
690
|
-
OmniauthOpenidFederation.configure do |config|
|
|
691
|
-
# Cache configuration
|
|
692
|
-
config.cache_ttl = 3600 # JWKS cache TTL in seconds
|
|
693
|
-
config.rotate_on_errors = true # Auto-rotate on key-related errors
|
|
694
|
-
|
|
695
|
-
# Security instrumentation (Sentry, Honeybadger, etc.)
|
|
696
|
-
config.instrumentation = ->(event, data) do
|
|
697
|
-
Sentry.capture_message("OpenID Federation: #{event}", level: :warning, extra: data)
|
|
698
|
-
end
|
|
699
|
-
|
|
700
|
-
# HTTP configuration
|
|
701
|
-
config.http_timeout = 10
|
|
702
|
-
config.max_retries = 3
|
|
703
|
-
config.verify_ssl = true
|
|
704
|
-
end
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
### Request Object Security (Signing vs Encryption)
|
|
352
|
+
- `entity_statement_url` - URL to fetch entity statement (auto-fetches if provided)
|
|
353
|
+
- `entity_statement_fingerprint` - Fingerprint for verification
|
|
354
|
+
- `client_entity_statement_url` - Client entity statement URL (for automatic registration)
|
|
355
|
+
- `client_entity_statement_path` - Client entity statement path (cached copy)
|
|
356
|
+
- `always_encrypt_request_object` - Force encryption of request objects (default: false)
|
|
357
|
+
- `request_object_params` - Array of parameter names to include in request object (allow-list)
|
|
358
|
+
- `prepare_request_object_params` - Proc to modify params before adding to signed request object: `proc { |params| modified_params }`
|
|
359
|
+
- `discovery` - Enable automatic endpoint discovery (default: true)
|
|
708
360
|
|
|
709
|
-
|
|
710
|
-
- **Signing (MANDATORY)**: Request objects **MUST be signed** using RS256 (always enforced, cannot be disabled)
|
|
711
|
-
- **Encryption (OPTIONAL)**: Request objects **MAY be encrypted** when provider requires it or when `always_encrypt_request_object: true`
|
|
712
|
-
|
|
713
|
-
**Encryption Behavior:**
|
|
714
|
-
- **Default** (`always_encrypt_request_object: false`): Only encrypts if provider metadata specifies `request_object_encryption_alg`
|
|
715
|
-
- **When `true`**: Encrypts even if provider doesn't require it (if encryption keys available)
|
|
716
|
-
- **Use case**: High-security deployments requiring defense-in-depth beyond minimum spec
|
|
717
|
-
|
|
718
|
-
**Note**: Signing provides authentication and integrity. Encryption adds confidentiality but is optional and adds overhead.
|
|
719
|
-
|
|
720
|
-
### Detailed Configuration Examples
|
|
721
|
-
|
|
722
|
-
#### Devise with Environment Variables (Recommended)
|
|
723
|
-
|
|
724
|
-
```ruby
|
|
725
|
-
# config/initializers/devise.rb
|
|
726
|
-
require "omniauth_openid_federation"
|
|
727
|
-
|
|
728
|
-
private_key = if ENV["OPENID_CLIENT_PRIVATE_KEY"]
|
|
729
|
-
OpenSSL::PKey::RSA.new(Base64.decode64(ENV["OPENID_CLIENT_PRIVATE_KEY"]))
|
|
730
|
-
else
|
|
731
|
-
OpenSSL::PKey::RSA.new(File.read("config/client-private-key.pem"))
|
|
732
|
-
end
|
|
733
|
-
|
|
734
|
-
config.omniauth :openid_federation,
|
|
735
|
-
discovery: true, # Auto-discovers endpoints from entity statement
|
|
736
|
-
entity_statement_url: ENV["OPENID_ENTITY_STATEMENT_URL"], # Always provide URL
|
|
737
|
-
entity_statement_fingerprint: ENV["OPENID_ENTITY_STATEMENT_FINGERPRINT"], # Fingerprint for verification
|
|
738
|
-
entity_statement_path: "config/provider-entity-statement.jwt", # Cached copy from URL (fetch via rake task)
|
|
739
|
-
client_entity_statement_url: "#{ENV["APP_URL"]}/.well-known/openid-federation", # For automatic registration
|
|
740
|
-
client_options: {
|
|
741
|
-
identifier: ENV["OPENID_CLIENT_ID"],
|
|
742
|
-
redirect_uri: "#{ENV["APP_URL"]}/users/auth/openid_federation/callback",
|
|
743
|
-
private_key: private_key
|
|
744
|
-
}
|
|
745
|
-
# All endpoints are auto-discovered - no manual configuration needed
|
|
746
|
-
```
|
|
747
|
-
|
|
748
|
-
#### OmniAuth with URL-based Entity Statement (Production)
|
|
749
|
-
|
|
750
|
-
```ruby
|
|
751
|
-
# config/initializers/omniauth.rb
|
|
752
|
-
require "omniauth_openid_federation"
|
|
753
|
-
|
|
754
|
-
entity_statement_url = "https://provider.example.com/.well-known/openid-federation"
|
|
755
|
-
entity_statement_fingerprint = "expected-fingerprint-hash"
|
|
756
|
-
|
|
757
|
-
Rails.application.config.middleware.use OmniAuth::Builder do
|
|
758
|
-
provider :openid_federation,
|
|
759
|
-
discovery: true,
|
|
760
|
-
entity_statement_url: entity_statement_url, # Always provide URL
|
|
761
|
-
entity_statement_fingerprint: entity_statement_fingerprint, # Fingerprint for verification
|
|
762
|
-
entity_statement_path: "config/provider-entity-statement.jwt", # Cached copy from URL
|
|
763
|
-
client_options: {
|
|
764
|
-
identifier: ENV["OPENID_CLIENT_ID"],
|
|
765
|
-
redirect_uri: "https://your-app.com/auth/openid_federation/callback",
|
|
766
|
-
private_key: OpenSSL::PKey::RSA.new(File.read("config/client-private-key.pem"))
|
|
767
|
-
}
|
|
768
|
-
end
|
|
769
|
-
```
|
|
770
|
-
|
|
771
|
-
**Key Points**:
|
|
772
|
-
- **Always provide `entity_statement_url`** - this is the source of truth
|
|
773
|
-
- `entity_statement_fingerprint` is used for verification when fetching
|
|
774
|
-
- `entity_statement_path` points to the cached copy fetched from the URL
|
|
775
|
-
- All endpoints are automatically discovered - no manual endpoint configuration
|
|
776
|
-
|
|
777
|
-
## API Reference
|
|
778
|
-
|
|
779
|
-
### `OmniauthOpenidFederation::Jws`
|
|
780
|
-
|
|
781
|
-
Builds and signs JWT request objects:
|
|
782
|
-
|
|
783
|
-
```ruby
|
|
784
|
-
jws = OmniauthOpenidFederation::Jws.new(
|
|
785
|
-
client_id: "client-id",
|
|
786
|
-
redirect_uri: "https://example.com/callback",
|
|
787
|
-
scope: "openid",
|
|
788
|
-
issuer: "https://provider.example.com",
|
|
789
|
-
audience: "https://provider.example.com",
|
|
790
|
-
private_key: private_key
|
|
791
|
-
)
|
|
792
|
-
signed_jwt = jws.sign
|
|
793
|
-
```
|
|
794
|
-
|
|
795
|
-
### `OmniauthOpenidFederation::Federation::EntityStatement`
|
|
796
|
-
|
|
797
|
-
Fetches and validates entity statements:
|
|
798
|
-
|
|
799
|
-
```ruby
|
|
800
|
-
statement = OmniauthOpenidFederation::Federation::EntityStatement.fetch!(
|
|
801
|
-
"https://provider.example.com/.well-known/openid-federation",
|
|
802
|
-
fingerprint: "expected-fingerprint"
|
|
803
|
-
)
|
|
804
|
-
metadata = statement.parse
|
|
805
|
-
```
|
|
806
|
-
|
|
807
|
-
### `OmniauthOpenidFederation::Federation::SignedJWKS`
|
|
808
|
-
|
|
809
|
-
Fetches and validates signed JWKS:
|
|
810
|
-
|
|
811
|
-
```ruby
|
|
812
|
-
signed_jwks = OmniauthOpenidFederation::Federation::SignedJWKS.fetch!(
|
|
813
|
-
signed_jwks_uri,
|
|
814
|
-
entity_jwks
|
|
815
|
-
)
|
|
816
|
-
```
|
|
361
|
+
## Security
|
|
817
362
|
|
|
818
|
-
|
|
363
|
+
- All user input is validated and sanitized
|
|
364
|
+
- Configuration values are trusted (not validated)
|
|
365
|
+
- Signed request objects are required (RFC 9101)
|
|
366
|
+
- CSRF protection via Rails tokens (request phase) and OAuth state (callback phase)
|
|
367
|
+
- Private keys should never be committed to version control
|
|
819
368
|
|
|
820
369
|
## Troubleshooting
|
|
821
370
|
|
|
822
|
-
**"
|
|
823
|
-
- Generate keys: `rake openid_federation:prepare_client_keys`
|
|
824
|
-
- Verify key path and format (PEM)
|
|
825
|
-
|
|
826
|
-
**"Audience is required"**
|
|
827
|
-
- Provide `entity_statement_url` and `entity_statement_path` (auto-resolves audience from entity statement)
|
|
828
|
-
|
|
829
|
-
**"Entity statement fingerprint mismatch"**
|
|
830
|
-
- Verify `entity_statement_fingerprint` with provider
|
|
831
|
-
- Fetch fresh entity statement from URL: `rake openid_federation:fetch_entity_statement[entity_statement_url, entity_statement_fingerprint, entity_statement_path]`
|
|
832
|
-
- Always use the provider URL as the source of truth
|
|
833
|
-
|
|
834
|
-
**"JWT signature verification failed"**
|
|
835
|
-
- Provider may have rotated keys (auto-handled with `rotate_on_errors: true`)
|
|
836
|
-
- Clear cache: `Rails.cache.delete_matched("openid_federation_jwks_*")`
|
|
371
|
+
**"Missing authorization code"**: Check that redirect_uri matches provider configuration exactly.
|
|
837
372
|
|
|
838
|
-
**"
|
|
839
|
-
- **Request phase (initiating OAuth)**: Ensure forms include Rails CSRF tokens using `button_to` or `form_with` helpers
|
|
840
|
-
- **Callback phase (external provider redirect)**: Ensure CSRF protection is configured correctly (see [Step 7: Configure CSRF Protection](#step-7-configure-csrf-protection))
|
|
841
|
-
- Verify `OmniAuth.config.request_validation_phase` is configured to skip CSRF validation for callback paths
|
|
842
|
-
- Ensure `skip_before_action :verify_authenticity_token` is present in the callback controller for callback actions
|
|
843
|
-
- Check that OAuth state parameter validation is working (handled automatically by the strategy)
|
|
373
|
+
**"Failed to exchange authorization code"**: Verify private key is correct and client_id matches provider.
|
|
844
374
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
See [SECURITY.md](SECURITY.md) for detailed security features, protections, and vulnerability reporting.
|
|
375
|
+
**"Entity statement not found"**: Ensure entity statement is fetched and cached locally, or provide `entity_statement_url`.
|
|
848
376
|
|
|
849
377
|
## Requirements
|
|
850
378
|
|
|
851
379
|
- Ruby >= 3.0
|
|
852
|
-
- Rails >= 6.
|
|
853
|
-
-
|
|
854
|
-
- `openid_connect` ~> 2.3
|
|
855
|
-
- `jwe` ~> 1.1
|
|
856
|
-
- `jwt` ~> 3.1
|
|
857
|
-
- `http` ~> 5.3
|
|
380
|
+
- Rails >= 6.1 (or compatible Rack application)
|
|
381
|
+
- OpenSSL (for RSA key operations)
|
|
858
382
|
|
|
859
383
|
## Example Files
|
|
860
384
|
|
|
861
385
|
See `examples/` directory for complete configuration examples:
|
|
862
386
|
- `examples/config/initializers/devise.rb.example`
|
|
863
|
-
- `examples/
|
|
864
|
-
- `examples/
|
|
387
|
+
- `examples/config/initializers/omniauth_openid_federation.rb.example`
|
|
388
|
+
- `examples/config/open_id_connect_config.rb.example`
|
|
865
389
|
|
|
866
390
|
## Development
|
|
867
391
|
|
|
868
|
-
Run release.rb script to prepare code for publishing, it has all the required checks and tests.
|
|
869
|
-
|
|
870
|
-
```bash
|
|
871
|
-
usr/bin/release.rb
|
|
872
|
-
```
|
|
873
|
-
|
|
874
|
-
### Development: Using from Local Repository
|
|
875
|
-
|
|
876
|
-
When developing the gem or testing changes in your application, you can point your Gemfile to a local path:
|
|
877
|
-
|
|
878
|
-
```ruby
|
|
879
|
-
# In your application's Gemfile
|
|
880
|
-
gem "omniauth_openid_federation", path: "../omniauth_openid_federation.rb"
|
|
881
|
-
```
|
|
882
|
-
|
|
883
|
-
Then run:
|
|
884
|
-
|
|
885
392
|
```bash
|
|
393
|
+
git clone https://github.com/amkisko/omniauth_openid_federation.rb.git
|
|
394
|
+
cd omniauth_openid_federation.rb
|
|
886
395
|
bundle install
|
|
396
|
+
bin/rspec
|
|
887
397
|
```
|
|
888
398
|
|
|
889
|
-
**Note:** When using `path:` in your Gemfile, Bundler will use the local gem directly. Changes you make to the gem code will be immediately available in your application without needing to rebuild or reinstall the gem. This is ideal for development and testing.
|
|
890
|
-
|
|
891
399
|
## Contributing
|
|
892
400
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
Contribution policy:
|
|
896
|
-
- New features are not necessarily added to the gem
|
|
897
|
-
- Pull request should have test coverage for affected parts
|
|
898
|
-
- Pull request should have changelog entry
|
|
899
|
-
|
|
900
|
-
Review policy:
|
|
901
|
-
- It might take up to 2 calendar weeks to review and merge critical fixes
|
|
902
|
-
- It might take up to 6 calendar months to review and merge pull request
|
|
903
|
-
- It might take up to 1 calendar year to review an issue
|
|
401
|
+
Contributions welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
904
402
|
|
|
403
|
+
## References
|
|
905
404
|
|
|
906
|
-
|
|
405
|
+
### Specifications
|
|
907
406
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
gem push omniauth_openid_federation-*.gem
|
|
912
|
-
```
|
|
407
|
+
- [OpenID Federation 1.0](https://openid.net/specs/openid-federation-1_0.html)
|
|
408
|
+
- [RFC 9101: OAuth 2.0 Authorization Server Issuer Identification](https://www.rfc-editor.org/rfc/rfc9101.html)
|
|
409
|
+
- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
|
|
913
410
|
|
|
914
|
-
|
|
411
|
+
### Related Gems
|
|
915
412
|
|
|
916
|
-
- [
|
|
917
|
-
- [
|
|
918
|
-
- [
|
|
413
|
+
- [omniauth](https://github.com/omniauth/omniauth) - Authentication framework
|
|
414
|
+
- [devise](https://github.com/heartcombo/devise) - Rails authentication solution
|
|
415
|
+
- [jwt](https://github.com/jwt/ruby-jwt) - JSON Web Token implementation
|
|
416
|
+
- [jwe](https://github.com/nov/jwe) - JSON Web Encryption
|
|
417
|
+
- [openid_connect](https://github.com/nov/openid_connect) - OpenID Connect client
|
|
418
|
+
- [http](https://github.com/httprb/http) - HTTP client
|
|
419
|
+
- [anyway_config](https://github.com/palkan/anyway_config) - Configuration management
|
|
420
|
+
- [action_reporter](https://github.com/basecamp/action_reporter) - Error reporting
|
|
919
421
|
|
|
920
422
|
## License
|
|
921
423
|
|
|
922
|
-
|
|
424
|
+
MIT License. See [LICENSE.md](LICENSE.md) for details.
|