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