1: %%==============================================================================
    2: %% Copyright 2014 Erlang Solutions Ltd.
    3: %%
    4: %% Licensed under the Apache License, Version 2.0 (the "License");
    5: %% you may not use this file except in compliance with the License.
    6: %% You may obtain a copy of the License at
    7: %%
    8: %% http://www.apache.org/licenses/LICENSE-2.0
    9: %%
   10: %% Unless required by applicable law or agreed to in writing, software
   11: %% distributed under the License is distributed on an "AS IS" BASIS,
   12: %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   13: %% See the License for the specific language governing permissions and
   14: %% limitations under the License.
   15: %%==============================================================================
   16: 
   17: -module(auth_http_SUITE).
   18: -compile([export_all, nowarn_export_all]).
   19: -author('piotr.nosek@erlang-solutions.com').
   20: 
   21: -include_lib("common_test/include/ct.hrl").
   22: 
   23: -define(DOMAIN, <<"localhost">>).
   24: -define(HOST_TYPE, <<"test host type">>).
   25: -define(AUTH_HOST, "http://localhost:12000").
   26: -define(BASIC_AUTH, "softkitty:purrpurrpurr").
   27: 
   28: %%--------------------------------------------------------------------
   29: %% Suite configuration
   30: %%--------------------------------------------------------------------
   31: 
   32: all() ->
   33:     [{group, auth_requests_plain}, {group, auth_requests_scram}].
   34: 
   35: groups() ->
   36:     [
   37:      {cert_auth, cert_auth()},
   38:      {auth_requests_plain, [sequence], all_tests()},
   39:      {auth_requests_scram, [sequence], [{group, cert_auth} | all_tests()]}
   40:     ].
   41: 
   42: all_tests() ->
   43:     [
   44:      check_password,
   45:      set_password,
   46:      try_register,
   47:      get_password,
   48:      does_user_exist,
   49:      remove_user,
   50:      supported_sasl_mechanisms
   51:     ].
   52: 
   53: cert_auth() ->
   54:     [
   55:         cert_auth_fail,
   56:         cert_auth_success,
   57:         cert_auth_nonexistent
   58:     ].
   59: 
   60: suite() ->
   61:     [].
   62: 
   63: %%--------------------------------------------------------------------
   64: %% Init & teardown
   65: %%--------------------------------------------------------------------
   66: 
   67: init_per_suite(Config) ->
   68:     {ok, _} = application:ensure_all_started(jid),
   69:     set_opts(Config),
   70:     mim_ct_rest:start(?BASIC_AUTH, Config),
   71:     % Separate process needs to do this, because this one will terminate
   72:     % so will supervisor and children and ETS tables
   73:     mim_ct_rest:do(
   74:       fun() ->
   75:               mim_ct_sup:start_link(ejabberd_sup),
   76:               mongoose_wpool:ensure_started(),
   77:               % This would be started via outgoing_pools in normal case
   78:               Pool = {http, host, auth,
   79:                       [{strategy, random_worker}, {call_timeout, 5000}, {workers, 20}],
   80:                       [{path_prefix, "/auth/"}, {http_opts, []}, {server, ?AUTH_HOST}]},
   81:               HostTypes = [?HOST_TYPE, <<"another host type">>],
   82:               mongoose_wpool:start_configured_pools([Pool], HostTypes),
   83:               mongoose_wpool_http:init(),
   84:               ejabberd_auth_http:start(?HOST_TYPE)
   85:       end),
   86:     Config.
   87: 
   88: end_per_suite(Config) ->
   89:     ejabberd_auth_http:stop(?HOST_TYPE),
   90:     ok = mim_ct_rest:stop(),
   91:     unset_opts(),
   92:     Config.
   93: 
   94: init_per_group(cert_auth, Config) ->
   95:     Root = small_path_helper:repo_dir(Config),
   96:     SslDir = filename:join(Root, "tools/ssl"),
   97:     try
   98:         {ok, Cert1} = file:read_file(filename:join(SslDir, "mongooseim/cert.pem")),
   99:         {ok, Cert2} = file:read_file(filename:join(SslDir,  "ca/cacert.pem")),
  100:         [{'Certificate', DerBin, not_encrypted} | _] = public_key:pem_decode(Cert2),
  101:         [{der_cert, DerBin}, {pem_cert1, Cert1}, {pem_cert2, Cert2} | Config]
  102:     catch
  103:         _:E ->
  104:             {skip, {E, SslDir, element(2, file:get_cwd())}}
  105:     end;
  106: init_per_group(GroupName, Config) ->
  107:     Config2 = lists:keystore(scram_group, 1, Config,
  108:                              {scram_group, GroupName == auth_requests_scram}),
  109:     set_opts(Config2),
  110:     mim_ct_rest:register(<<"alice">>, ?DOMAIN, do_scram(<<"makota">>, Config2)),
  111:     mim_ct_rest:register(<<"bob">>, ?DOMAIN, do_scram(<<"niema5klepki">>, Config2)),
  112:     Config2.
  113: 
  114: end_per_group(cert_auth, Config) ->
  115:     Config;
  116: end_per_group(_GroupName, Config) ->
  117:     mim_ct_rest:remove_user(<<"alice">>, ?DOMAIN),
  118:     mim_ct_rest:remove_user(<<"bob">>, ?DOMAIN),
  119:     Config.
  120: 
  121: init_per_testcase(remove_user, Config) ->
  122:     mim_ct_rest:register(<<"toremove1">>, ?DOMAIN, do_scram(<<"pass">>, Config)),
  123:     mim_ct_rest:register(<<"toremove2">>, ?DOMAIN, do_scram(<<"pass">>, Config)),
  124:     Config;
  125: init_per_testcase(cert_auth_fail, Config) ->
  126:     Cert = proplists:get_value(pem_cert1, Config),
  127:     mim_ct_rest:register(<<"cert_user">>, ?DOMAIN, Cert),
  128:     Config;
  129: init_per_testcase(cert_auth_success, Config) ->
  130:     Cert1 = proplists:get_value(pem_cert1, Config),
  131:     Cert2 = proplists:get_value(pem_cert2, Config),
  132:     SeveralCerts = <<Cert1/bitstring, Cert2/bitstring>>,
  133:     mim_ct_rest:register(<<"cert_user">>, ?DOMAIN, SeveralCerts),
  134:     Config;
  135: init_per_testcase(_CaseName, Config) ->
  136:     Config.
  137: 
  138: end_per_testcase(try_register, Config) ->
  139:     mim_ct_rest:remove_user(<<"nonexistent">>, ?DOMAIN),
  140:     Config;
  141: end_per_testcase(remove_user, Config) ->
  142:     mim_ct_rest:remove_user(<<"toremove1">>, ?DOMAIN),
  143:     mim_ct_rest:remove_user(<<"toremove2">>, ?DOMAIN),
  144:     Config;
  145: end_per_testcase(cert_auth_fail, Config) ->
  146:     mim_ct_rest:remove_user(<<"cert_user">>, ?DOMAIN),
  147:     Config;
  148: end_per_testcase(cert_auth_success, Config) ->
  149:     mim_ct_rest:remove_user(<<"cert_user">>, ?DOMAIN),
  150:     Config;
  151: end_per_testcase(_CaseName, Config) ->
  152:     Config.
  153: 
  154: %%--------------------------------------------------------------------
  155: %% Authentication tests
  156: %%--------------------------------------------------------------------
  157: 
  158: check_password(_Config) ->
  159:     true = ejabberd_auth_http:check_password(?HOST_TYPE, <<"alice">>,
  160:                                              ?DOMAIN, <<"makota">>),
  161:     false = ejabberd_auth_http:check_password(?HOST_TYPE, <<"alice">>,
  162:                                               ?DOMAIN, <<"niemakota">>),
  163:     false = ejabberd_auth_http:check_password(?HOST_TYPE, <<"kate">>,
  164:                                               ?DOMAIN, <<"mapsa">>).
  165: 
  166: set_password(_Config) ->
  167:     ok = ejabberd_auth_http:set_password(?HOST_TYPE, <<"alice">>,
  168:                                          ?DOMAIN, <<"mialakota">>),
  169:     true = ejabberd_auth_http:check_password(?HOST_TYPE, <<"alice">>,
  170:                                              ?DOMAIN, <<"mialakota">>),
  171:     ok = ejabberd_auth_http:set_password(?HOST_TYPE, <<"alice">>,
  172:                                          ?DOMAIN, <<"makota">>).
  173: 
  174: try_register(_Config) ->
  175:     ok = ejabberd_auth_http:try_register(?HOST_TYPE, <<"nonexistent">>,
  176:                                          ?DOMAIN, <<"newpass">>),
  177:     true = ejabberd_auth_http:check_password(?HOST_TYPE, <<"nonexistent">>,
  178:                                              ?DOMAIN, <<"newpass">>),
  179:     {error, exists} = ejabberd_auth_http:try_register(?HOST_TYPE, <<"nonexistent">>,
  180:                                                       ?DOMAIN, <<"anypass">>).
  181: 
  182: % get_password + get_password_s
  183: get_password(_Config) ->
  184:     case mongoose_scram:enabled(?HOST_TYPE) of
  185:         false ->
  186:             <<"makota">> = ejabberd_auth_http:get_password(?HOST_TYPE, <<"alice">>, ?DOMAIN),
  187:             <<"makota">> = ejabberd_auth_http:get_password_s(?HOST_TYPE, <<"alice">>, ?DOMAIN);
  188:         true ->
  189:             % map with SCRAM data
  190:             true = is_map(ejabberd_auth_http:get_password(?HOST_TYPE, <<"alice">>, ?DOMAIN)),
  191:             <<>> = ejabberd_auth_http:get_password_s(?HOST_TYPE, <<"alice">>, ?DOMAIN)
  192:     end,
  193:     false = ejabberd_auth_http:get_password(?HOST_TYPE, <<"anakin">>, ?DOMAIN),
  194:     <<>> = ejabberd_auth_http:get_password_s(?HOST_TYPE, <<"anakin">>, ?DOMAIN).
  195: 
  196: does_user_exist(_Config) ->
  197:     true = ejabberd_auth_http:does_user_exist(?HOST_TYPE, <<"alice">>, ?DOMAIN),
  198:     false = ejabberd_auth_http:does_user_exist(?HOST_TYPE, <<"madhatter">>, ?DOMAIN).
  199: 
  200: % remove_user/2
  201: remove_user(_Config) ->
  202:     true = ejabberd_auth_http:does_user_exist(?HOST_TYPE, <<"toremove1">>, ?DOMAIN),
  203:     ok = ejabberd_auth_http:remove_user(?HOST_TYPE, <<"toremove1">>, ?DOMAIN),
  204:     false = ejabberd_auth_http:does_user_exist(?HOST_TYPE, <<"toremove1">>, ?DOMAIN).
  205: 
  206: supported_sasl_mechanisms(Config) ->
  207:     Modules = [cyrsasl_plain, cyrsasl_digest, cyrsasl_external,
  208:                cyrsasl_scram_sha1, cyrsasl_scram_sha224, cyrsasl_scram_sha256,
  209:                cyrsasl_scram_sha384, cyrsasl_scram_sha512],
  210:     DigestSupported = case lists:keyfind(scram_group, 1, Config) of
  211:                           {_, true} -> false;
  212:                           _ -> true
  213:                       end,
  214:     [true, DigestSupported, false, true, true, true, true, true] =
  215:         [ejabberd_auth_http:supports_sasl_module(?HOST_TYPE, Mod) || Mod <- Modules].
  216: 
  217: cert_auth_fail(Config) ->
  218:     Creds = creds_with_cert(Config, <<"cert_user">>),
  219:     {error, not_authorized} = ejabberd_auth_http:authorize(Creds).
  220: 
  221: cert_auth_success(Config) ->
  222:     Creds = creds_with_cert(Config, <<"cert_user">>),
  223:     {ok, _} = ejabberd_auth_http:authorize(Creds).
  224: 
  225: cert_auth_nonexistent(Config) ->
  226:     Creds = creds_with_cert(Config, <<"nonexistent">>),
  227:     {error, not_authorized} = ejabberd_auth_http:authorize(Creds).
  228: 
  229: %%--------------------------------------------------------------------
  230: %% Helpers
  231: %%--------------------------------------------------------------------
  232: creds_with_cert(Config, Username) ->
  233:     Cert = proplists:get_value(der_cert, Config),
  234:     NewCreds = mongoose_credentials:new(?DOMAIN, ?HOST_TYPE),
  235:     mongoose_credentials:extend(NewCreds, [{der_cert, Cert},
  236:                                            {username, Username}]).
  237: 
  238: set_opts(Config) ->
  239:     PasswordFormat = case lists:keyfind(scram_group, 1, Config) of
  240:                          {_, false} -> plain;
  241:                          _ -> scram
  242:                      end,
  243:     HttpOpts = #{basic_auth => ?BASIC_AUTH},
  244:     mongoose_config:set_opt({auth, ?HOST_TYPE}, #{methods => [http],
  245:                                                   password => #{format => PasswordFormat,
  246:                                                                 scram_iterations => 10},
  247:                                                   http => HttpOpts}).
  248: 
  249: unset_opts() ->
  250:     mongoose_config:unset_opt({auth, ?HOST_TYPE}).
  251: 
  252: do_scram(Pass, Config) ->
  253:     case lists:keyfind(scram_group, 1, Config) of
  254:         {_, true} ->
  255:             Iterations =  mongoose_scram:iterations(?HOST_TYPE),
  256:             Scram = mongoose_scram:password_to_scram(?HOST_TYPE, Pass, Iterations),
  257:             mongoose_scram:serialize(Scram);
  258:         _ ->
  259:             Pass
  260:     end.