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()]).