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