Skip to content

Commit 825d062

Browse files
Merge pull request #18 from opf/claim-validation
Fix validation of essential claims
2 parents f0c1ecd + 87c3f12 commit 825d062

File tree

5 files changed

+179
-32
lines changed

5 files changed

+179
-32
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module OmniAuth
22
module OpenIDConnect
3-
VERSION = '0.4.2'
3+
VERSION = '0.5.0'
44
end
55
end

lib/omniauth/strategies/openid_connect.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ def callback_phase
106106
fail!(:timeout, e)
107107
rescue ::SocketError => e
108108
fail!(:failed_to_connect, e)
109+
rescue OmniAuth::Strategies::OpenIDConnect::Claims::InvalidClaims => e
110+
fail!(:claim_validation_error, e)
109111
end
110112

111113
def other_phase

lib/omniauth/strategies/openid_connect/claims.rb

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ module OmniAuth
22
module Strategies
33
class OpenIDConnect
44
module Claims
5+
class InvalidClaims < Error; end
6+
57
def self.prepended(base)
68
base.class_exec do
79
# Additional, specific claims to be requested on top of those requested via scopes.
@@ -28,7 +30,7 @@ def self.prepended(base)
2830
def validate_access_token!(access_token)
2931
super
3032

31-
verify_id_token_claims! access_token
33+
verify_id_token_claims!(access_token)
3234
end
3335

3436
def authorize_options
@@ -73,41 +75,32 @@ def essential_claims(context)
7375
def verify_id_token_claims!(access_token)
7476
return unless claims?
7577

76-
id_token = decode_id_token access_token.id_token
78+
id_token = decode_id_token(access_token.id_token)
7779

7880
essential_claims(:id_token).each do |claim, request|
79-
require_essential_claim! claim, Hash(request), id_token.send(claim) if id_token.respond_to? claim
81+
fail_missing_claim!(claim) unless id_token.respond_to?(claim)
82+
83+
validate_essential_claim_value!(claim, request, id_token.public_send(claim))
8084
end
8185
end
8286

83-
def require_essential_claim!(claim, request, response)
84-
expected_values = [request["value"]].select(&:present?).presence || claim_values(request["values"].presence)
85-
86-
return unless expected_values.present?
87-
88-
actual_values = claim_values response
89-
90-
return if expected_values.any? { |value| actual_values.include? value }
87+
def fail_missing_claim!(name)
88+
raise InvalidClaims, "Expected #{name} claim, but it was missing"
89+
end
9190

92-
expected = expected_values.map { |v| "'#{v}'" }.join(", ")
93-
actual = actual_values.map { |v| "'#{v}'" }.join(", ")
91+
def validate_essential_claim_value!(claim, request, actual)
92+
requested_values = requested_values(request)
93+
return if requested_values.nil?
94+
return if requested_values.include?(actual)
9495

95-
raise(
96-
::OpenIDConnect::ResponseObject::IdToken::InvalidToken,
97-
"Expected one of #{claim.to_s.upcase} values [#{expected}] in [#{actual}]"
98-
)
96+
raise InvalidClaims, "Expected one of #{claim} values #{requested_values.inspect}, got #{actual.inspect}"
9997
end
10098

101-
##
102-
# Makes sure the given ACR values are parsed correctly as an array.
103-
# They are supposed to be given as an array but in other places such as the `acr_values`
104-
# request parameter they are just a string of space-separated values.
105-
#
106-
# @param input [String, Array<String>] ACR values either directly as an array or as a space-separated string.
107-
#
108-
# @return [Array<String>] An array of ACR values.
109-
def claim_values(input)
110-
Array(input).flat_map { |value| String(value).split(" ") }
99+
def requested_values(request)
100+
return [request["value"]] if request.key?("value")
101+
return request["values"] if request.key?("values")
102+
103+
nil
111104
end
112105
end
113106
end

test/lib/omniauth/strategies/openid_connect_test.rb

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,18 +212,73 @@ def test_callback_phase_with_missing_claims
212212
}
213213
}
214214

215+
stub_id_token = lambda do |id_token|
216+
id_token.stubs(:abc).returns("def")
217+
end
218+
219+
catching_failures do
220+
test_callback_phase(options: options, userinfo: false, stub_id_token: stub_id_token)
221+
expect_authentication_error(
222+
:claim_validation_error,
223+
exception_class: OmniAuth::Strategies::OpenIDConnect::Claims::InvalidClaims,
224+
message: "Expected acr claim, but it was missing"
225+
)
226+
end
227+
end
228+
229+
def test_callback_phase_with_missing_claim_values
230+
options = {
231+
claims: {
232+
id_token: {
233+
acr: {
234+
essential: true,
235+
values: ["phr", "phrh"]
236+
}
237+
}
238+
}
239+
}
240+
215241
stub_id_token = lambda do |id_token|
216242
id_token.stubs(:acr).returns([])
217243
end
218244

