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