9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
|
# File 'lib/omniauth_openid_federation/tasks/callback_processor.rb', line 9
def self.process(
callback_url:,
base_url:,
client_id:, redirect_uri:, private_key:, entity_statement_url: nil,
entity_statement_path: nil,
provider_acr: nil,
client_entity_statement_url: nil,
client_entity_statement_path: nil
)
require "uri"
require "json"
require "base64"
require_relative "../strategy"
results = {
steps_completed: [],
errors: [],
warnings: [],
compliance_checks: {},
token_info: {},
id_token_claims: {}
}
begin
begin
uri = URI.parse(callback_url)
rescue URI::InvalidURIError => e
raise "Invalid callback URL: #{e.message}"
end
pairs = URI.decode_www_form(uri.query || "")
params = pairs.group_by(&:first).transform_values { |vs| vs.map(&:last) }
auth_code = params["code"]&.first
state = params["state"]&.first
error = params["error"]&.first
error_description = params["error_description"]&.first
if error
raise "Authorization error: #{error}#{" - #{error_description}" if error_description}"
end
unless auth_code
raise "No authorization code found in callback URL"
end
results[:authorization_code] = auth_code
results[:state] = state
results[:steps_completed] << "parse_callback"
rescue => e
results[:errors] << "Callback parsing: #{e.message}"
raise
end
begin
resolved_entity_statement_url = entity_statement_url
if resolved_entity_statement_url.nil? && entity_statement_path
resolved_entity_statement_url = "#{base_url}/.well-known/openid-federation"
end
resolved_client_entity_statement_url = client_entity_statement_url
if resolved_client_entity_statement_url.nil? && client_entity_statement_path
resolved_client_entity_statement_url = "#{base_url}/.well-known/openid-federation"
end
strategy_options = {
discovery: true,
scope: [:openid],
response_type: "code",
client_auth_method: :jwt_bearer,
client_signing_alg: :RS256,
always_encrypt_request_object: true,
entity_statement_url: resolved_entity_statement_url,
entity_statement_path: entity_statement_path,
client_entity_statement_url: resolved_client_entity_statement_url,
client_entity_statement_path: client_entity_statement_path,
client_options: {
identifier: client_id,
redirect_uri: redirect_uri,
private_key: private_key
}
}
client_auth_method = strategy_options[:client_auth_method] || :jwt_bearer
strategy_options = strategy_options.reject { |_k, v| v.nil? }
strategy_options[:client_options] = strategy_options[:client_options].reject { |_k, v| v.nil? }
strategy = OmniAuth::Strategies::OpenIDFederation.new(nil, strategy_options)
oidc_client = strategy.client
unless oidc_client
raise "Failed to initialize OpenID Connect client"
end
unless oidc_client.private_key
raise "Private key not set on OpenID Connect client (required for private_key_jwt)"
end
results[:steps_completed] << "initialize_strategy"
rescue => e
results[:errors] << "Strategy initialization: #{e.message}"
raise
end
begin
oidc_client.authorization_code = auth_code
oidc_client.redirect_uri = redirect_uri
access_token = oidc_client.access_token!(client_auth_method)
id_token_raw = access_token.id_token
access_token_value = access_token.access_token
refresh_token = access_token.refresh_token
results[:token_info] = {
access_token: access_token_value ? "#{access_token_value[0..30]}..." : nil,
refresh_token: refresh_token ? "Present" : "Not provided",
id_token_encrypted: id_token_raw ? "#{id_token_raw[0..50]}..." : nil
}
results[:steps_completed] << "token_exchange"
rescue => e
results[:errors] << "Token exchange: #{e.message}"
raise
end
begin
id_token = strategy.send(:decode_id_token, id_token_raw)
results[:id_token_claims] = {
iss: id_token.iss,
sub: id_token.sub,
aud: id_token.aud,
exp: id_token.exp,
iat: id_token.iat,
nonce: id_token.nonce,
acr: id_token.acr,
auth_time: id_token.auth_time,
amr: id_token.amr
}
required_claims = {
iss: id_token.iss,
sub: id_token.sub,
aud: id_token.aud,
exp: id_token.exp,
iat: id_token.iat
}
missing_claims = required_claims.select { |_k, v| v.nil? }
if missing_claims.empty?
results[:id_token_valid] = true
else
results[:errors] << "Missing required ID token claims: #{missing_claims.keys.join(", ")}"
end
results[:steps_completed] << "id_token_validation"
rescue => e
results[:errors] << "ID token validation: #{e.message}"
raise
end
results[:compliance_checks] = {
"Signed Request Objects" => {
status: "✅ MANDATORY",
description: "All requests use signed request objects (RFC 9101)",
verified: true
},
"ID Token Encryption" => {
status: "✅ MANDATORY",
description: "ID tokens are encrypted (RSA-OAEP + A128CBC-HS256)",
verified: OmniauthOpenidFederation::Jwe.encrypted?(id_token_raw)
},
"Client Assertion (private_key_jwt)" => {
status: "✅ MANDATORY",
description: "Token endpoint uses private_key_jwt authentication",
verified: true
},
"Entity Statement JWKS" => {
status: "✅ MANDATORY",
description: "JWKS extracted from entity statement",
verified: StringHelpers.present?(entity_statement_path) || StringHelpers.present?(entity_statement_url)
},
"Signed JWKS Support" => {
status: "✅ MANDATORY",
description: "Supports OpenID Federation signed JWKS for key rotation",
verified: true
}
}
if StringHelpers.present?(client_entity_statement_path) || StringHelpers.present?(client_entity_statement_url)
results[:compliance_checks]["Client Entity Statement"] = {
status: "✅ RECOMMENDED",
description: "Client entity statement for federation-based key management",
verified: true
}
end
if StringHelpers.present?(client_entity_statement_path) || StringHelpers.present?(client_entity_statement_url)
results[:compliance_checks]["Automatic Registration"] = {
status: "✅ ENABLED",
description: "Automatic client registration using entity statement",
verified: true
}
end
results[:all_compliance_verified] = results[:compliance_checks].all? { |_k, v| v[:verified] }
results
end
|