1: -module(auth_tokens_SUITE). 2: -compile([export_all, nowarn_export_all]). 3: 4: -include_lib("exml/include/exml.hrl"). 5: -include_lib("common_test/include/ct.hrl"). 6: -include_lib("proper/include/proper.hrl"). 7: -include_lib("eunit/include/eunit.hrl"). 8: 9: -include("jlib.hrl"). 10: -include("mod_auth_token.hrl"). 11: 12: -import(prop_helper, [prop/2]). 13: 14: -define(TESTED, mod_auth_token). 15: -define(ae(Expected, Actual), ?assertEqual(Expected, Actual)). 16: 17: -define(l2b(List), list_to_binary(List)). 18: -define(i2b(I), integer_to_binary(I)). 19: 20: all() -> 21: [{group, creation}, 22: {group, revocation}]. 23: 24: groups() -> 25: [ 26: {creation, [], 27: [ 28: expiry_date_roundtrip_test, 29: join_and_split_with_base16_and_zeros_are_reversible_property, 30: serialize_deserialize_property, 31: validation_test, 32: validation_property, 33: validity_period_test, 34: choose_key_by_token_type 35: ]}, 36: {revocation, [], 37: [ 38: revoked_token_is_not_valid 39: ]} 40: ]. 41: 42: init_per_suite(C) -> 43: {ok, _} = application:ensure_all_started(jid), 44: C. 45: 46: end_per_suite(C) -> 47: C. 48: 49: init_per_testcase(Test, Config) 50: when Test =:= serialize_deserialize_property; 51: Test =:= validation_test; 52: Test =:= validation_property; 53: Test =:= choose_key_by_token_type -> 54: mock_mongoose_metrics(), 55: Config1 = async_helper:start(Config, [{gen_hook, start_link, []}]), 56: mock_keystore(), 57: mock_rdbms_backend(), 58: Config1; 59: 60: init_per_testcase(validity_period_test, Config) -> 61: mongoose_config:set_opt({modules, host_type()}, 62: #{?TESTED => validity_period_cfg(access, {13, hours})}), 63: mock_rdbms_backend(), 64: mock_mongoose_metrics(), 65: mock_gen_iq_handler(), 66: mock_ejabberd_commands(), 67: async_helper:start(Config, [{gen_hook, start_link, []}]); 68: 69: init_per_testcase(revoked_token_is_not_valid, Config) -> 70: mock_mongoose_metrics(), 71: mock_tested_backend(), 72: Config1 = async_helper:start(Config, [{gen_hook, start_link, []}]), 73: mock_keystore(), 74: Config1; 75: 76: init_per_testcase(_, C) -> C. 77: 78: end_per_testcase(Test, C) 79: when Test =:= serialize_deserialize_property; 80: Test =:= validation_test; 81: Test =:= validation_property; 82: Test =:= choose_key_by_token_type -> 83: meck:unload(mongoose_metrics), 84: meck:unload(mod_auth_token_backend), 85: async_helper:stop_all(C), 86: C; 87: 88: end_per_testcase(validity_period_test, C) -> 89: mongoose_config:unset_opt({modules, host_type()}), 90: meck:unload(mod_auth_token_backend), 91: meck:unload(mongoose_metrics), 92: meck:unload(gen_iq_handler), 93: meck:unload(ejabberd_commands), 94: async_helper:stop_all(C), 95: C; 96: 97: end_per_testcase(revoked_token_is_not_valid, C) -> 98: meck:unload(mongoose_metrics), 99: meck:unload(mod_auth_token_backend), 100: async_helper:stop_all(C), 101: C; 102: 103: end_per_testcase(_, C) -> C. 104: 105: %% 106: %% Tests 107: %% 108: 109: expiry_date_roundtrip_test(_) -> 110: D = {{2015,9,17},{20,28,21}}, %% DateTime 111: S = mod_auth_token:datetime_to_seconds(D), 112: ResD = mod_auth_token:seconds_to_datetime(S), 113: ?ae(D, ResD). 114: 115: join_and_split_with_base16_and_zeros_are_reversible_property(_) -> 116: prop(join_and_split_are_reversible_property, 117: ?FORALL(RawToken, serialized_token(<<0>>), 118: is_join_and_split_with_base16_and_zeros_reversible(RawToken))). 119: 120: serialize_deserialize_property(_) -> 121: prop(serialize_deserialize_property, 122: ?FORALL(Token, token(), is_serialization_reversible(Token))). 123: 124: validation_test(Config) -> 125: validation_test(Config, provision_token_example()), 126: validation_test(Config, refresh_token_example()). 127: 128: validation_test(_, ExampleToken) -> 129: %% given 130: Serialized = ?TESTED:serialize(ExampleToken), 131: %% when 132: Result = ?TESTED:authenticate(host_type(), Serialized), 133: %% then 134: ?ae(true, is_validation_success(Result)). 135: 136: validation_property(_) -> 137: prop(validation_property, 138: ?FORALL(Token, valid_token(), is_valid_token_prop(Token))). 139: 140: validity_period_test(_) -> 141: %% given 142: ok = ?TESTED:start(host_type(), mongoose_config:get_opt([{modules, host_type()}, ?TESTED])), 143: UTCSeconds = utc_now_as_seconds(), 144: ExpectedSeconds = UTCSeconds + ( 13 %% hours 145: * 3600 %% seconds per hour 146: ), 147: %% when 148: ActualDT = ?TESTED:expiry_datetime(host_type(), access, UTCSeconds), 149: %% then 150: ?ae(calendar:gregorian_seconds_to_datetime(ExpectedSeconds), 151: ActualDT). 152: 153: choose_key_by_token_type(_) -> 154: %% given mocked keystore (see init_per_testcase) 155: %% when mod_auth_token asks for key for given token type 156: %% then the correct key is returned 157: ?ae(<<"access_or_refresh">>, ?TESTED:get_key_for_host_type(host_type(), access)), 158: ?ae(<<"access_or_refresh">>, ?TESTED:get_key_for_host_type(host_type(), refresh)), 159: ?ae(<<"provision">>, ?TESTED:get_key_for_host_type(host_type(), provision)). 160: 161: is_join_and_split_with_base16_and_zeros_reversible(RawToken) -> 162: MAC = base16:encode(crypto:mac(hmac, sha384, <<"unused_key">>, RawToken)), 163: Token = <<RawToken/bytes, 0, MAC/bytes>>, 164: BodyPartsLen = length(binary:split(RawToken, <<0>>, [global])), 165: Parts = binary:split(Token, <<0>>, [global]), 166: case BodyPartsLen + 1 == length(Parts) of 167: true -> true; 168: false -> 169: ct:pal("invalid MAC: ~s", [MAC]), 170: false 171: end. 172: 173: is_serialization_reversible(Token) -> 174: Token =:= ?TESTED:deserialize(?TESTED:serialize(Token)). 175: 176: is_valid_token_prop(Token) -> 177: Serialized = ?TESTED:serialize(Token), 178: R = ?TESTED:authenticate(host_type(), Serialized), 179: case is_validation_success(R) of 180: true -> true; 181: _ -> ct:fail(R) 182: end. 183: 184: is_validation_success(Result) -> 185: case Result of 186: {ok, _, _} -> true; 187: {ok, _, _, _} -> true; 188: _ -> false 189: end. 190: 191: revoked_token_is_not_valid(_) -> 192: %% given 193: ValidSeqNo = 123456, 194: RevokedSeqNo = 123455, 195: self() ! {valid_seq_no, ValidSeqNo}, 196: T = #token{type = refresh, 197: expiry_datetime = ?TESTED:seconds_to_datetime(utc_now_as_seconds() + 10), 198: user_jid = jid:from_binary(<<"alice@localhost">>), 199: sequence_no = RevokedSeqNo}, 200: Revoked = ?TESTED:serialize(?TESTED:token_with_mac(host_type(), T)), 201: %% when 202: ValidationResult = ?TESTED:authenticate(host_type(), Revoked), 203: %% then 204: {error, _} = ValidationResult. 205: 206: %% 207: %% Helpers 208: %% 209: 210: datetime_to_seconds(DateTime) -> 211: calendar:datetime_to_gregorian_seconds(DateTime). 212: 213: seconds_to_datetime(Seconds) -> 214: calendar:gregorian_seconds_to_datetime(Seconds). 215: 216: utc_now_as_seconds() -> 217: calendar:datetime_to_gregorian_seconds(calendar:universal_time()). 218: 219: %% Like in: 220: %% {modules, [ 221: %% {mod_auth_token, [{{validity_period, access}, {13, minutes}}, 222: %% {{validity_period, refresh}, {13, days}}]} 223: %% ]}. 224: validity_period_cfg(Type, Period) -> 225: [{{validity_period, Type}, Period}]. 226: 227: %% This is a negative test case helper - that's why we invert the logic below. 228: %% I.e. we expect the property to fail. 229: negative_prop(Name, Prop) -> 230: Props = proper:conjunction([{Name, Prop}]), 231: [[{Name, _}]] = proper:quickcheck(Props, [verbose, long_result, {numtests, 50}]). 232: 233: mock_mongoose_metrics() -> 234: meck:new(mongoose_metrics, []), 235: meck:expect(mongoose_metrics, create_generic_hook_metric, fun (_, _) -> ok end), 236: meck:expect(mongoose_metrics, increment_generic_hook_metric, fun (_, _) -> ok end), 237: ok. 238: 239: mock_rdbms_backend() -> 240: meck:new(mod_auth_token_backend, []), 241: meck:expect(mod_auth_token_backend, start, fun(_, _) -> ok end), 242: meck:expect(mod_auth_token_backend, get_valid_sequence_number, 243: fun (_, _) -> valid_seq_no_threshold() end), 244: ok. 245: 246: mock_keystore() -> 247: ejabberd_hooks:add(get_key, host_type(), ?MODULE, mod_keystore_get_key, 50). 248: 249: mock_gen_iq_handler() -> 250: meck:new(gen_iq_handler, []), 251: meck:expect(gen_iq_handler, add_iq_handler_for_domain, fun (_, _, _, _, _, _) -> ok end). 252: 253: mod_keystore_get_key(_, {KeyName, _} = KeyID) -> 254: case KeyName of 255: token_secret -> [{KeyID, <<"access_or_refresh">>}]; 256: provision_pre_shared -> [{KeyID, <<"provision">>}] 257: end. 258: 259: mock_tested_backend() -> 260: meck:new(mod_auth_token_backend, []), 261: meck:expect(mod_auth_token_backend, get_valid_sequence_number, 262: fun (_, _) -> 263: receive {valid_seq_no, SeqNo} -> SeqNo end 264: end). 265: 266: mock_ejabberd_commands() -> 267: meck:new(ejabberd_commands, []), 268: meck:expect(ejabberd_commands, register_commands, fun (_) -> ok end). 269: 270: provision_token_example() -> 271: {token,provision, 272: {{2055,10,27},{10,54,22}}, 273: {jid,<<"cEE2M1S0I">>,domain(),<<>>,<<"cee2m1s0i">>, 274: domain(),<<>>}, 275: undefined, 276: {xmlel,<<"vCard">>, 277: [{<<"sgzldnl">>,<<"inxdutpu">>}, 278: {<<"scmgsrfi">>,<<"nhgybwu">>}, 279: {<<"ixrsmzee">>,<<"rysdh">>}, 280: {<<"oxwothgyei">>,<<"wderkfgexv">>}], 281: [{xmlel,<<"nqe">>, 282: [{<<"i">>,<<"u">>}, 283: {<<"gagnixjgml">>,<<"odaorofnra">>}, 284: {<<"ijz">>,<<"zvbrqnybi">>}], 285: [{xmlcdata,<<"uprmzqf">>}, 286: {xmlel,<<"lnnitxm">>, 287: [{<<"qytehi">>,<<"axl">>}, 288: {<<"xaxforb">>,<<"jrdeydsqhj">>}], 289: []}, 290: {xmlcdata,<<"pncgsaxl">>}, 291: {xmlel,<<"jfofazuau">>,[{<<"si">>,<<"l">>}],[]}]}, 292: {xmlel,<<"moy">>, 293: [{<<"femjc">>,<<"qqb">>},{<<"tirfmekvpk">>,<<"sa">>}], 294: []}, 295: {xmlcdata,<<"bgxlyqdeeuo">>}]}, 296: <<109,213,86,17,172,7,27,229,193,103,207,86,43,31,239,117,234,234, 297: 232,0,223,168,125,154,189,87,232,159,77,11,35,216,127,171,83,207, 298: 208,184,40,208,45,102,189,131,110,204,245,28>>, 299: <<112,114,111,118,105,115,105,111,110,0,99,69,69,50,77,49,83,48,73, 300: 64,108,111,99,97,108,104,111,115,116,0,54,52,56,55,53,52,54,54,52, 301: 54,50,0,60,118,67,97,114,100,32,115,103,122,108,100,110,108,61,39, 302: 105,110,120,100,117,116,112,117,39,32,115,99,109,103,115,114,102, 303: 105,61,39,110,104,103,121,98,119,117,39,32,105,120,114,115,109, 304: 122,101,101,61,39,114,121,115,100,104,39,32,111,120,119,111,116, 305: 104,103,121,101,105,61,39,119,100,101,114,107,102,103,101,120,118, 306: 39,62,60,110,113,101,32,105,61,39,117,39,32,103,97,103,110,105, 307: 120,106,103,109,108,61,39,111,100,97,111,114,111,102,110,114,97, 308: 39,32,105,106,122,61,39,122,118,98,114,113,110,121,98,105,39,62, 309: 117,112,114,109,122,113,102,60,108,110,110,105,116,120,109,32,113, 310: 121,116,101,104,105,61,39,97,120,108,39,32,120,97,120,102,111,114, 311: 98,61,39,106,114,100,101,121,100,115,113,104,106,39,47,62,112,110, 312: 99,103,115,97,120,108,60,106,102,111,102,97,122,117,97,117,32,115, 313: 105,61,39,108,39,47,62,60,47,110,113,101,62,60,109,111,121,32,102, 314: 101,109,106,99,61,39,113,113,98,39,32,116,105,114,102,109,101,107, 315: 118,112,107,61,39,115,97,39,47,62,98,103,120,108,121,113,100,101, 316: 101,117,111,60,47,118,67,97,114,100,62>>}. 317: 318: refresh_token_example() -> 319: {token,refresh, 320: {{2055,10,27},{10,54,14}}, 321: {jid,<<"a">>,domain(),<<>>,<<"a">>,domain(),<<>>}, 322: 4,undefined, 323: <<151,225,117,181,0,168,228,208,238,182,157,253,24,200,231,25,189, 324: 160,176,144,85,193,20,108,31,23,46,35,215,41,250,57,68,201,45,33, 325: 241,219,197,83,155,118,217,92,172,42,8,118>>, 326: <<114,101,102,114,101,115,104,0,97,64,108,111,99,97,108,104,111,115, 327: 116,0,54,52,56,55,53,52,54,54,52,53,52,0,52>>}. 328: 329: %% 330: %% Generators 331: %% 332: 333: valid_token() -> 334: ?LET(TokenParts, {token_type(), valid_expiry_datetime(), 335: bare_jid(), valid_seq_no(), vcard()}, 336: make_token(TokenParts)). 337: 338: %% Arbitrary date in the future. 339: validity_threshold() -> 340: {{2055,10,27}, {10,54,14}}. 341: 342: valid_seq_no_threshold() -> 343: 3. 344: 345: valid_seq_no() -> 346: integer(valid_seq_no_threshold() + 1, inf). 347: 348: token() -> 349: ?LET(TokenParts, {token_type(), expiry_datetime(), 350: bare_jid(), seq_no(), vcard()}, 351: make_token(TokenParts)). 352: 353: make_token({Type, Expiry, JID, SeqNo, VCard}) -> 354: T = #token{type = Type, 355: expiry_datetime = Expiry, 356: user_jid = jid:from_binary(JID)}, 357: case Type of 358: access -> 359: ?TESTED:token_with_mac(host_type(), T); 360: refresh -> 361: ?TESTED:token_with_mac(host_type(), T#token{sequence_no = SeqNo}); 362: provision -> 363: ?TESTED:token_with_mac(host_type(), T#token{vcard = VCard}) 364: end. 365: 366: serialized_token(Sep) -> 367: ?LET({Type, JID, Expiry, SeqNo}, 368: {oneof([<<"access">>, <<"refresh">>]), bare_jid(), expiry_date_as_seconds(), seq_no()}, 369: case Type of 370: <<"access">> -> 371: <<"access", Sep/bytes, JID/bytes, Sep/bytes, (?i2b(Expiry))/bytes>>; 372: <<"refresh">> -> 373: <<"refresh", Sep/bytes, JID/bytes, Sep/bytes, (?i2b(Expiry))/bytes, 374: Sep/bytes, (?i2b(SeqNo))/bytes>> 375: end). 376: 377: token_type() -> 378: oneof([access, refresh, provision]). 379: 380: expiry_datetime() -> 381: ?LET(Seconds, pos_integer(), seconds_to_datetime(Seconds)). 382: 383: valid_expiry_datetime() -> 384: ?LET(Seconds, integer( datetime_to_seconds(validity_threshold()), 385: datetime_to_seconds({{2100,1,1},{0,0,0}}) ), 386: seconds_to_datetime(Seconds)). 387: 388: expiry_date_as_seconds() -> pos_integer(). 389: 390: seq_no() -> pos_integer(). 391: 392: vcard() -> 393: ?LET(Element, xmlel_gen:xmlel(3), 394: Element#xmlel{name = <<"vCard">>}). 395: 396: bare_jid() -> 397: ?LET({Username, Domain}, {username(), domain()}, 398: <<(?l2b(Username))/bytes, "@", (Domain)/bytes>>). 399: 400: %full_jid() -> 401: % ?LET({Username, Domain, Res}, {username(), domain(), resource()}, 402: % <<(?l2b(Username))/bytes, "@", (?l2b(Domain))/bytes, "/", (?l2b(Res))/bytes>>). 403: 404: username() -> ascii_string(). 405: domain() -> <<"localhost">>. 406: %resource() -> ascii_string(). 407: host_type() -> <<"localhost">>. 408: 409: ascii_string() -> 410: ?LET({Alpha, Alnum}, {ascii_alpha(), list(ascii_alnum())}, [Alpha | Alnum]). 411: 412: ascii_digit() -> choose($0, $9). 413: ascii_lower() -> choose($a, $z). 414: ascii_upper() -> choose($A, $Z). 415: ascii_alpha() -> union([ascii_lower(), ascii_upper()]). 416: ascii_alnum() -> union([ascii_alpha(), ascii_digit()]).