test_server.rb
2457 lines
| 85.7 KiB
| text/x-ruby
|
RubyLexer
|
r2376 | require 'openid/server' | ||
require 'openid/cryptutil' | ||||
require 'openid/association' | ||||
require 'openid/util' | ||||
require 'openid/message' | ||||
require 'openid/store/memory' | ||||
require 'openid/dh' | ||||
require 'openid/consumer/associationmanager' | ||||
require 'util' | ||||
require "testutil" | ||||
require 'test/unit' | ||||
require 'uri' | ||||
# In general, if you edit or add tests here, try to move in the | ||||
# direction of testing smaller units. For testing the external | ||||
# interfaces, we'll be developing an implementation-agnostic testing | ||||
# suite. | ||||
# for more, see /etc/ssh/moduli | ||||
module OpenID | ||||
ALT_MODULUS = 0xCAADDDEC1667FC68B5FA15D53C4E1532DD24561A1A2D47A12C01ABEA1E00731F6921AAC40742311FDF9E634BB7131BEE1AF240261554389A910425E044E88C8359B010F5AD2B80E29CB1A5B027B19D9E01A6F63A6F45E5D7ED2FF6A2A0085050A7D0CF307C3DB51D2490355907B4427C23A98DF1EB8ABEF2BA209BB7AFFE86A7 | ||||
ALT_GEN = 5 | ||||
class CatchLogs | ||||
def catchlogs_setup | ||||
@old_logger = Util.logger | ||||
Util.logger = self.method('got_log_message') | ||||
@messages = [] | ||||
end | ||||
def got_log_message(message) | ||||
@messages << message | ||||
end | ||||
def teardown | ||||
Util.logger = @old_logger | ||||
end | ||||
end | ||||
class TestProtocolError < Test::Unit::TestCase | ||||
def test_browserWithReturnTo | ||||
return_to = "http://rp.unittest/consumer" | ||||
# will be a ProtocolError raised by Decode or | ||||
# CheckIDRequest.answer | ||||
args = Message.from_post_args({ | ||||
'openid.mode' => 'monkeydance', | ||||
'openid.identity' => 'http://wagu.unittest/', | ||||
'openid.return_to' => return_to, | ||||
}) | ||||
e = Server::ProtocolError.new(args, "plucky") | ||||
assert(e.has_return_to) | ||||
expected_args = { | ||||
'openid.mode' => 'error', | ||||
'openid.error' => 'plucky', | ||||
} | ||||
rt_base, result_args = e.encode_to_url.split('?', 2) | ||||
result_args = Util.parse_query(result_args) | ||||
assert_equal(result_args, expected_args) | ||||
end | ||||
def test_browserWithReturnTo_OpenID2_GET | ||||
return_to = "http://rp.unittest/consumer" | ||||
# will be a ProtocolError raised by Decode or | ||||
# CheckIDRequest.answer | ||||
args = Message.from_post_args({ | ||||
'openid.ns' => OPENID2_NS, | ||||
'openid.mode' => 'monkeydance', | ||||
'openid.identity' => 'http://wagu.unittest/', | ||||
'openid.claimed_id' => 'http://wagu.unittest/', | ||||
'openid.return_to' => return_to, | ||||
}) | ||||
e = Server::ProtocolError.new(args, "plucky") | ||||
assert(e.has_return_to) | ||||
expected_args = { | ||||
'openid.ns' => OPENID2_NS, | ||||
'openid.mode' => 'error', | ||||
'openid.error' => 'plucky', | ||||
} | ||||
rt_base, result_args = e.encode_to_url.split('?', 2) | ||||
result_args = Util.parse_query(result_args) | ||||
assert_equal(result_args, expected_args) | ||||
end | ||||
def test_browserWithReturnTo_OpenID2_POST | ||||
return_to = "http://rp.unittest/consumer" + ('x' * OPENID1_URL_LIMIT) | ||||
# will be a ProtocolError raised by Decode or | ||||
# CheckIDRequest.answer | ||||
args = Message.from_post_args({ | ||||
'openid.ns' => OPENID2_NS, | ||||
'openid.mode' => 'monkeydance', | ||||
'openid.identity' => 'http://wagu.unittest/', | ||||
'openid.claimed_id' => 'http://wagu.unittest/', | ||||
'openid.return_to' => return_to, | ||||
}) | ||||
e = Server::ProtocolError.new(args, "plucky") | ||||
assert(e.has_return_to) | ||||
expected_args = { | ||||
'openid.ns' => OPENID2_NS, | ||||
'openid.mode' => 'error', | ||||
'openid.error' => 'plucky', | ||||
} | ||||
assert(e.which_encoding == Server::ENCODE_HTML_FORM) | ||||
assert(e.to_form_markup == e.to_message.to_form_markup( | ||||
args.get_arg(OPENID_NS, 'return_to'))) | ||||
end | ||||
def test_browserWithReturnTo_OpenID1_exceeds_limit | ||||
return_to = "http://rp.unittest/consumer" + ('x' * OPENID1_URL_LIMIT) | ||||
# will be a ProtocolError raised by Decode or | ||||
# CheckIDRequest.answer | ||||
args = Message.from_post_args({ | ||||
'openid.mode' => 'monkeydance', | ||||
'openid.identity' => 'http://wagu.unittest/', | ||||
'openid.return_to' => return_to, | ||||
}) | ||||
e = Server::ProtocolError.new(args, "plucky") | ||||
assert(e.has_return_to) | ||||
expected_args = { | ||||
'openid.mode' => 'error', | ||||
'openid.error' => 'plucky', | ||||
} | ||||
assert(e.which_encoding == Server::ENCODE_URL) | ||||
rt_base, result_args = e.encode_to_url.split('?', 2) | ||||
result_args = Util.parse_query(result_args) | ||||
assert_equal(result_args, expected_args) | ||||
end | ||||
def test_noReturnTo | ||||
# will be a ProtocolError raised by Decode or | ||||
# CheckIDRequest.answer | ||||
args = Message.from_post_args({ | ||||
'openid.mode' => 'zebradance', | ||||
'openid.identity' => 'http://wagu.unittest/', | ||||
}) | ||||
e = Server::ProtocolError.new(args, "waffles") | ||||
assert(!e.has_return_to) | ||||
expected = "error:waffles\nmode:error\n" | ||||
assert_equal(e.encode_to_kvform, expected) | ||||
end | ||||
def test_no_message | ||||
e = Server::ProtocolError.new(nil, "no message") | ||||
assert(e.get_return_to.nil?) | ||||
assert_equal(e.which_encoding, nil) | ||||
end | ||||
def test_which_encoding_no_message | ||||
e = Server::ProtocolError.new(nil, "no message") | ||||
assert(e.which_encoding.nil?) | ||||
end | ||||
end | ||||
class TestDecode < Test::Unit::TestCase | ||||
def setup | ||||
@claimed_id = 'http://de.legating.de.coder.unittest/' | ||||
@id_url = "http://decoder.am.unittest/" | ||||
@rt_url = "http://rp.unittest/foobot/?qux=zam" | ||||
@tr_url = "http://rp.unittest/" | ||||
@assoc_handle = "{assoc}{handle}" | ||||
@op_endpoint = 'http://endpoint.unittest/encode' | ||||
@store = Store::Memory.new() | ||||
@server = Server::Server.new(@store, @op_endpoint) | ||||
@decode = Server::Decoder.new(@server).method('decode') | ||||
end | ||||
def test_none | ||||
args = {} | ||||
r = @decode.call(args) | ||||
assert_equal(r, nil) | ||||
end | ||||
def test_irrelevant | ||||
args = { | ||||
'pony' => 'spotted', | ||||
'sreg.mutant_power' => 'decaffinator', | ||||
} | ||||
assert_raise(Server::ProtocolError) { | ||||
@decode.call(args) | ||||
} | ||||
end | ||||
def test_bad | ||||
args = { | ||||
'openid.mode' => 'twos-compliment', | ||||
'openid.pants' => 'zippered', | ||||
} | ||||
assert_raise(Server::ProtocolError) { | ||||
@decode.call(args) | ||||
} | ||||
end | ||||
def test_dictOfLists | ||||
args = { | ||||
'openid.mode' => ['checkid_setup'], | ||||
'openid.identity' => @id_url, | ||||
'openid.assoc_handle' => @assoc_handle, | ||||
'openid.return_to' => @rt_url, | ||||
'openid.trust_root' => @tr_url, | ||||
} | ||||
begin | ||||
result = @decode.call(args) | ||||
rescue ArgumentError => err | ||||
assert(!err.to_s.index('values').nil?, err) | ||||
else | ||||
flunk("Expected ArgumentError, but got result #{result}") | ||||
end | ||||
end | ||||
def test_checkidImmediate | ||||
args = { | ||||
'openid.mode' => 'checkid_immediate', | ||||
'openid.identity' => @id_url, | ||||
'openid.assoc_handle' => @assoc_handle, | ||||
'openid.return_to' => @rt_url, | ||||
'openid.trust_root' => @tr_url, | ||||
# should be ignored | ||||
'openid.some.extension' => 'junk', | ||||
} | ||||
r = @decode.call(args) | ||||
assert(r.is_a?(Server::CheckIDRequest)) | ||||
assert_equal(r.mode, "checkid_immediate") | ||||
assert_equal(r.immediate, true) | ||||
assert_equal(r.identity, @id_url) | ||||
assert_equal(r.trust_root, @tr_url) | ||||
assert_equal(r.return_to, @rt_url) | ||||
assert_equal(r.assoc_handle, @assoc_handle) | ||||
end | ||||
def test_checkidImmediate_constructor | ||||
r = Server::CheckIDRequest.new(@id_url, @rt_url, nil, | ||||
@rt_url, true, @assoc_handle) | ||||
assert(r.mode == 'checkid_immediate') | ||||
assert(r.immediate) | ||||
end | ||||
def test_checkid_missing_return_to_and_trust_root | ||||
args = { | ||||
'openid.ns' => OPENID2_NS, | ||||
'openid.mode' => 'checkid_setup', | ||||
'openid.identity' => @id_url, | ||||
'openid.claimed_id' => @id_url, | ||||
'openid.assoc_handle' => @assoc_handle, | ||||
} | ||||
assert_raise(Server::ProtocolError) { | ||||
m = Message.from_post_args(args) | ||||
Server::CheckIDRequest.from_message(m, @op_endpoint) | ||||
} | ||||
end | ||||
def test_checkid_id_select | ||||
args = { | ||||
'openid.ns' => OPENID2_NS, | ||||
'openid.mode' => 'checkid_setup', | ||||
'openid.identity' => IDENTIFIER_SELECT, | ||||
'openid.claimed_id' => IDENTIFIER_SELECT, | ||||
'openid.assoc_handle' => @assoc_handle, | ||||
'openid.return_to' => @rt_url, | ||||
'openid.realm' => @tr_url, | ||||
} | ||||
m = Message.from_post_args(args) | ||||
req = Server::CheckIDRequest.from_message(m, @op_endpoint) | ||||
assert(req.id_select) | ||||
end | ||||
def test_checkid_not_id_select | ||||
args = { | ||||
'openid.ns' => OPENID2_NS, | ||||
'openid.mode' => 'checkid_setup', | ||||
'openid.assoc_handle' => @assoc_handle, | ||||
'openid.return_to' => @rt_url, | ||||
'openid.realm' => @tr_url, | ||||
} | ||||
id_args = [ | ||||
{'openid.claimed_id' => IDENTIFIER_SELECT, | ||||
'openid.identity' => 'http://bogus.com/'}, | ||||
{'openid.claimed_id' => 'http://bogus.com/', | ||||
'openid.identity' => 'http://bogus.com/'}, | ||||
] | ||||
id_args.each { |id| | ||||
m = Message.from_post_args(args.merge(id)) | ||||
req = Server::CheckIDRequest.from_message(m, @op_endpoint) | ||||
assert(!req.id_select) | ||||
} | ||||
end | ||||
def test_checkidSetup | ||||
args = { | ||||
'openid.mode' => 'checkid_setup', | ||||
'openid.identity' => @id_url, | ||||
'openid.assoc_handle' => @assoc_handle, | ||||
'openid.return_to' => @rt_url, | ||||
'openid.trust_root' => @tr_url, | ||||
} | ||||
r = @decode.call(args) | ||||
assert(r.is_a?(Server::CheckIDRequest)) | ||||
assert_equal(r.mode, "checkid_setup") | ||||
assert_equal(r.immediate, false) | ||||
assert_equal(r.identity, @id_url) | ||||
assert_equal(r.trust_root, @tr_url) | ||||
assert_equal(r.return_to, @rt_url) | ||||
end | ||||
def test_checkidSetupOpenID2 | ||||
args = { | ||||
'openid.ns' => OPENID2_NS, | ||||
'openid.mode' => 'checkid_setup', | ||||
'openid.identity' => @id_url, | ||||
'openid.claimed_id' => @claimed_id, | ||||
'openid.assoc_handle' => @assoc_handle, | ||||
'openid.return_to' => @rt_url, | ||||
'openid.realm' => @tr_url, | ||||
} | ||||
r = @decode.call(args) | ||||
assert(r.is_a?(Server::CheckIDRequest)) | ||||
assert_equal(r.mode, "checkid_setup") | ||||
assert_equal(r.immediate, false) | ||||
assert_equal(r.identity, @id_url) | ||||
assert_equal(r.claimed_id, @claimed_id) | ||||
assert_equal(r.trust_root, @tr_url) | ||||
assert_equal(r.return_to, @rt_url) | ||||
end | ||||
def test_checkidSetupNoClaimedIDOpenID2 | ||||
args = { | ||||
'openid.ns' => OPENID2_NS, | ||||
'openid.mode' => 'checkid_setup', | ||||
'openid.identity' => @id_url, | ||||
'openid.assoc_handle' => @assoc_handle, | ||||
'openid.return_to' => @rt_url, | ||||
'openid.realm' => @tr_url, | ||||
} | ||||
assert_raise(Server::ProtocolError) { | ||||
@decode.call(args) | ||||
} | ||||
end | ||||
def test_checkidSetupNoIdentityOpenID2 | ||||
args = { | ||||
'openid.ns' => OPENID2_NS, | ||||
'openid.mode' => 'checkid_setup', | ||||
'openid.assoc_handle' => @assoc_handle, | ||||
'openid.return_to' => @rt_url, | ||||
'openid.realm' => @tr_url, | ||||
} | ||||
r = @decode.call(args) | ||||
assert(r.is_a?(Server::CheckIDRequest)) | ||||
assert_equal(r.mode, "checkid_setup") | ||||
assert_equal(r.immediate, false) | ||||
assert_equal(r.identity, nil) | ||||
assert_equal(r.trust_root, @tr_url) | ||||
assert_equal(r.return_to, @rt_url) | ||||
end | ||||
def test_checkidSetupNoReturnOpenID1 | ||||
# Make sure an OpenID 1 request cannot be decoded if it lacks a | ||||
# return_to. | ||||
args = { | ||||
'openid.mode' => 'checkid_setup', | ||||
'openid.identity' => @id_url, | ||||
'openid.assoc_handle' => @assoc_handle, | ||||
'openid.trust_root' => @tr_url, | ||||
} | ||||
assert_raise(Server::ProtocolError) { | ||||
@decode.call(args) | ||||
} | ||||
end | ||||
def test_checkidSetupNoReturnOpenID2 | ||||
# Make sure an OpenID 2 request with no return_to can be decoded, | ||||
# and make sure a response to such a request raises | ||||
# NoReturnToError. | ||||
args = { | ||||
'openid.ns' => OPENID2_NS, | ||||
'openid.mode' => 'checkid_setup', | ||||
'openid.identity' => @id_url, | ||||
'openid.claimed_id' => @id_url, | ||||
'openid.assoc_handle' => @assoc_handle, | ||||
'openid.realm' => @tr_url, | ||||
} | ||||
req = @decode.call(args) | ||||
assert(req.is_a?(Server::CheckIDRequest)) | ||||
assert_raise(Server::NoReturnToError) { | ||||
req.answer(false) | ||||
} | ||||
assert_raise(Server::NoReturnToError) { | ||||
req.encode_to_url('bogus') | ||||
} | ||||
assert_raise(Server::NoReturnToError) { | ||||
req.cancel_url | ||||
} | ||||
end | ||||
def test_checkidSetupRealmRequiredOpenID2 | ||||
# Make sure that an OpenID 2 request which lacks return_to cannot | ||||
# be decoded if it lacks a realm. Spec => This value | ||||
# (openid.realm) MUST be sent if openid.return_to is omitted. | ||||
args = { | ||||
'openid.ns' => OPENID2_NS, | ||||
'openid.mode' => 'checkid_setup', | ||||
'openid.identity' => @id_url, | ||||
'openid.assoc_handle' => @assoc_handle, | ||||
} | ||||
assert_raise(Server::ProtocolError) { | ||||
@decode.call(args) | ||||
} | ||||
end | ||||
def test_checkidSetupBadReturn | ||||
args = { | ||||
'openid.mode' => 'checkid_setup', | ||||
'openid.identity' => @id_url, | ||||
'openid.assoc_handle' => @assoc_handle, | ||||
'openid.return_to' => 'not a url', | ||||
} | ||||
begin | ||||
result = @decode.call(args) | ||||
rescue Server::ProtocolError => err | ||||
assert(err.openid_message) | ||||
else | ||||
flunk("Expected ProtocolError, instead returned with #{result}") | ||||
end | ||||
end | ||||
def test_checkidSetupUntrustedReturn | ||||
args = { | ||||
'openid.mode' => 'checkid_setup', | ||||
'openid.identity' => @id_url, | ||||
'openid.assoc_handle' => @assoc_handle, | ||||
'openid.return_to' => @rt_url, | ||||
'openid.trust_root' => 'http://not-the-return-place.unittest/', | ||||
} | ||||
begin | ||||
result = @decode.call(args) | ||||
rescue Server::UntrustedReturnURL => err | ||||
assert(err.openid_message, err.to_s) | ||||
else | ||||
flunk("Expected UntrustedReturnURL, instead returned with #{result}") | ||||
end | ||||
end | ||||
def test_checkidSetupUntrustedReturn_Constructor | ||||
assert_raise(Server::UntrustedReturnURL) { | ||||
Server::CheckIDRequest.new(@id_url, @rt_url, nil, | ||||
'http://not-the-return-place.unittest/', | ||||
false, @assoc_handle) | ||||
} | ||||
end | ||||
def test_checkidSetupMalformedReturnURL_Constructor | ||||
assert_raise(Server::MalformedReturnURL) { | ||||
Server::CheckIDRequest.new(@id_url, 'bogus://return.url', nil, | ||||
'http://trustroot.com/', | ||||
false, @assoc_handle) | ||||
} | ||||
end | ||||
def test_checkAuth | ||||
args = { | ||||
'openid.mode' => 'check_authentication', | ||||
'openid.assoc_handle' => '{dumb}{handle}', | ||||
'openid.sig' => 'sigblob', | ||||
'openid.signed' => 'identity,return_to,response_nonce,mode', | ||||
'openid.identity' => 'signedval1', | ||||
'openid.return_to' => 'signedval2', | ||||
'openid.response_nonce' => 'signedval3', | ||||
'openid.baz' => 'unsigned', | ||||
} | ||||
r = @decode.call(args) | ||||
assert(r.is_a?(Server::CheckAuthRequest)) | ||||
assert_equal(r.mode, 'check_authentication') | ||||
assert_equal(r.sig, 'sigblob') | ||||
end | ||||
def test_checkAuthMissingSignature | ||||
args = { | ||||
'openid.mode' => 'check_authentication', | ||||
'openid.assoc_handle' => '{dumb}{handle}', | ||||
'openid.signed' => 'foo,bar,mode', | ||||
'openid.foo' => 'signedval1', | ||||
'openid.bar' => 'signedval2', | ||||
'openid.baz' => 'unsigned', | ||||
} | ||||
assert_raise(Server::ProtocolError) { | ||||
@decode.call(args) | ||||
} | ||||
end | ||||
def test_checkAuthAndInvalidate | ||||
args = { | ||||
'openid.mode' => 'check_authentication', | ||||
'openid.assoc_handle' => '{dumb}{handle}', | ||||
'openid.invalidate_handle' => '[[SMART_handle]]', | ||||
'openid.sig' => 'sigblob', | ||||
'openid.signed' => 'identity,return_to,response_nonce,mode', | ||||
'openid.identity' => 'signedval1', | ||||
'openid.return_to' => 'signedval2', | ||||
'openid.response_nonce' => 'signedval3', | ||||
'openid.baz' => 'unsigned', | ||||
} | ||||
r = @decode.call(args) | ||||
assert(r.is_a?(Server::CheckAuthRequest)) | ||||
assert_equal(r.invalidate_handle, '[[SMART_handle]]') | ||||
end | ||||
def test_associateDH | ||||
args = { | ||||
'openid.mode' => 'associate', | ||||
'openid.session_type' => 'DH-SHA1', | ||||
'openid.dh_consumer_public' => "Rzup9265tw==", | ||||
} | ||||
r = @decode.call(args) | ||||
assert(r.is_a?(Server::AssociateRequest)) | ||||
assert_equal(r.mode, "associate") | ||||
assert_equal(r.session.session_type, "DH-SHA1") | ||||
assert_equal(r.assoc_type, "HMAC-SHA1") | ||||
assert(r.session.consumer_pubkey) | ||||
end | ||||
def test_associateDHMissingKey | ||||
# Trying DH assoc w/o public key | ||||
args = { | ||||
'openid.mode' => 'associate', | ||||
'openid.session_type' => 'DH-SHA1', | ||||
} | ||||
# Using DH-SHA1 without supplying dh_consumer_public is an error. | ||||
assert_raise(Server::ProtocolError) { | ||||
@decode.call(args) | ||||
} | ||||
end | ||||
def test_associateDHpubKeyNotB64 | ||||
args = { | ||||
'openid.mode' => 'associate', | ||||
'openid.session_type' => 'DH-SHA1', | ||||
'openid.dh_consumer_public' => "donkeydonkeydonkey", | ||||
} | ||||
assert_raise(Server::ProtocolError) { | ||||
@decode.call(args) | ||||
} | ||||
end | ||||
def test_associateDHModGen | ||||
# test dh with non-default but valid values for dh_modulus and | ||||
# dh_gen | ||||
args = { | ||||
'openid.mode' => 'associate', | ||||
'openid.session_type' => 'DH-SHA1', | ||||
'openid.dh_consumer_public' => "Rzup9265tw==", | ||||
'openid.dh_modulus' => CryptUtil.num_to_base64(ALT_MODULUS), | ||||
'openid.dh_gen' => CryptUtil.num_to_base64(ALT_GEN) , | ||||
} | ||||
r = @decode.call(args) | ||||
assert(r.is_a?(Server::AssociateRequest)) | ||||
assert_equal(r.mode, "associate") | ||||
assert_equal(r.session.session_type, "DH-SHA1") | ||||
assert_equal(r.assoc_type, "HMAC-SHA1") | ||||
assert_equal(r.session.dh.modulus, ALT_MODULUS) | ||||
assert_equal(r.session.dh.generator, ALT_GEN) | ||||
assert(r.session.consumer_pubkey) | ||||
end | ||||
def test_associateDHCorruptModGen | ||||
# test dh with non-default but valid values for dh_modulus and | ||||
# dh_gen | ||||
args = { | ||||
'openid.mode' => 'associate', | ||||
'openid.session_type' => 'DH-SHA1', | ||||
'openid.dh_consumer_public' => "Rzup9265tw==", | ||||
'openid.dh_modulus' => 'pizza', | ||||
'openid.dh_gen' => 'gnocchi', | ||||
} | ||||
assert_raise(Server::ProtocolError) { | ||||
@decode.call(args) | ||||
} | ||||
end | ||||
def test_associateDHMissingGen | ||||
args = { | ||||
'openid.mode' => 'associate', | ||||
'openid.session_type' => 'DH-SHA1', | ||||
'openid.dh_consumer_public' => "Rzup9265tw==", | ||||
'openid.dh_modulus' => 'pizza', | ||||
} | ||||
assert_raise(Server::ProtocolError) { | ||||
@decode.call(args) | ||||
} | ||||
end | ||||
def test_associateDHMissingMod | ||||
args = { | ||||
'openid.mode' => 'associate', | ||||
'openid.session_type' => 'DH-SHA1', | ||||
'openid.dh_consumer_public' => "Rzup9265tw==", | ||||
'openid.dh_gen' => 'pizza', | ||||
} | ||||
assert_raise(Server::ProtocolError) { | ||||
@decode.call(args) | ||||
} | ||||
end | ||||
# def test_associateDHInvalidModGen(self): | ||||
# # test dh with properly encoded values that are not a valid | ||||
# # modulus/generator combination. | ||||
# args = { | ||||
# 'openid.mode': 'associate', | ||||
# 'openid.session_type': 'DH-SHA1', | ||||
# 'openid.dh_consumer_public': "Rzup9265tw==", | ||||
# 'openid.dh_modulus': cryptutil.longToBase64(9), | ||||
# 'openid.dh_gen': cryptutil.longToBase64(27) , | ||||
# } | ||||
# self.failUnlessRaises(server.ProtocolError, self.decode, args) | ||||
# test_associateDHInvalidModGen.todo = "low-priority feature" | ||||
def test_associateWeirdSession | ||||
args = { | ||||
'openid.mode' => 'associate', | ||||
'openid.session_type' => 'FLCL6', | ||||
'openid.dh_consumer_public' => "YQ==\n", | ||||
} | ||||
assert_raise(Server::ProtocolError) { | ||||
@decode.call(args) | ||||
} | ||||
end | ||||
def test_associatePlain | ||||
args = { | ||||
'openid.mode' => 'associate', | ||||
} | ||||
r = @decode.call(args) | ||||
assert(r.is_a?(Server::AssociateRequest)) | ||||
assert_equal(r.mode, "associate") | ||||
assert_equal(r.session.session_type, "no-encryption") | ||||
assert_equal(r.assoc_type, "HMAC-SHA1") | ||||
end | ||||
def test_nomode | ||||
args = { | ||||
'openid.session_type' => 'DH-SHA1', | ||||
'openid.dh_consumer_public' => "my public keeey", | ||||
} | ||||
assert_raise(Server::ProtocolError) { | ||||
@decode.call(args) | ||||
} | ||||
end | ||||
def test_invalidns | ||||
args = {'openid.ns' => 'Vegetables', | ||||
'openid.mode' => 'associate'} | ||||
begin | ||||
r = @decode.call(args) | ||||
rescue Server::ProtocolError => err | ||||
assert(err.openid_message) | ||||
assert(err.to_s.index('Vegetables')) | ||||
end | ||||
end | ||||
end | ||||
class BogusEncoder < Server::Encoder | ||||
def encode(response) | ||||
return "BOGUS" | ||||
end | ||||
end | ||||
class BogusDecoder < Server::Decoder | ||||
def decode(query) | ||||
return "BOGUS" | ||||
end | ||||
end | ||||
class TestEncode < Test::Unit::TestCase | ||||
def setup | ||||
@encoder = Server::Encoder.new | ||||
@encode = @encoder.method('encode') | ||||
@op_endpoint = 'http://endpoint.unittest/encode' | ||||
@store = Store::Memory.new | ||||
@server = Server::Server.new(@store, @op_endpoint) | ||||
end | ||||
def test_id_res_OpenID2_GET | ||||
# Check that when an OpenID 2 response does not exceed the OpenID | ||||
# 1 message size, a GET response (i.e., redirect) is issued. | ||||
request = Server::CheckIDRequest.new( | ||||
'http://bombom.unittest/', | ||||
'http://burr.unittest/999', | ||||
@server.op_endpoint, | ||||
'http://burr.unittest/', | ||||
false, | ||||
nil) | ||||
request.message = Message.new(OPENID2_NS) | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields = Message.from_openid_args({ | ||||
'ns' => OPENID2_NS, | ||||
'mode' => 'id_res', | ||||
'identity' => request.identity, | ||||
'claimed_id' => request.identity, | ||||
'return_to' => request.return_to, | ||||
}) | ||||
assert(!response.render_as_form) | ||||
assert(response.which_encoding == Server::ENCODE_URL) | ||||
webresponse = @encode.call(response) | ||||
assert(webresponse.headers.member?('location')) | ||||
end | ||||
def test_id_res_OpenID2_POST | ||||
# Check that when an OpenID 2 response exceeds the OpenID 1 | ||||
# message size, a POST response (i.e., an HTML form) is returned. | ||||
request = Server::CheckIDRequest.new( | ||||
'http://bombom.unittest/', | ||||
'http://burr.unittest/999', | ||||
@server.op_endpoint, | ||||
'http://burr.unittest/', | ||||
false, | ||||
nil) | ||||
request.message = Message.new(OPENID2_NS) | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields = Message.from_openid_args({ | ||||
'ns' => OPENID2_NS, | ||||
'mode' => 'id_res', | ||||
'identity' => request.identity, | ||||
'claimed_id' => request.identity, | ||||
'return_to' => 'x' * OPENID1_URL_LIMIT, | ||||
}) | ||||
assert(response.render_as_form) | ||||
assert(response.encode_to_url.length > OPENID1_URL_LIMIT) | ||||
assert(response.which_encoding == Server::ENCODE_HTML_FORM) | ||||
webresponse = @encode.call(response) | ||||
assert_equal(webresponse.body, response.to_form_markup) | ||||
end | ||||
def test_to_form_markup | ||||
request = Server::CheckIDRequest.new( | ||||
'http://bombom.unittest/', | ||||
'http://burr.unittest/999', | ||||
@server.op_endpoint, | ||||
'http://burr.unittest/', | ||||
false, | ||||
nil) | ||||
request.message = Message.new(OPENID2_NS) | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields = Message.from_openid_args({ | ||||
'ns' => OPENID2_NS, | ||||
'mode' => 'id_res', | ||||
'identity' => request.identity, | ||||
'claimed_id' => request.identity, | ||||
'return_to' => 'x' * OPENID1_URL_LIMIT, | ||||
}) | ||||
form_markup = response.to_form_markup({'foo'=>'bar'}) | ||||
assert(/ foo="bar"/ =~ form_markup, form_markup) | ||||
end | ||||
def test_to_html | ||||
request = Server::CheckIDRequest.new( | ||||
'http://bombom.unittest/', | ||||
'http://burr.unittest/999', | ||||
@server.op_endpoint, | ||||
'http://burr.unittest/', | ||||
false, | ||||
nil) | ||||
request.message = Message.new(OPENID2_NS) | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields = Message.from_openid_args({ | ||||
'ns' => OPENID2_NS, | ||||
'mode' => 'id_res', | ||||
'identity' => request.identity, | ||||
'claimed_id' => request.identity, | ||||
'return_to' => 'x' * OPENID1_URL_LIMIT, | ||||
}) | ||||
html = response.to_html | ||||
assert(html) | ||||
end | ||||
def test_id_res_OpenID1_exceeds_limit | ||||
# Check that when an OpenID 1 response exceeds the OpenID 1 | ||||
# message size, a GET response is issued. Technically, this | ||||
# shouldn't be permitted by the library, but this test is in place | ||||
# to preserve the status quo for OpenID 1. | ||||
request = Server::CheckIDRequest.new( | ||||
'http://bombom.unittest/', | ||||
'http://burr.unittest/999', | ||||
@server.op_endpoint, | ||||
'http://burr.unittest/', | ||||
false, | ||||
nil) | ||||
request.message = Message.new(OPENID1_NS) | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields = Message.from_openid_args({ | ||||
'mode' => 'id_res', | ||||
'identity' => request.identity, | ||||
'return_to' => 'x' * OPENID1_URL_LIMIT, | ||||
}) | ||||
assert(!response.render_as_form) | ||||
assert(response.encode_to_url.length > OPENID1_URL_LIMIT) | ||||
assert(response.which_encoding == Server::ENCODE_URL) | ||||
webresponse = @encode.call(response) | ||||
assert_equal(webresponse.headers['location'], response.encode_to_url) | ||||
end | ||||
def test_id_res | ||||
request = Server::CheckIDRequest.new( | ||||
'http://bombom.unittest/', | ||||
'http://burr.unittest/999', | ||||
@server.op_endpoint, | ||||
'http://burr.unittest/', | ||||
false, nil) | ||||
request.message = Message.new(OPENID1_NS) | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields = Message.from_openid_args({ | ||||
'mode' => 'id_res', | ||||
'identity' => request.identity, | ||||
'return_to' => request.return_to, | ||||
}) | ||||
webresponse = @encode.call(response) | ||||
assert_equal(webresponse.code, Server::HTTP_REDIRECT) | ||||
assert(webresponse.headers.member?('location')) | ||||
location = webresponse.headers['location'] | ||||
assert(location.starts_with?(request.return_to), | ||||
sprintf("%s does not start with %s", | ||||
location, request.return_to)) | ||||
# argh. | ||||
q2 = Util.parse_query(URI::parse(location).query) | ||||
expected = response.fields.to_post_args | ||||
assert_equal(q2, expected) | ||||
end | ||||
def test_cancel | ||||
request = Server::CheckIDRequest.new( | ||||
'http://bombom.unittest/', | ||||
'http://burr.unittest/999', | ||||
@server.op_endpoint, | ||||
'http://burr.unittest/', | ||||
false, nil) | ||||
request.message = Message.new(OPENID2_NS) | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields = Message.from_openid_args({ | ||||
'mode' => 'cancel', | ||||
}) | ||||
webresponse = @encode.call(response) | ||||
assert_equal(webresponse.code, Server::HTTP_REDIRECT) | ||||
assert(webresponse.headers.member?('location')) | ||||
end | ||||
def test_cancel_to_form | ||||
request = Server::CheckIDRequest.new( | ||||
'http://bombom.unittest/', | ||||
'http://burr.unittest/999', | ||||
@server.op_endpoint, | ||||
'http://burr.unittest/', | ||||
false, nil) | ||||
request.message = Message.new(OPENID2_NS) | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields = Message.from_openid_args({ | ||||
'mode' => 'cancel', | ||||
}) | ||||
form = response.to_form_markup | ||||
assert(form.index(request.return_to)) | ||||
end | ||||
def test_assocReply | ||||
msg = Message.new(OPENID2_NS) | ||||
msg.set_arg(OPENID2_NS, 'session_type', 'no-encryption') | ||||
request = Server::AssociateRequest.from_message(msg) | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields = Message.from_post_args( | ||||
{'openid.assoc_handle' => "every-zig"}) | ||||
webresponse = @encode.call(response) | ||||
body = "assoc_handle:every-zig\n" | ||||
assert_equal(webresponse.code, Server::HTTP_OK) | ||||
assert_equal(webresponse.headers, {}) | ||||
assert_equal(webresponse.body, body) | ||||
end | ||||
def test_checkauthReply | ||||
request = Server::CheckAuthRequest.new('a_sock_monkey', | ||||
'siggggg', | ||||
[]) | ||||
request.message = Message.new(OPENID2_NS) | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields = Message.from_openid_args({ | ||||
'is_valid' => 'true', | ||||
'invalidate_handle' => 'xXxX:xXXx' | ||||
}) | ||||
body = "invalidate_handle:xXxX:xXXx\nis_valid:true\n" | ||||
webresponse = @encode.call(response) | ||||
assert_equal(webresponse.code, Server::HTTP_OK) | ||||
assert_equal(webresponse.headers, {}) | ||||
assert_equal(webresponse.body, body) | ||||
end | ||||
def test_unencodableError | ||||
args = Message.from_post_args({ | ||||
'openid.identity' => 'http://limu.unittest/', | ||||
}) | ||||
e = Server::ProtocolError.new(args, "wet paint") | ||||
assert_raise(Server::EncodingError) { | ||||
@encode.call(e) | ||||
} | ||||
end | ||||
def test_encodableError | ||||
args = Message.from_post_args({ | ||||
'openid.mode' => 'associate', | ||||
'openid.identity' => 'http://limu.unittest/', | ||||
}) | ||||
body="error:snoot\nmode:error\n" | ||||
webresponse = @encode.call(Server::ProtocolError.new(args, "snoot")) | ||||
assert_equal(webresponse.code, Server::HTTP_ERROR) | ||||
assert_equal(webresponse.headers, {}) | ||||
assert_equal(webresponse.body, body) | ||||
end | ||||
end | ||||
class TestSigningEncode < Test::Unit::TestCase | ||||
def setup | ||||
@_dumb_key = Server::Signatory._dumb_key | ||||
@_normal_key = Server::Signatory._normal_key | ||||
@store = Store::Memory.new() | ||||
@server = Server::Server.new(@store, "http://signing.unittest/enc") | ||||
@request = Server::CheckIDRequest.new( | ||||
'http://bombom.unittest/', | ||||
'http://burr.unittest/999', | ||||
@server.op_endpoint, | ||||
'http://burr.unittest/', | ||||
false, nil) | ||||
@request.message = Message.new(OPENID2_NS) | ||||
@response = Server::OpenIDResponse.new(@request) | ||||
@response.fields = Message.from_openid_args({ | ||||
'mode' => 'id_res', | ||||
'identity' => @request.identity, | ||||
'return_to' => @request.return_to, | ||||
}) | ||||
@signatory = Server::Signatory.new(@store) | ||||
@encoder = Server::SigningEncoder.new(@signatory) | ||||
@encode = @encoder.method('encode') | ||||
end | ||||
def test_idres | ||||
assoc_handle = '{bicycle}{shed}' | ||||
@store.store_association( | ||||
@_normal_key, | ||||
Association.from_expires_in(60, assoc_handle, | ||||
'sekrit', 'HMAC-SHA1')) | ||||
@request.assoc_handle = assoc_handle | ||||
webresponse = @encode.call(@response) | ||||
assert_equal(webresponse.code, Server::HTTP_REDIRECT) | ||||
assert(webresponse.headers.member?('location')) | ||||
location = webresponse.headers['location'] | ||||
query = Util.parse_query(URI::parse(location).query) | ||||
assert(query.member?('openid.sig')) | ||||
assert(query.member?('openid.assoc_handle')) | ||||
assert(query.member?('openid.signed')) | ||||
end | ||||
def test_idresDumb | ||||
webresponse = @encode.call(@response) | ||||
assert_equal(webresponse.code, Server::HTTP_REDIRECT) | ||||
assert(webresponse.headers.has_key?('location')) | ||||
location = webresponse.headers['location'] | ||||
query = Util.parse_query(URI::parse(location).query) | ||||
assert(query.member?('openid.sig')) | ||||
assert(query.member?('openid.assoc_handle')) | ||||
assert(query.member?('openid.signed')) | ||||
end | ||||
def test_forgotStore | ||||
@encoder.signatory = nil | ||||
assert_raise(ArgumentError) { | ||||
@encode.call(@response) | ||||
} | ||||
end | ||||
def test_cancel | ||||
request = Server::CheckIDRequest.new( | ||||
'http://bombom.unittest/', | ||||
'http://burr.unittest/999', | ||||
@server.op_endpoint, | ||||
'http://burr.unittest/', | ||||
false, nil) | ||||
request.message = Message.new(OPENID2_NS) | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields.set_arg(OPENID_NS, 'mode', 'cancel') | ||||
webresponse = @encode.call(response) | ||||
assert_equal(webresponse.code, Server::HTTP_REDIRECT) | ||||
assert(webresponse.headers.has_key?('location')) | ||||
location = webresponse.headers['location'] | ||||
query = Util.parse_query(URI::parse(location).query) | ||||
assert(!query.has_key?('openid.sig'), response.fields.to_post_args()) | ||||
end | ||||
def test_assocReply | ||||
msg = Message.new(OPENID2_NS) | ||||
msg.set_arg(OPENID2_NS, 'session_type', 'no-encryption') | ||||
request = Server::AssociateRequest.from_message(msg) | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields = Message.from_openid_args({'assoc_handle' => "every-zig"}) | ||||
webresponse = @encode.call(response) | ||||
body = "assoc_handle:every-zig\n" | ||||
assert_equal(webresponse.code, Server::HTTP_OK) | ||||
assert_equal(webresponse.headers, {}) | ||||
assert_equal(webresponse.body, body) | ||||
end | ||||
def test_alreadySigned | ||||
@response.fields.set_arg(OPENID_NS, 'sig', 'priorSig==') | ||||
assert_raise(Server::AlreadySigned) { | ||||
@encode.call(@response) | ||||
} | ||||
end | ||||
end | ||||
class TestCheckID < Test::Unit::TestCase | ||||
def setup | ||||
@op_endpoint = 'http://endpoint.unittest/' | ||||
@store = Store::Memory.new() | ||||
@server = Server::Server.new(@store, @op_endpoint) | ||||
@request = Server::CheckIDRequest.new( | ||||
'http://bambam.unittest/', | ||||
'http://bar.unittest/999', | ||||
@server.op_endpoint, | ||||
'http://bar.unittest/', | ||||
false) | ||||
@request.message = Message.new(OPENID2_NS) | ||||
end | ||||
def test_trustRootInvalid | ||||
@request.trust_root = "http://foo.unittest/17" | ||||
@request.return_to = "http://foo.unittest/39" | ||||
assert(!@request.trust_root_valid()) | ||||
end | ||||
def test_trustRootInvalid_modified | ||||
@request.trust_root = "does://not.parse/" | ||||
@request.message = :sentinel | ||||
begin | ||||
result = @request.trust_root_valid | ||||
rescue Server::MalformedTrustRoot => why | ||||
assert_equal(:sentinel, why.openid_message) | ||||
else | ||||
flunk("Expected MalformedTrustRoot, got #{result.inspect}") | ||||
end | ||||
end | ||||
def test_trustRootvalid_absent_trust_root | ||||
@request.trust_root = nil | ||||
assert(@request.trust_root_valid()) | ||||
end | ||||
def test_trustRootValid | ||||
@request.trust_root = "http://foo.unittest/" | ||||
@request.return_to = "http://foo.unittest/39" | ||||
assert(@request.trust_root_valid()) | ||||
end | ||||
def test_trustRootValidNoReturnTo | ||||
request = Server::CheckIDRequest.new( | ||||
'http://bambam.unittest/', | ||||
nil, | ||||
@server.op_endpoint, | ||||
'http://bar.unittest/', | ||||
false) | ||||
assert(request.trust_root_valid()) | ||||
end | ||||
def test_returnToVerified_callsVerify | ||||
# Make sure that verifyReturnTo is calling the trustroot | ||||
# function verifyReturnTo | ||||
# Ensure that exceptions are passed through | ||||
sentinel = Exception.new() | ||||
__req = @request | ||||
tc = self | ||||
vrfyExc = Proc.new { |trust_root, return_to| | ||||
tc.assert_equal(__req.trust_root, trust_root) | ||||
tc.assert_equal(__req.return_to, return_to) | ||||
raise sentinel | ||||
} | ||||
TrustRoot.extend(OverrideMethodMixin) | ||||
TrustRoot.with_method_overridden(:verify_return_to, vrfyExc) do | ||||
begin | ||||
@request.return_to_verified() | ||||
flunk("Expected sentinel to be raised, got success") | ||||
rescue Exception => e | ||||
assert(e.equal?(sentinel), [e, sentinel].inspect) | ||||
end | ||||
end | ||||
# Ensure that True and False are passed through unchanged | ||||
constVerify = Proc.new { |val| | ||||
verify = Proc.new { |trust_root, return_to| | ||||
tc.assert_equal(__req.trust_root, trust_root) | ||||
tc.assert_equal(__req.request.return_to, return_to) | ||||
return val | ||||
} | ||||
return verify | ||||
} | ||||
[true, false].each { |val| | ||||
verifier = constVerify.call(val) | ||||
TrustRoot.with_method_overridden(:verify_return_to, verifier) do | ||||
assert_equal(val, @request.return_to_verified()) | ||||
end | ||||
} | ||||
end | ||||
def _expectAnswer(answer, identity=nil, claimed_id=nil) | ||||
expected_list = [ | ||||
['mode', 'id_res'], | ||||
['return_to', @request.return_to], | ||||
['op_endpoint', @op_endpoint], | ||||
] | ||||
if identity | ||||
expected_list << ['identity', identity] | ||||
if claimed_id | ||||
expected_list << ['claimed_id', claimed_id] | ||||
else | ||||
expected_list << ['claimed_id', identity] | ||||
end | ||||
end | ||||
expected_list.each { |k, expected| | ||||
actual = answer.fields.get_arg(OPENID_NS, k) | ||||
assert_equal(expected, actual, | ||||
sprintf("%s: expected %s, got %s", | ||||
k, expected, actual)) | ||||
} | ||||
assert(answer.fields.has_key?(OPENID_NS, 'response_nonce')) | ||||
assert(answer.fields.get_openid_namespace() == OPENID2_NS) | ||||
# One for nonce, one for ns | ||||
assert_equal(answer.fields.to_post_args.length, | ||||
expected_list.length + 2, | ||||
answer.fields.to_post_args.inspect) | ||||
end | ||||
def test_answerAllow | ||||
# Check the fields specified by "Positive Assertions" | ||||
# | ||||
# including mode=id_res, identity, claimed_id, op_endpoint, | ||||
# return_to | ||||
answer = @request.answer(true) | ||||
assert_equal(answer.request, @request) | ||||
_expectAnswer(answer, @request.identity) | ||||
end | ||||
def test_answerAllowDelegatedIdentity | ||||
@request.claimed_id = 'http://delegating.unittest/' | ||||
answer = @request.answer(true) | ||||
_expectAnswer(answer, @request.identity, | ||||
@request.claimed_id) | ||||
end | ||||
def test_answerAllowWithoutIdentityReally | ||||
@request.identity = nil | ||||
answer = @request.answer(true) | ||||
assert_equal(answer.request, @request) | ||||
_expectAnswer(answer) | ||||
end | ||||
def test_answerAllowAnonymousFail | ||||
@request.identity = nil | ||||
# XXX - Check on this, I think this behavior is legal in OpenID | ||||
# 2.0? | ||||
assert_raise(ArgumentError) { | ||||
@request.answer(true, nil, "=V") | ||||
} | ||||
end | ||||
def test_answerAllowWithIdentity | ||||
@request.identity = IDENTIFIER_SELECT | ||||
selected_id = 'http://anon.unittest/9861' | ||||
answer = @request.answer(true, nil, selected_id) | ||||
_expectAnswer(answer, selected_id) | ||||
end | ||||
def test_answerAllowWithNoIdentity | ||||
@request.identity = IDENTIFIER_SELECT | ||||
selected_id = 'http://anon.unittest/9861' | ||||
assert_raise(ArgumentError) { | ||||
answer = @request.answer(true, nil, nil) | ||||
} | ||||
end | ||||
def test_immediate_openid1_no_identity | ||||
@request.message = Message.new(OPENID1_NS) | ||||
@request.immediate = true | ||||
@request.mode = 'checkid_immediate' | ||||
resp = @request.answer(false) | ||||
assert(resp.fields.get_arg(OPENID_NS, 'mode') == 'id_res') | ||||
end | ||||
def test_checkid_setup_openid1_no_identity | ||||
@request.message = Message.new(OPENID1_NS) | ||||
@request.immediate = false | ||||
@request.mode = 'checkid_setup' | ||||
resp = @request.answer(false) | ||||
assert(resp.fields.get_arg(OPENID_NS, 'mode') == 'cancel') | ||||
end | ||||
def test_immediate_openid1_no_server_url | ||||
@request.message = Message.new(OPENID1_NS) | ||||
@request.immediate = true | ||||
@request.mode = 'checkid_immediate' | ||||
@request.op_endpoint = nil | ||||
assert_raise(ArgumentError) { | ||||
resp = @request.answer(false) | ||||
} | ||||
end | ||||
def test_immediate_encode_to_url | ||||
@request.message = Message.new(OPENID1_NS) | ||||
@request.immediate = true | ||||
@request.mode = 'checkid_immediate' | ||||
@request.trust_root = "BOGUS" | ||||
@request.assoc_handle = "ASSOC" | ||||
server_url = "http://server.com/server" | ||||
url = @request.encode_to_url(server_url) | ||||
assert(url.starts_with?(server_url)) | ||||
unused, query = url.split("?", 2) | ||||
args = Util.parse_query(query) | ||||
m = Message.from_post_args(args) | ||||
assert(m.get_arg(OPENID_NS, 'trust_root') == "BOGUS") | ||||
assert(m.get_arg(OPENID_NS, 'assoc_handle') == "ASSOC") | ||||
assert(m.get_arg(OPENID_NS, 'mode'), "checkid_immediate") | ||||
assert(m.get_arg(OPENID_NS, 'identity') == @request.identity) | ||||
assert(m.get_arg(OPENID_NS, 'claimed_id') == @request.claimed_id) | ||||
assert(m.get_arg(OPENID_NS, 'return_to') == @request.return_to) | ||||
end | ||||
def test_answerAllowWithDelegatedIdentityOpenID2 | ||||
# Answer an IDENTIFIER_SELECT case with a delegated identifier. | ||||
# claimed_id delegates to selected_id here. | ||||
@request.identity = IDENTIFIER_SELECT | ||||
selected_id = 'http://anon.unittest/9861' | ||||
claimed_id = 'http://monkeyhat.unittest/' | ||||
answer = @request.answer(true, nil, selected_id, claimed_id) | ||||
_expectAnswer(answer, selected_id, claimed_id) | ||||
end | ||||
def test_answerAllowWithDelegatedIdentityOpenID1 | ||||
# claimed_id parameter doesn't exist in OpenID 1. | ||||
@request.message = Message.new(OPENID1_NS) | ||||
# claimed_id delegates to selected_id here. | ||||
@request.identity = IDENTIFIER_SELECT | ||||
selected_id = 'http://anon.unittest/9861' | ||||
claimed_id = 'http://monkeyhat.unittest/' | ||||
assert_raise(Server::VersionError) { | ||||
@request.answer(true, nil, selected_id, claimed_id) | ||||
} | ||||
end | ||||
def test_answerAllowWithAnotherIdentity | ||||
# XXX - Check on this, I think this behavior is legal in OpenID | ||||
# 2.0? | ||||
assert_raise(ArgumentError){ | ||||
@request.answer(true, nil, "http://pebbles.unittest/") | ||||
} | ||||
end | ||||
def test_answerAllowNoIdentityOpenID1 | ||||
@request.message = Message.new(OPENID1_NS) | ||||
@request.identity = nil | ||||
assert_raise(ArgumentError) { | ||||
@request.answer(true, nil, nil) | ||||
} | ||||
end | ||||
def test_answerAllowForgotEndpoint | ||||
@request.op_endpoint = nil | ||||
assert_raise(RuntimeError) { | ||||
@request.answer(true) | ||||
} | ||||
end | ||||
def test_checkIDWithNoIdentityOpenID1 | ||||
msg = Message.new(OPENID1_NS) | ||||
msg.set_arg(OPENID_NS, 'return_to', 'bogus') | ||||
msg.set_arg(OPENID_NS, 'trust_root', 'bogus') | ||||
msg.set_arg(OPENID_NS, 'mode', 'checkid_setup') | ||||
msg.set_arg(OPENID_NS, 'assoc_handle', 'bogus') | ||||
assert_raise(Server::ProtocolError) { | ||||
Server::CheckIDRequest.from_message(msg, @server) | ||||
} | ||||
end | ||||
def test_fromMessageClaimedIDWithoutIdentityOpenID2 | ||||
msg = Message.new(OPENID2_NS) | ||||
msg.set_arg(OPENID_NS, 'mode', 'checkid_setup') | ||||
msg.set_arg(OPENID_NS, 'return_to', 'http://invalid:8000/rt') | ||||
msg.set_arg(OPENID_NS, 'claimed_id', 'https://example.myopenid.com') | ||||
assert_raise(Server::ProtocolError) { | ||||
Server::CheckIDRequest.from_message(msg, @server) | ||||
} | ||||
end | ||||
def test_fromMessageIdentityWithoutClaimedIDOpenID2 | ||||
msg = Message.new(OPENID2_NS) | ||||
msg.set_arg(OPENID_NS, 'mode', 'checkid_setup') | ||||
msg.set_arg(OPENID_NS, 'return_to', 'http://invalid:8000/rt') | ||||
msg.set_arg(OPENID_NS, 'identity', 'https://example.myopenid.com') | ||||
assert_raise(Server::ProtocolError) { | ||||
Server::CheckIDRequest.from_message(msg, @server) | ||||
} | ||||
end | ||||
def test_fromMessageWithEmptyTrustRoot | ||||
return_to = 'http://some.url/foo?bar=baz' | ||||
msg = Message.from_post_args({ | ||||
'openid.assoc_handle' => '{blah}{blah}{OZivdQ==}', | ||||
'openid.claimed_id' => 'http://delegated.invalid/', | ||||
'openid.identity' => 'http://op-local.example.com/', | ||||
'openid.mode' => 'checkid_setup', | ||||
'openid.ns' => 'http://openid.net/signon/1.0', | ||||
'openid.return_to' => return_to, | ||||
'openid.trust_root' => '' | ||||
}); | ||||
result = Server::CheckIDRequest.from_message(msg, @server) | ||||
assert_equal(return_to, result.trust_root) | ||||
end | ||||
def test_trustRootOpenID1 | ||||
# Ignore openid.realm in OpenID 1 | ||||
msg = Message.new(OPENID1_NS) | ||||
msg.set_arg(OPENID_NS, 'mode', 'checkid_setup') | ||||
msg.set_arg(OPENID_NS, 'trust_root', 'http://trustroot.com/') | ||||
msg.set_arg(OPENID_NS, 'realm', 'http://fake_trust_root/') | ||||
msg.set_arg(OPENID_NS, 'return_to', 'http://trustroot.com/foo') | ||||
msg.set_arg(OPENID_NS, 'assoc_handle', 'bogus') | ||||
msg.set_arg(OPENID_NS, 'identity', 'george') | ||||
result = Server::CheckIDRequest.from_message(msg, @server.op_endpoint) | ||||
assert(result.trust_root == 'http://trustroot.com/') | ||||
end | ||||
def test_trustRootOpenID2 | ||||
# Ignore openid.trust_root in OpenID 2 | ||||
msg = Message.new(OPENID2_NS) | ||||
msg.set_arg(OPENID_NS, 'mode', 'checkid_setup') | ||||
msg.set_arg(OPENID_NS, 'realm', 'http://trustroot.com/') | ||||
msg.set_arg(OPENID_NS, 'trust_root', 'http://fake_trust_root/') | ||||
msg.set_arg(OPENID_NS, 'return_to', 'http://trustroot.com/foo') | ||||
msg.set_arg(OPENID_NS, 'assoc_handle', 'bogus') | ||||
msg.set_arg(OPENID_NS, 'identity', 'george') | ||||
msg.set_arg(OPENID_NS, 'claimed_id', 'george') | ||||
result = Server::CheckIDRequest.from_message(msg, @server.op_endpoint) | ||||
assert(result.trust_root == 'http://trustroot.com/') | ||||
end | ||||
def test_answerAllowNoTrustRoot | ||||
@request.trust_root = nil | ||||
answer = @request.answer(true) | ||||
assert_equal(answer.request, @request) | ||||
_expectAnswer(answer, @request.identity) | ||||
end | ||||
def test_answerImmediateDenyOpenID2 | ||||
# Look for mode=setup_needed in checkid_immediate negative | ||||
# response in OpenID 2 case. | ||||
# | ||||
# See specification Responding to Authentication Requests / | ||||
# Negative Assertions / In Response to Immediate Requests. | ||||
@request.mode = 'checkid_immediate' | ||||
@request.immediate = true | ||||
server_url = "http://setup-url.unittest/" | ||||
# crappiting setup_url, you dirty my interface with your presence! | ||||
answer = @request.answer(false, server_url) | ||||
assert_equal(answer.request, @request) | ||||
assert_equal(answer.fields.to_post_args.length, 3, answer.fields) | ||||
assert_equal(answer.fields.get_openid_namespace, OPENID2_NS) | ||||
assert_equal(answer.fields.get_arg(OPENID_NS, 'mode'), | ||||
'setup_needed') | ||||
# user_setup_url no longer required. | ||||
end | ||||
def test_answerImmediateDenyOpenID1 | ||||
# Look for user_setup_url in checkid_immediate negative response | ||||
# in OpenID 1 case. | ||||
@request.message = Message.new(OPENID1_NS) | ||||
@request.mode = 'checkid_immediate' | ||||
@request.immediate = true | ||||
@request.claimed_id = 'http://claimed-id.test/' | ||||
server_url = "http://setup-url.unittest/" | ||||
# crappiting setup_url, you dirty my interface with your presence! | ||||
answer = @request.answer(false, server_url) | ||||
assert_equal(answer.request, @request) | ||||
assert_equal(2, answer.fields.to_post_args.length, answer.fields) | ||||
assert_equal(OPENID1_NS, answer.fields.get_openid_namespace) | ||||
assert_equal('id_res', answer.fields.get_arg(OPENID_NS, 'mode')) | ||||
usu = answer.fields.get_arg(OPENID_NS, 'user_setup_url', '') | ||||
assert(usu.starts_with?(server_url)) | ||||
expected_substr = 'openid.claimed_id=http%3A%2F%2Fclaimed-id.test%2F' | ||||
assert(!usu.index(expected_substr).nil?, usu) | ||||
end | ||||
def test_answerSetupDeny | ||||
answer = @request.answer(false) | ||||
assert_equal(answer.fields.get_args(OPENID_NS), { | ||||
'mode' => 'cancel', | ||||
}) | ||||
end | ||||
def test_encodeToURL | ||||
server_url = 'http://openid-server.unittest/' | ||||
result = @request.encode_to_url(server_url) | ||||
# How to check? How about a round-trip test. | ||||
base, result_args = result.split('?', 2) | ||||
result_args = Util.parse_query(result_args) | ||||
message = Message.from_post_args(result_args) | ||||
rebuilt_request = Server::CheckIDRequest.from_message(message, | ||||
@server.op_endpoint) | ||||
@request.message = message | ||||
@request.instance_variables.each { |var| | ||||
assert_equal(@request.instance_variable_get(var), | ||||
rebuilt_request.instance_variable_get(var), var) | ||||
} | ||||
end | ||||
def test_getCancelURL | ||||
url = @request.cancel_url | ||||
rt, query_string = url.split('?', -1) | ||||
assert_equal(@request.return_to, rt) | ||||
query = Util.parse_query(query_string) | ||||
assert_equal(query, {'openid.mode' => 'cancel', | ||||
'openid.ns' => OPENID2_NS}) | ||||
end | ||||
def test_getCancelURLimmed | ||||
@request.mode = 'checkid_immediate' | ||||
@request.immediate = true | ||||
assert_raise(ArgumentError) { | ||||
@request.cancel_url | ||||
} | ||||
end | ||||
def test_fromMessageWithoutTrustRoot | ||||
msg = Message.new(OPENID2_NS) | ||||
msg.set_arg(OPENID_NS, 'mode', 'checkid_setup') | ||||
msg.set_arg(OPENID_NS, 'return_to', 'http://real.trust.root/foo') | ||||
msg.set_arg(OPENID_NS, 'assoc_handle', 'bogus') | ||||
msg.set_arg(OPENID_NS, 'identity', 'george') | ||||
msg.set_arg(OPENID_NS, 'claimed_id', 'george') | ||||
result = Server::CheckIDRequest.from_message(msg, @server.op_endpoint) | ||||
assert_equal(result.trust_root, 'http://real.trust.root/foo') | ||||
end | ||||
def test_fromMessageWithoutTrustRootOrReturnTo | ||||
msg = Message.new(OPENID2_NS) | ||||
msg.set_arg(OPENID_NS, 'mode', 'checkid_setup') | ||||
msg.set_arg(OPENID_NS, 'assoc_handle', 'bogus') | ||||
msg.set_arg(OPENID_NS, 'identity', 'george') | ||||
msg.set_arg(OPENID_NS, 'claimed_id', 'george') | ||||
assert_raises(Server::ProtocolError) { | ||||
Server::CheckIDRequest.from_message(msg, @server.op_endpoint) | ||||
} | ||||
end | ||||
end | ||||
class TestCheckIDExtension < Test::Unit::TestCase | ||||
def setup | ||||
@op_endpoint = 'http://endpoint.unittest/ext' | ||||
@store = Store::Memory.new() | ||||
@server = Server::Server.new(@store, @op_endpoint) | ||||
@request = Server::CheckIDRequest.new( | ||||
'http://bambam.unittest/', | ||||
'http://bar.unittest/999', | ||||
@server.op_endpoint, | ||||
'http://bar.unittest/', | ||||
false) | ||||
@request.message = Message.new(OPENID2_NS) | ||||
@response = Server::OpenIDResponse.new(@request) | ||||
@response.fields.set_arg(OPENID_NS, 'mode', 'id_res') | ||||
@response.fields.set_arg(OPENID_NS, 'blue', 'star') | ||||
end | ||||
def test_addField | ||||
namespace = 'something:' | ||||
@response.fields.set_arg(namespace, 'bright', 'potato') | ||||
assert_equal(@response.fields.get_args(OPENID_NS), | ||||
{'blue' => 'star', | ||||
'mode' => 'id_res', | ||||
}) | ||||
assert_equal(@response.fields.get_args(namespace), | ||||
{'bright' => 'potato'}) | ||||
end | ||||
def test_addFields | ||||
namespace = 'mi5:' | ||||
args = {'tangy' => 'suspenders', | ||||
'bravo' => 'inclusion'} | ||||
@response.fields.update_args(namespace, args) | ||||
assert_equal(@response.fields.get_args(OPENID_NS), | ||||
{'blue' => 'star', | ||||
'mode' => 'id_res', | ||||
}) | ||||
assert_equal(@response.fields.get_args(namespace), args) | ||||
end | ||||
end | ||||
class MockSignatory | ||||
attr_accessor :isValid, :assocs | ||||
def initialize(assoc) | ||||
@isValid = true | ||||
@assocs = [assoc] | ||||
end | ||||
def verify(assoc_handle, message) | ||||
Util.assert(message.has_key?(OPENID_NS, "sig")) | ||||
if self.assocs.member?([true, assoc_handle]) | ||||
return @isValid | ||||
else | ||||
return false | ||||
end | ||||
end | ||||
def get_association(assoc_handle, dumb) | ||||
if self.assocs.member?([dumb, assoc_handle]) | ||||
# This isn't a valid implementation for many uses of this | ||||
# function, mind you. | ||||
return true | ||||
else | ||||
return nil | ||||
end | ||||
end | ||||
def invalidate(assoc_handle, dumb) | ||||
if self.assocs.member?([dumb, assoc_handle]) | ||||
@assocs.delete([dumb, assoc_handle]) | ||||
end | ||||
end | ||||
end | ||||
class TestCheckAuth < Test::Unit::TestCase | ||||
def setup | ||||
@assoc_handle = 'mooooooooo' | ||||
@message = Message.from_post_args({ | ||||
'openid.sig' => 'signarture', | ||||
'one' => 'alpha', | ||||
'two' => 'beta', | ||||
}) | ||||
@request = Server::CheckAuthRequest.new( | ||||
@assoc_handle, @message) | ||||
@request.message = Message.new(OPENID2_NS) | ||||
@signatory = MockSignatory.new([true, @assoc_handle]) | ||||
end | ||||
def test_to_s | ||||
@request.to_s | ||||
end | ||||
def test_valid | ||||
r = @request.answer(@signatory) | ||||
assert_equal({'is_valid' => 'true'}, | ||||
r.fields.get_args(OPENID_NS)) | ||||
assert_equal(r.request, @request) | ||||
end | ||||
def test_invalid | ||||
@signatory.isValid = false | ||||
r = @request.answer(@signatory) | ||||
assert_equal({'is_valid' => 'false'}, | ||||
r.fields.get_args(OPENID_NS)) | ||||
end | ||||
def test_replay | ||||
# Don't validate the same response twice. | ||||
# | ||||
# From "Checking the Nonce":: | ||||
# | ||||
# When using "check_authentication", the OP MUST ensure that an | ||||
# assertion has not yet been accepted with the same value for | ||||
# "openid.response_nonce". | ||||
# | ||||
# In this implementation, the assoc_handle is only valid once. | ||||
# And nonces are a signed component of the message, so they can't | ||||
# be used with another handle without breaking the sig. | ||||
r = @request.answer(@signatory) | ||||
r = @request.answer(@signatory) | ||||
assert_equal({'is_valid' => 'false'}, | ||||
r.fields.get_args(OPENID_NS)) | ||||
end | ||||
def test_invalidatehandle | ||||
@request.invalidate_handle = "bogusHandle" | ||||
r = @request.answer(@signatory) | ||||
assert_equal(r.fields.get_args(OPENID_NS), | ||||
{'is_valid' => 'true', | ||||
'invalidate_handle' => "bogusHandle"}) | ||||
assert_equal(r.request, @request) | ||||
end | ||||
def test_invalidatehandleNo | ||||
assoc_handle = 'goodhandle' | ||||
@signatory.assocs << [false, 'goodhandle'] | ||||
@request.invalidate_handle = assoc_handle | ||||
r = @request.answer(@signatory) | ||||
assert_equal(r.fields.get_args(OPENID_NS), {'is_valid' => 'true'}) | ||||
end | ||||
end | ||||
class TestAssociate < Test::Unit::TestCase | ||||
# TODO: test DH with non-default values for modulus and gen. | ||||
# (important to do because we actually had it broken for a while.) | ||||
def setup | ||||
@request = Server::AssociateRequest.from_message(Message.from_post_args({})) | ||||
@store = Store::Memory.new() | ||||
@signatory = Server::Signatory.new(@store) | ||||
end | ||||
def test_dhSHA1 | ||||
@assoc = @signatory.create_association(false, 'HMAC-SHA1') | ||||
consumer_dh = DiffieHellman.from_defaults() | ||||
cpub = consumer_dh.public | ||||
server_dh = DiffieHellman.from_defaults() | ||||
session = Server::DiffieHellmanSHA1ServerSession.new(server_dh, cpub) | ||||
@request = Server::AssociateRequest.new(session, 'HMAC-SHA1') | ||||
@request.message = Message.new(OPENID2_NS) | ||||
response = @request.answer(@assoc) | ||||
rfg = lambda { |f| response.fields.get_arg(OPENID_NS, f) } | ||||
assert_equal(rfg.call("assoc_type"), "HMAC-SHA1") | ||||
assert_equal(rfg.call("assoc_handle"), @assoc.handle) | ||||
assert(!rfg.call("mac_key")) | ||||
assert_equal(rfg.call("session_type"), "DH-SHA1") | ||||
assert(rfg.call("enc_mac_key")) | ||||
assert(rfg.call("dh_server_public")) | ||||
enc_key = Util.from_base64(rfg.call("enc_mac_key")) | ||||
spub = CryptUtil.base64_to_num(rfg.call("dh_server_public")) | ||||
secret = consumer_dh.xor_secret(CryptUtil.method('sha1'), | ||||
spub, enc_key) | ||||
assert_equal(secret, @assoc.secret) | ||||
end | ||||
def test_dhSHA256 | ||||
@assoc = @signatory.create_association(false, 'HMAC-SHA256') | ||||
consumer_dh = DiffieHellman.from_defaults() | ||||
cpub = consumer_dh.public | ||||
server_dh = DiffieHellman.from_defaults() | ||||
session = Server::DiffieHellmanSHA256ServerSession.new(server_dh, cpub) | ||||
@request = Server::AssociateRequest.new(session, 'HMAC-SHA256') | ||||
@request.message = Message.new(OPENID2_NS) | ||||
response = @request.answer(@assoc) | ||||
rfg = lambda { |f| response.fields.get_arg(OPENID_NS, f) } | ||||
assert_equal(rfg.call("assoc_type"), "HMAC-SHA256") | ||||
assert_equal(rfg.call("assoc_handle"), @assoc.handle) | ||||
assert(!rfg.call("mac_key")) | ||||
assert_equal(rfg.call("session_type"), "DH-SHA256") | ||||
assert(rfg.call("enc_mac_key")) | ||||
assert(rfg.call("dh_server_public")) | ||||
enc_key = Util.from_base64(rfg.call("enc_mac_key")) | ||||
spub = CryptUtil.base64_to_num(rfg.call("dh_server_public")) | ||||
secret = consumer_dh.xor_secret(CryptUtil.method('sha256'), | ||||
spub, enc_key) | ||||
assert_equal(secret, @assoc.secret) | ||||
end | ||||
def test_protoError256 | ||||
s256_session = Consumer::DiffieHellmanSHA256Session.new() | ||||
invalid_s256 = {'openid.assoc_type' => 'HMAC-SHA1', | ||||
'openid.session_type' => 'DH-SHA256',} | ||||
invalid_s256.merge!(s256_session.get_request()) | ||||
invalid_s256_2 = {'openid.assoc_type' => 'MONKEY-PIRATE', | ||||
'openid.session_type' => 'DH-SHA256',} | ||||
invalid_s256_2.merge!(s256_session.get_request()) | ||||
bad_request_argss = [ | ||||
invalid_s256, | ||||
invalid_s256_2, | ||||
] | ||||
bad_request_argss.each { |request_args| | ||||
message = Message.from_post_args(request_args) | ||||
assert_raise(Server::ProtocolError) { | ||||
Server::AssociateRequest.from_message(message) | ||||
} | ||||
} | ||||
end | ||||
def test_protoError | ||||
s1_session = Consumer::DiffieHellmanSHA1Session.new() | ||||
invalid_s1 = {'openid.assoc_type' => 'HMAC-SHA256', | ||||
'openid.session_type' => 'DH-SHA1',} | ||||
invalid_s1.merge!(s1_session.get_request()) | ||||
invalid_s1_2 = {'openid.assoc_type' => 'ROBOT-NINJA', | ||||
'openid.session_type' => 'DH-SHA1',} | ||||
invalid_s1_2.merge!(s1_session.get_request()) | ||||
bad_request_argss = [ | ||||
{'openid.assoc_type' => 'Wha?'}, | ||||
invalid_s1, | ||||
invalid_s1_2, | ||||
] | ||||
bad_request_argss.each { |request_args| | ||||
message = Message.from_post_args(request_args) | ||||
assert_raise(Server::ProtocolError) { | ||||
Server::AssociateRequest.from_message(message) | ||||
} | ||||
} | ||||
end | ||||
def test_protoErrorFields | ||||
contact = 'user@example.invalid' | ||||
reference = 'Trac ticket number MAX_INT' | ||||
error = 'poltergeist' | ||||
openid1_args = { | ||||
'openid.identitiy' => 'invalid', | ||||
'openid.mode' => 'checkid_setup', | ||||
} | ||||
openid2_args = openid1_args.dup | ||||
openid2_args.merge!({'openid.ns' => OPENID2_NS}) | ||||
# Check presence of optional fields in both protocol versions | ||||
openid1_msg = Message.from_post_args(openid1_args) | ||||
p = Server::ProtocolError.new(openid1_msg, error, | ||||
reference, contact) | ||||
reply = p.to_message() | ||||
assert_equal(reply.get_arg(OPENID_NS, 'reference'), reference) | ||||
assert_equal(reply.get_arg(OPENID_NS, 'contact'), contact) | ||||
openid2_msg = Message.from_post_args(openid2_args) | ||||
p = Server::ProtocolError.new(openid2_msg, error, | ||||
reference, contact) | ||||
reply = p.to_message() | ||||
assert_equal(reply.get_arg(OPENID_NS, 'reference'), reference) | ||||
assert_equal(reply.get_arg(OPENID_NS, 'contact'), contact) | ||||
end | ||||
def failUnlessExpiresInMatches(msg, expected_expires_in) | ||||
expires_in_str = msg.get_arg(OPENID_NS, 'expires_in', NO_DEFAULT) | ||||
expires_in = expires_in_str.to_i | ||||
# Slop is necessary because the tests can sometimes get run | ||||
# right on a second boundary | ||||
slop = 1 # second | ||||
difference = expected_expires_in - expires_in | ||||
error_message = sprintf('"expires_in" value not within %s of expected: ' + | ||||
'expected=%s, actual=%s', slop, expected_expires_in, | ||||
expires_in) | ||||
assert((0 <= difference and difference <= slop), error_message) | ||||
end | ||||
def test_plaintext | ||||
@assoc = @signatory.create_association(false, 'HMAC-SHA1') | ||||
response = @request.answer(@assoc) | ||||
rfg = lambda { |f| response.fields.get_arg(OPENID_NS, f) } | ||||
assert_equal(rfg.call("assoc_type"), "HMAC-SHA1") | ||||
assert_equal(rfg.call("assoc_handle"), @assoc.handle) | ||||
failUnlessExpiresInMatches(response.fields, | ||||
@signatory.secret_lifetime) | ||||
assert_equal( | ||||
rfg.call("mac_key"), Util.to_base64(@assoc.secret)) | ||||
assert(!rfg.call("session_type")) | ||||
assert(!rfg.call("enc_mac_key")) | ||||
assert(!rfg.call("dh_server_public")) | ||||
end | ||||
def test_plaintext_v2 | ||||
# The main difference between this and the v1 test is that | ||||
# session_type is always returned in v2. | ||||
args = { | ||||
'openid.ns' => OPENID2_NS, | ||||
'openid.mode' => 'associate', | ||||
'openid.assoc_type' => 'HMAC-SHA1', | ||||
'openid.session_type' => 'no-encryption', | ||||
} | ||||
@request = Server::AssociateRequest.from_message( | ||||
Message.from_post_args(args)) | ||||
assert(!@request.message.is_openid1()) | ||||
@assoc = @signatory.create_association(false, 'HMAC-SHA1') | ||||
response = @request.answer(@assoc) | ||||
rfg = lambda { |f| response.fields.get_arg(OPENID_NS, f) } | ||||
assert_equal(rfg.call("assoc_type"), "HMAC-SHA1") | ||||
assert_equal(rfg.call("assoc_handle"), @assoc.handle) | ||||
failUnlessExpiresInMatches( | ||||
response.fields, @signatory.secret_lifetime) | ||||
assert_equal( | ||||
rfg.call("mac_key"), Util.to_base64(@assoc.secret)) | ||||
assert_equal(rfg.call("session_type"), "no-encryption") | ||||
assert(!rfg.call("enc_mac_key")) | ||||
assert(!rfg.call("dh_server_public")) | ||||
end | ||||
def test_plaintext256 | ||||
@assoc = @signatory.create_association(false, 'HMAC-SHA256') | ||||
response = @request.answer(@assoc) | ||||
rfg = lambda { |f| response.fields.get_arg(OPENID_NS, f) } | ||||
assert_equal(rfg.call("assoc_type"), "HMAC-SHA1") | ||||
assert_equal(rfg.call("assoc_handle"), @assoc.handle) | ||||
failUnlessExpiresInMatches( | ||||
response.fields, @signatory.secret_lifetime) | ||||
assert_equal( | ||||
rfg.call("mac_key"), Util.to_base64(@assoc.secret)) | ||||
assert(!rfg.call("session_type")) | ||||
assert(!rfg.call("enc_mac_key")) | ||||
assert(!rfg.call("dh_server_public")) | ||||
end | ||||
def test_unsupportedPrefer | ||||
allowed_assoc = 'COLD-PET-RAT' | ||||
allowed_sess = 'FROG-BONES' | ||||
message = 'This is a unit test' | ||||
# Set an OpenID 2 message so answerUnsupported doesn't raise | ||||
# ProtocolError. | ||||
@request.message = Message.new(OPENID2_NS) | ||||
response = @request.answer_unsupported(message, | ||||
allowed_assoc, | ||||
allowed_sess) | ||||
rfg = lambda { |f| response.fields.get_arg(OPENID_NS, f) } | ||||
assert_equal(rfg.call('error_code'), 'unsupported-type') | ||||
assert_equal(rfg.call('assoc_type'), allowed_assoc) | ||||
assert_equal(rfg.call('error'), message) | ||||
assert_equal(rfg.call('session_type'), allowed_sess) | ||||
end | ||||
def test_unsupported | ||||
message = 'This is a unit test' | ||||
# Set an OpenID 2 message so answerUnsupported doesn't raise | ||||
# ProtocolError. | ||||
@request.message = Message.new(OPENID2_NS) | ||||
response = @request.answer_unsupported(message) | ||||
rfg = lambda { |f| response.fields.get_arg(OPENID_NS, f) } | ||||
assert_equal(rfg.call('error_code'), 'unsupported-type') | ||||
assert_equal(rfg.call('assoc_type'), nil) | ||||
assert_equal(rfg.call('error'), message) | ||||
assert_equal(rfg.call('session_type'), nil) | ||||
end | ||||
def test_openid1_unsupported_explode | ||||
# answer_unsupported on an associate request should explode if | ||||
# the request was an OpenID 1 request. | ||||
m = Message.new(OPENID1_NS) | ||||
assert_raise(Server::ProtocolError) { | ||||
@request.answer_unsupported(m) | ||||
} | ||||
end | ||||
end | ||||
class Counter | ||||
def initialize | ||||
@count = 0 | ||||
end | ||||
def inc | ||||
@count += 1 | ||||
end | ||||
end | ||||
class UnhandledError < Exception | ||||
end | ||||
class TestServer < Test::Unit::TestCase | ||||
include TestUtil | ||||
def setup | ||||
@store = Store::Memory.new() | ||||
@server = Server::Server.new(@store, "http://server.unittest/endpt") | ||||
# catchlogs_setup() | ||||
end | ||||
def test_failed_dispatch | ||||
request = Server::OpenIDRequest.new() | ||||
request.mode = "monkeymode" | ||||
request.message = Message.new(OPENID1_NS) | ||||
assert_raise(RuntimeError) { | ||||
webresult = @server.handle_request(request) | ||||
} | ||||
end | ||||
def test_decode_request | ||||
@server.decoder = BogusDecoder.new(@server) | ||||
assert(@server.decode_request({}) == "BOGUS") | ||||
end | ||||
def test_encode_response | ||||
@server.encoder = BogusEncoder.new | ||||
assert(@server.encode_response(nil) == "BOGUS") | ||||
end | ||||
def test_dispatch | ||||
monkeycalled = Counter.new() | ||||
@server.extend(InstanceDefExtension) | ||||
@server.instance_def(:openid_monkeymode) do |request| | ||||
raise UnhandledError | ||||
end | ||||
request = Server::OpenIDRequest.new() | ||||
request.mode = "monkeymode" | ||||
request.message = Message.new(OPENID1_NS) | ||||
assert_raise(UnhandledError) { | ||||
webresult = @server.handle_request(request) | ||||
} | ||||
end | ||||
def test_associate | ||||
request = Server::AssociateRequest.from_message(Message.from_post_args({})) | ||||
response = @server.openid_associate(request) | ||||
assert(response.fields.has_key?(OPENID_NS, "assoc_handle"), | ||||
sprintf("No assoc_handle here: %s", response.fields.inspect)) | ||||
end | ||||
def test_associate2 | ||||
# Associate when the server has no allowed association types | ||||
# | ||||
# Gives back an error with error_code and no fallback session or | ||||
# assoc types. | ||||
@server.negotiator.allowed_types = [] | ||||
# Set an OpenID 2 message so answerUnsupported doesn't raise | ||||
# ProtocolError. | ||||
msg = Message.from_post_args({ | ||||
'openid.ns' => OPENID2_NS, | ||||
'openid.session_type' => 'no-encryption', | ||||
}) | ||||
request = Server::AssociateRequest.from_message(msg) | ||||
response = @server.openid_associate(request) | ||||
assert(response.fields.has_key?(OPENID_NS, "error")) | ||||
assert(response.fields.has_key?(OPENID_NS, "error_code")) | ||||
assert(!response.fields.has_key?(OPENID_NS, "assoc_handle")) | ||||
assert(!response.fields.has_key?(OPENID_NS, "assoc_type")) | ||||
assert(!response.fields.has_key?(OPENID_NS, "session_type")) | ||||
end | ||||
def test_associate3 | ||||
# Request an assoc type that is not supported when there are | ||||
# supported types. | ||||
# | ||||
# Should give back an error message with a fallback type. | ||||
@server.negotiator.allowed_types = [['HMAC-SHA256', 'DH-SHA256']] | ||||
msg = Message.from_post_args({ | ||||
'openid.ns' => OPENID2_NS, | ||||
'openid.session_type' => 'no-encryption', | ||||
}) | ||||
request = Server::AssociateRequest.from_message(msg) | ||||
response = @server.openid_associate(request) | ||||
assert(response.fields.has_key?(OPENID_NS, "error")) | ||||
assert(response.fields.has_key?(OPENID_NS, "error_code")) | ||||
assert(!response.fields.has_key?(OPENID_NS, "assoc_handle")) | ||||
assert_equal(response.fields.get_arg(OPENID_NS, "assoc_type"), | ||||
'HMAC-SHA256') | ||||
assert_equal(response.fields.get_arg(OPENID_NS, "session_type"), | ||||
'DH-SHA256') | ||||
end | ||||
def test_associate4 | ||||
# DH-SHA256 association session | ||||
@server.negotiator.allowed_types = [['HMAC-SHA256', 'DH-SHA256']] | ||||
query = { | ||||
'openid.dh_consumer_public' => | ||||
'ALZgnx8N5Lgd7pCj8K86T/DDMFjJXSss1SKoLmxE72kJTzOtG6I2PaYrHX' + | ||||
'xku4jMQWSsGfLJxwCZ6280uYjUST/9NWmuAfcrBfmDHIBc3H8xh6RBnlXJ' + | ||||
'1WxJY3jHd5k1/ZReyRZOxZTKdF/dnIqwF8ZXUwI6peV0TyS/K1fOfF/s', | ||||
'openid.assoc_type' => 'HMAC-SHA256', | ||||
'openid.session_type' => 'DH-SHA256', | ||||
} | ||||
message = Message.from_post_args(query) | ||||
request = Server::AssociateRequest.from_message(message) | ||||
response = @server.openid_associate(request) | ||||
assert(response.fields.has_key?(OPENID_NS, "assoc_handle")) | ||||
end | ||||
def test_no_encryption_openid1 | ||||
# Make sure no-encryption associate requests for OpenID 1 are | ||||
# logged. | ||||
assert_log_matches(/Continuing anyway./) { | ||||
m = Message.from_openid_args({ | ||||
'session_type' => 'no-encryption', | ||||
}) | ||||
req = Server::AssociateRequest.from_message(m) | ||||
} | ||||
end | ||||
def test_missingSessionTypeOpenID2 | ||||
# Make sure session_type is required in OpenID 2 | ||||
msg = Message.from_post_args({ | ||||
'openid.ns' => OPENID2_NS, | ||||
}) | ||||
assert_raises(Server::ProtocolError) { | ||||
Server::AssociateRequest.from_message(msg) | ||||
} | ||||
end | ||||
def test_checkAuth | ||||
request = Server::CheckAuthRequest.new('arrrrrf', '0x3999', []) | ||||
request.message = Message.new(OPENID2_NS) | ||||
response = nil | ||||
silence_logging { | ||||
response = @server.openid_check_authentication(request) | ||||
} | ||||
assert(response.fields.has_key?(OPENID_NS, "is_valid")) | ||||
end | ||||
end | ||||
class TestingRequest < Server::OpenIDRequest | ||||
attr_accessor :assoc_handle, :namespace | ||||
end | ||||
class TestSignatory < Test::Unit::TestCase | ||||
include TestUtil | ||||
def setup | ||||
@store = Store::Memory.new() | ||||
@signatory = Server::Signatory.new(@store) | ||||
@_dumb_key = @signatory.class._dumb_key | ||||
@_normal_key = @signatory.class._normal_key | ||||
# CatchLogs.setUp(self) | ||||
end | ||||
def test_get_association_nil | ||||
assert_raises(ArgumentError) { | ||||
@signatory.get_association(nil, false) | ||||
} | ||||
end | ||||
def test_sign | ||||
request = TestingRequest.new() | ||||
assoc_handle = '{assoc}{lookatme}' | ||||
@store.store_association( | ||||
@_normal_key, | ||||
Association.from_expires_in(60, assoc_handle, | ||||
'sekrit', 'HMAC-SHA1')) | ||||
request.assoc_handle = assoc_handle | ||||
request.namespace = OPENID1_NS | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields = Message.from_openid_args({ | ||||
'foo' => 'amsigned', | ||||
'bar' => 'notsigned', | ||||
'azu' => 'alsosigned', | ||||
}) | ||||
sresponse = @signatory.sign(response) | ||||
assert_equal( | ||||
sresponse.fields.get_arg(OPENID_NS, 'assoc_handle'), | ||||
assoc_handle) | ||||
assert_equal(sresponse.fields.get_arg(OPENID_NS, 'signed'), | ||||
'assoc_handle,azu,bar,foo,signed') | ||||
assert(sresponse.fields.get_arg(OPENID_NS, 'sig')) | ||||
# assert(!@messages, @messages) | ||||
end | ||||
def test_signDumb | ||||
request = TestingRequest.new() | ||||
request.assoc_handle = nil | ||||
request.namespace = OPENID2_NS | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields = Message.from_openid_args({ | ||||
'foo' => 'amsigned', | ||||
'bar' => 'notsigned', | ||||
'azu' => 'alsosigned', | ||||
'ns' => OPENID2_NS, | ||||
}) | ||||
sresponse = @signatory.sign(response) | ||||
assoc_handle = sresponse.fields.get_arg(OPENID_NS, 'assoc_handle') | ||||
assert(assoc_handle) | ||||
assoc = @signatory.get_association(assoc_handle, true) | ||||
assert(assoc) | ||||
assert_equal(sresponse.fields.get_arg(OPENID_NS, 'signed'), | ||||
'assoc_handle,azu,bar,foo,ns,signed') | ||||
assert(sresponse.fields.get_arg(OPENID_NS, 'sig')) | ||||
# assert(!@messages, @messages) | ||||
end | ||||
def test_signExpired | ||||
# Sign a response to a message with an expired handle (using | ||||
# invalidate_handle). | ||||
# | ||||
# From "Verifying with an Association": | ||||
# | ||||
# If an authentication request included an association handle | ||||
# for an association between the OP and the Relying party, and | ||||
# the OP no longer wishes to use that handle (because it has | ||||
# expired or the secret has been compromised, for instance), | ||||
# the OP will send a response that must be verified directly | ||||
# with the OP, as specified in Section 11.3.2. In that | ||||
# instance, the OP will include the field | ||||
# "openid.invalidate_handle" set to the association handle | ||||
# that the Relying Party included with the original request. | ||||
request = TestingRequest.new() | ||||
request.namespace = OPENID2_NS | ||||
assoc_handle = '{assoc}{lookatme}' | ||||
@store.store_association( | ||||
@_normal_key, | ||||
Association.from_expires_in(-10, assoc_handle, | ||||
'sekrit', 'HMAC-SHA1')) | ||||
assert(@store.get_association(@_normal_key, assoc_handle)) | ||||
request.assoc_handle = assoc_handle | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields = Message.from_openid_args({ | ||||
'foo' => 'amsigned', | ||||
'bar' => 'notsigned', | ||||
'azu' => 'alsosigned', | ||||
}) | ||||
sresponse = nil | ||||
silence_logging { | ||||
sresponse = @signatory.sign(response) | ||||
} | ||||
new_assoc_handle = sresponse.fields.get_arg(OPENID_NS, 'assoc_handle') | ||||
assert(new_assoc_handle) | ||||
assert(new_assoc_handle != assoc_handle) | ||||
assert_equal( | ||||
sresponse.fields.get_arg(OPENID_NS, 'invalidate_handle'), | ||||
assoc_handle) | ||||
assert_equal(sresponse.fields.get_arg(OPENID_NS, 'signed'), | ||||
'assoc_handle,azu,bar,foo,invalidate_handle,signed') | ||||
assert(sresponse.fields.get_arg(OPENID_NS, 'sig')) | ||||
# make sure the expired association is gone | ||||
assert(!@store.get_association(@_normal_key, assoc_handle), | ||||
"expired association is still retrievable.") | ||||
# make sure the new key is a dumb mode association | ||||
assert(@store.get_association(@_dumb_key, new_assoc_handle)) | ||||
assert(!@store.get_association(@_normal_key, new_assoc_handle)) | ||||
# assert(@messages) | ||||
end | ||||
def test_signInvalidHandle | ||||
request = TestingRequest.new() | ||||
request.namespace = OPENID2_NS | ||||
assoc_handle = '{bogus-assoc}{notvalid}' | ||||
request.assoc_handle = assoc_handle | ||||
response = Server::OpenIDResponse.new(request) | ||||
response.fields = Message.from_openid_args({ | ||||
'foo' => 'amsigned', | ||||
'bar' => 'notsigned', | ||||
'azu' => 'alsosigned', | ||||
}) | ||||
sresponse = @signatory.sign(response) | ||||
new_assoc_handle = sresponse.fields.get_arg(OPENID_NS, 'assoc_handle') | ||||
assert(new_assoc_handle) | ||||
assert(new_assoc_handle != assoc_handle) | ||||
assert_equal( | ||||
sresponse.fields.get_arg(OPENID_NS, 'invalidate_handle'), | ||||
assoc_handle) | ||||
assert_equal( | ||||
sresponse.fields.get_arg(OPENID_NS, 'signed'), | ||||
'assoc_handle,azu,bar,foo,invalidate_handle,signed') | ||||
assert(sresponse.fields.get_arg(OPENID_NS, 'sig')) | ||||
# make sure the new key is a dumb mode association | ||||
assert(@store.get_association(@_dumb_key, new_assoc_handle)) | ||||
assert(!@store.get_association(@_normal_key, new_assoc_handle)) | ||||
# @failIf(@messages, @messages) | ||||
end | ||||
def test_verify | ||||
assoc_handle = '{vroom}{zoom}' | ||||
assoc = Association.from_expires_in( | ||||
60, assoc_handle, 'sekrit', 'HMAC-SHA1') | ||||
@store.store_association(@_dumb_key, assoc) | ||||
signed = Message.from_post_args({ | ||||
'openid.foo' => 'bar', | ||||
'openid.apple' => 'orange', | ||||
'openid.assoc_handle' => assoc_handle, | ||||
'openid.signed' => 'apple,assoc_handle,foo,signed', | ||||
'openid.sig' => 'uXoT1qm62/BB09Xbj98TQ8mlBco=', | ||||
}) | ||||
verified = @signatory.verify(assoc_handle, signed) | ||||
assert(verified) | ||||
# assert(!@messages, @messages) | ||||
end | ||||
def test_verifyBadSig | ||||
assoc_handle = '{vroom}{zoom}' | ||||
assoc = Association.from_expires_in( | ||||
60, assoc_handle, 'sekrit', 'HMAC-SHA1') | ||||
@store.store_association(@_dumb_key, assoc) | ||||
signed = Message.from_post_args({ | ||||
'openid.foo' => 'bar', | ||||
'openid.apple' => 'orange', | ||||
'openid.assoc_handle' => assoc_handle, | ||||
'openid.signed' => 'apple,assoc_handle,foo,signed', | ||||
'openid.sig' => 'uXoT1qm62/BB09Xbj98TQ8mlBco=BOGUS' | ||||
}) | ||||
verified = @signatory.verify(assoc_handle, signed) | ||||
# @failIf(@messages, @messages) | ||||
assert(!verified) | ||||
end | ||||
def test_verifyBadHandle | ||||
assoc_handle = '{vroom}{zoom}' | ||||
signed = Message.from_post_args({ | ||||
'foo' => 'bar', | ||||
'apple' => 'orange', | ||||
'openid.sig' => "Ylu0KcIR7PvNegB/K41KpnRgJl0=", | ||||
}) | ||||
verified = nil | ||||
silence_logging { | ||||
verified = @signatory.verify(assoc_handle, signed) | ||||
} | ||||
assert(!verified) | ||||
#assert(@messages) | ||||
end | ||||
def test_verifyAssocMismatch | ||||
# Attempt to validate sign-all message with a signed-list assoc. | ||||
assoc_handle = '{vroom}{zoom}' | ||||
assoc = Association.from_expires_in( | ||||
60, assoc_handle, 'sekrit', 'HMAC-SHA1') | ||||
@store.store_association(@_dumb_key, assoc) | ||||
signed = Message.from_post_args({ | ||||
'foo' => 'bar', | ||||
'apple' => 'orange', | ||||
'openid.sig' => "d71xlHtqnq98DonoSgoK/nD+QRM=", | ||||
}) | ||||
verified = nil | ||||
silence_logging { | ||||
verified = @signatory.verify(assoc_handle, signed) | ||||
} | ||||
assert(!verified) | ||||
#assert(@messages) | ||||
end | ||||
def test_getAssoc | ||||
assoc_handle = makeAssoc(true) | ||||
assoc = @signatory.get_association(assoc_handle, true) | ||||
assert(assoc) | ||||
assert_equal(assoc.handle, assoc_handle) | ||||
# @failIf(@messages, @messages) | ||||
end | ||||
def test_getAssocExpired | ||||
assoc_handle = makeAssoc(true, -10) | ||||
assoc = nil | ||||
silence_logging { | ||||
assoc = @signatory.get_association(assoc_handle, true) | ||||
} | ||||
assert(!assoc, assoc) | ||||
# assert(@messages) | ||||
end | ||||
def test_getAssocInvalid | ||||
ah = 'no-such-handle' | ||||
silence_logging { | ||||
assert_equal( | ||||
@signatory.get_association(ah, false), nil) | ||||
} | ||||
# assert(!@messages, @messages) | ||||
end | ||||
def test_getAssocDumbVsNormal | ||||
# getAssociation(dumb=False) cannot get a dumb assoc | ||||
assoc_handle = makeAssoc(true) | ||||
silence_logging { | ||||
assert_equal( | ||||
@signatory.get_association(assoc_handle, false), nil) | ||||
} | ||||
# @failIf(@messages, @messages) | ||||
end | ||||
def test_getAssocNormalVsDumb | ||||
# getAssociation(dumb=True) cannot get a shared assoc | ||||
# | ||||
# From "Verifying Directly with the OpenID Provider":: | ||||
# | ||||
# An OP MUST NOT verify signatures for associations that have shared | ||||
# MAC keys. | ||||
assoc_handle = makeAssoc(false) | ||||
silence_logging { | ||||
assert_equal( | ||||
@signatory.get_association(assoc_handle, true), nil) | ||||
} | ||||
# @failIf(@messages, @messages) | ||||
end | ||||
def test_createAssociation | ||||
assoc = @signatory.create_association(false) | ||||
silence_logging { | ||||
assert(@signatory.get_association(assoc.handle, false)) | ||||
} | ||||
# @failIf(@messages, @messages) | ||||
end | ||||
def makeAssoc(dumb, lifetime=60) | ||||
assoc_handle = '{bling}' | ||||
assoc = Association.from_expires_in(lifetime, assoc_handle, | ||||
'sekrit', 'HMAC-SHA1') | ||||
silence_logging { | ||||
@store.store_association(((dumb and @_dumb_key) or @_normal_key), assoc) | ||||
} | ||||
return assoc_handle | ||||
end | ||||
def test_invalidate | ||||
assoc_handle = '-squash-' | ||||
assoc = Association.from_expires_in(60, assoc_handle, | ||||
'sekrit', 'HMAC-SHA1') | ||||
silence_logging { | ||||
@store.store_association(@_dumb_key, assoc) | ||||
assoc = @signatory.get_association(assoc_handle, true) | ||||
assert(assoc) | ||||
assoc = @signatory.get_association(assoc_handle, true) | ||||
assert(assoc) | ||||
@signatory.invalidate(assoc_handle, true) | ||||
assoc = @signatory.get_association(assoc_handle, true) | ||||
assert(!assoc) | ||||
} | ||||
# @failIf(@messages, @messages) | ||||
end | ||||
end | ||||
class RunthroughTestCase < Test::Unit::TestCase | ||||
def setup | ||||
@store = Store::Memory.new | ||||
@server = Server::Server.new(@store, "http://example.com/openid/server") | ||||
end | ||||
def test_openid1_assoc_checkid | ||||
assoc_args = {'openid.mode' => 'associate', | ||||
'openid.assoc_type' => 'HMAC-SHA1'} | ||||
areq = @server.decode_request(assoc_args) | ||||
aresp = @server.handle_request(areq) | ||||
amess = aresp.fields | ||||
assert(amess.is_openid1) | ||||
ahandle = amess.get_arg(OPENID_NS, 'assoc_handle') | ||||
assert(ahandle) | ||||
assoc = @store.get_association('http://localhost/|normal', ahandle) | ||||
assert(assoc.is_a?(Association)) | ||||
checkid_args = {'openid.mode' => 'checkid_setup', | ||||
'openid.return_to' => 'http://example.com/openid/consumer', | ||||
'openid.assoc_handle' => ahandle, | ||||
'openid.identity' => 'http://foo.com/'} | ||||
cireq = @server.decode_request(checkid_args) | ||||
ciresp = cireq.answer(true) | ||||
signed_resp = @server.signatory.sign(ciresp) | ||||
assert_equal(assoc.get_message_signature(signed_resp.fields), | ||||
signed_resp.fields.get_arg(OPENID_NS, 'sig')) | ||||
assert(assoc.check_message_signature(signed_resp.fields)) | ||||
end | ||||
end | ||||
end | ||||