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