219-
ex = assert_raises ::OpenIDConnect::ResponseObject::IdToken::InvalidToken do
245+
catching_failures do
220246
test_callback_phase(options: options, userinfo: false, stub_id_token: stub_id_token)
247+
expect_authentication_error(
248+
:claim_validation_error,
249+
exception_class: OmniAuth::Strategies::OpenIDConnect::Claims::InvalidClaims,
250+
message: 'Expected one of acr values ["phr", "phrh"], got []'
251+
)
252+
end
253+
end
254+
255+
def test_callback_phase_with_wrong_claim_values
256+
options = {
257+
claims: {
258+
id_token: {
259+
acr: {
260+
essential: true,
261+
values: ["phr", "phrh"]
262+
}
263+
}
264+
}
265+
}
266+
267+
stub_id_token = lambda do |id_token|
268+
id_token.stubs(:acr).returns("abc")
221269
end
222270

223-
assert_equal ex.message, "Expected one of ACR values ['phr', 'phrh'] in []"
271+
catching_failures do
272+
test_callback_phase(options: options, userinfo: false, stub_id_token: stub_id_token)
273+
expect_authentication_error(
274+
:claim_validation_error,
275+
exception_class: OmniAuth::Strategies::OpenIDConnect::Claims::InvalidClaims,
276+
message: 'Expected one of acr values ["phr", "phrh"], got "abc"'
277+
)
278+
end
224279
end
225280

226-
def test_callback_phase_with_returned_claims
281+
def test_callback_phase_with_array_result
227282
options = {
228283
claims: {
229284
id_token: {
@@ -239,7 +294,84 @@ def test_callback_phase_with_returned_claims
239294
id_token.stubs(:acr).returns(["phr"])
240295
end
241296

242-
test_callback_phase(options: options, userinfo: true, stub_id_token: stub_id_token)
297+
catching_failures do
298+
# This is a regression test, we used to accept a claim response that was an array with one of the requested
299+
# values, when the specification merely asks to validate that the actual value EQUALS one of the requested values
300+
test_callback_phase(options: options, userinfo: false, stub_id_token: stub_id_token)
301+
expect_authentication_error(
302+
:claim_validation_error,
303+
exception_class: OmniAuth::Strategies::OpenIDConnect::Claims::InvalidClaims,
304+
message: 'Expected one of acr values ["phr", "phrh"], got ["phr"]'
305+
)
306+
end
307+
end
308+
309+
def test_callback_phase_with_expected_claim_value
310+
options = {
311+
claims: {
312+
id_token: {
313+
acr: {
314+
essential: true,
315+
values: ["phr", "phrh"]
316+
}
317+
}
318+
}
319+
}
320+
321+
stub_id_token = lambda do |id_token|
322+
id_token.stubs(:acr).returns("phr")
323+
end
324+
325+
catching_failures do
326+
test_callback_phase(options: options, userinfo: true, stub_id_token: stub_id_token)
327+
expect_no_authentication_error
328+
end
329+
end
330+
331+
def test_callback_phase_with_missing_essential_claims
332+
options = {
333+
claims: {
334+
id_token: {
335+
abc: {
336+
essential: true
337+
}
338+
}
339+
}
340+
}
341+
342+
stub_id_token = lambda do |id_token|
343+
id_token.stubs(:def).returns("ghi")
344+
end
345+
346+
catching_failures do
347+
test_callback_phase(options: options, userinfo: false, stub_id_token: stub_id_token)
348+
expect_authentication_error(
349+
:claim_validation_error,
350+
exception_class: OmniAuth::Strategies::OpenIDConnect::Claims::InvalidClaims,
351+
message: "Expected abc claim, but it was missing"
352+
)
353+
end
354+
end
355+
356+
def test_callback_phase_with_present_essential_claims
357+
options = {
358+
claims: {
359+
id_token: {
360+
abc: {
361+
essential: true
362+
}
363+
}
364+
}
365+
}
366+
367+
stub_id_token = lambda do |id_token|
368+
id_token.stubs(:abc).returns("def")
369+
end
370+
371+
catching_failures do
372+
test_callback_phase(options: options, userinfo: true, stub_id_token: stub_id_token)
373+
expect_no_authentication_error
374+
end
243375
end
244376

245377
def test_callback_phase_with_discovery

test/strategy_test_case.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,24 @@ def strategy
5151
strategy.stubs(:user_info).returns(user_info)
5252
end
5353
end
54+
55+
# Allows to test that properly handled authentication errors occured within the scope of the passed block
56+
def catching_failures
57+
original_failure_handler = OmniAuth.config.on_failure
58+
@failure_env = nil
59+
OmniAuth.config.on_failure = ->(env) { @failure_env = env; [400, {}, "sad sad"] }
60+
yield
61+
ensure
62+
OmniAuth.config.on_failure = original_failure_handler
63+
end
64+
65+
def expect_authentication_error(type, exception_class: nil, message: nil)
66+
assert_equal(@failure_env["omniauth.error.type"], type)
67+
assert_equal(@failure_env["omniauth.error"].class, exception_class) if exception_class
68+
assert_equal(@failure_env["omniauth.error"]&.message, message) if message
69+
end
70+
71+
def expect_no_authentication_error
72+
assert(@failure_env.nil?, "expected no authentication errors")
73+
end
5474
end

0 commit comments

Comments
 (0)