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