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 = #{type => http, scope => host, tag => auth,
   79:                        opts => #{strategy => random_worker, call_timeout => 5000, workers => 20},
   80:                        conn_opts => #{path_prefix => "/auth/", http_opts => [],
   81:                                       server => ?AUTH_HOST, request_timeout => 2000}},
   82:               HostTypes = [?HOST_TYPE, <<"another host type">>],
   83:               mongoose_wpool:start_configured_pools([Pool], HostTypes),
   84:               mongoose_wpool_http:init(),
   85:               ejabberd_auth_http:start(?HOST_TYPE)
   86:       end),
   87:     Config.
   88: 
   89: end_per_suite(Config) ->
   90:     ejabberd_auth_http:stop(?HOST_TYPE),
   91:     ok = mim_ct_rest:stop(),
   92:     unset_opts(),
   93:     Config.
   94: 
   95: init_per_group(cert_auth, Config) ->
   96:     Root = small_path_helper:repo_dir(Config),
   97:     SslDir = filename:join(Root, "tools/ssl"),
   98:     try
   99:         {ok, Cert1} = file:read_file(filename:join(SslDir, "mongooseim/cert.pem")),
  100:         {ok, Cert2} = file:read_file(filename:join(SslDir,  "ca/cacert.pem")),
  101:         [{'Certificate', DerBin, not_encrypted} | _] = public_key:pem_decode(Cert2),
  102:         [{der_cert, DerBin}, {pem_cert1, Cert1}, {pem_cert2, Cert2} | Config]
  103:     catch
  104:         _:E ->
  105:             {skip, {E, SslDir, element(2, file:get_cwd())}}
  106:     end;
  107: init_per_group(GroupName, Config) ->
  108:     Config2 = lists:keystore(scram_group, 1, Config,
  109:                              {scram_group, GroupName == auth_requests_scram}),
  110:     set_opts(Config2),
  111:     mim_ct_rest:register(<<"alice">>, ?DOMAIN, do_scram(<<"makota">>, Config2)),
  112:     mim_ct_rest:register(<<"bob">>, ?DOMAIN, do_scram(<<"niema5klepki">>, Config2)),
  113:     Config2.
  114: 
  115: end_per_group(cert_auth, Config) ->
  116:     Config;
  117: end_per_group(_GroupName, Config) ->
  118:     mim_ct_rest:remove_user(<<"alice">>, ?DOMAIN),
  119:     mim_ct_rest:remove_user(<<"bob">>, ?DOMAIN),
  120:     Config.
  121: 
  122: init_per_testcase(remove_user, Config) ->
  123:     mim_ct_rest:register(<<"toremove1">>, ?DOMAIN, do_scram(<<"pass">>, Config)),
  124:     mim_ct_rest:register(<<"toremove2">>, ?DOMAIN, do_scram(<<"pass">>, Config)),
  125:     Config;
  126: init_per_testcase(cert_auth_fail, Config) ->
  127:     Cert = proplists:get_value(pem_cert1, Config),
  128:     mim_ct_rest:register(<<"cert_user">>, ?DOMAIN, Cert),
  129:     Config;
  130: init_per_testcase(cert_auth_success, Config) ->
  131:     Cert1 = proplists:get_value(pem_cert1, Config),
  132:     Cert2 = proplists:get_value(pem_cert2, Config),
  133:     SeveralCerts = <<Cert1/bitstring, Cert2/bitstring>>,
  134:     mim_ct_rest:register(<<"cert_user">>, ?DOMAIN, SeveralCerts),
  135:     Config;
  136: init_per_testcase(_CaseName, Config) ->
  137:     Config.
  138: 
  139: end_per_testcase(try_register, Config) ->
  140:     mim_ct_rest:remove_user(<<"nonexistent">>, ?DOMAIN),
  141:     Config;
  142: end_per_testcase(remove_user, Config) ->
  143:     mim_ct_rest:remove_user(<<"toremove1">>, ?DOMAIN),
  144:     mim_ct_rest:remove_user(<<"toremove2">>, ?DOMAIN),
  145:     Config;
  146: end_per_testcase(cert_auth_fail, Config) ->
  147:     mim_ct_rest:remove_user(<<"cert_user">>, ?DOMAIN),
  148:     Config;
  149: end_per_testcase(cert_auth_success, Config) ->
  150:     mim_ct_rest:remove_user(<<"cert_user">>, ?DOMAIN),
  151:     Config;
  152: end_per_testcase(_CaseName, Config) ->
  153:     Config.
  154: 
  155: %%--------------------------------------------------------------------
  156: %% Authentication tests
  157: %%--------------------------------------------------------------------
  158: 
  159: check_password(_Config) ->
  160:     true = ejabberd_auth_http:check_password(?HOST_TYPE, <<"alice">>,
  161:                                              ?DOMAIN, <<"makota">>),
  162:     false = ejabberd_auth_http:check_password(?HOST_TYPE, <<"alice">>,
  163:                                               ?DOMAIN, <<"niemakota">>),
  164:     false = ejabberd_auth_http:check_password(?HOST_TYPE, <<"kate">>,
  165:                                               ?DOMAIN, <<"mapsa">>).
  166: 
  167: set_password(_Config) ->
  168:     ok = ejabberd_auth_http:set_password(?HOST_TYPE, <<"alice">>,
  169:                                          ?DOMAIN, <<"mialakota">>),
  170:     true = ejabberd_auth_http:check_password(?HOST_TYPE, <<"alice">>,
  171:                                              ?DOMAIN, <<"mialakota">>),
  172:     ok = ejabberd_auth_http:set_password(?HOST_TYPE, <<"alice">>,
  173:                                          ?DOMAIN, <<"makota">>).
  174: 
  175: try_register(_Config) ->
  176:     ok = ejabberd_auth_http:try_register(?HOST_TYPE, <<"nonexistent">>,
  177:                                          ?DOMAIN, <<"newpass">>),
  178:     true = ejabberd_auth_http:check_password(?HOST_TYPE, <<"nonexistent">>,
  179:                                              ?DOMAIN, <<"newpass">>),
  180:     {error, exists} = ejabberd_auth_http:try_register(?HOST_TYPE, <<"nonexistent">>,
  181:                                                       ?DOMAIN, <<"anypass">>).
  182: 
  183: % get_password + get_password_s
  184: get_password(_Config) ->
  185:     case mongoose_scram:enabled(?HOST_TYPE) of
  186:         false ->
  187:             <<"makota">> = ejabberd_auth_http:get_password(?HOST_TYPE, <<"alice">>, ?DOMAIN),
  188:             <<"makota">> = ejabberd_auth_http:get_password_s(?HOST_TYPE, <<"alice">>, ?DOMAIN);
  189:         true ->
  190:             % map with SCRAM data
  191:             true = is_map(ejabberd_auth_http:get_password(?HOST_TYPE, <<"alice">>, ?DOMAIN)),
  192:             <<>> = ejabberd_auth_http:get_password_s(?HOST_TYPE, <<"alice">>, ?DOMAIN)
  193:     end,
  194:     false = ejabberd_auth_http:get_password(?HOST_TYPE, <<"anakin">>, ?DOMAIN),
  195:     <<>> = ejabberd_auth_http:get_password_s(?HOST_TYPE, <<"anakin">>, ?DOMAIN).
  196: 
  197: does_user_exist(_Config) ->
  198:     true = ejabberd_auth_http:does_user_exist(?HOST_TYPE, <<"alice">>, ?DOMAIN),
  199:     false = ejabberd_auth_http:does_user_exist(?HOST_TYPE, <<"madhatter">>, ?DOMAIN).
  200: 
  201: % remove_user/2
  202: remove_user(_Config) ->
  203:     true = ejabberd_auth_http:does_user_exist(?HOST_TYPE, <<"toremove1">>, ?DOMAIN),
  204:     ok = ejabberd_auth_http:remove_user(?HOST_TYPE, <<"toremove1">>, ?DOMAIN),
  205:     false = ejabberd_auth_http:does_user_exist(?HOST_TYPE, <<"toremove1">>, ?DOMAIN).
  206: 
  207: supported_sasl_mechanisms(Config) ->
  208:     Modules = [cyrsasl_plain, cyrsasl_digest, cyrsasl_external,
  209:                cyrsasl_scram_sha1, cyrsasl_scram_sha224, cyrsasl_scram_sha256,
  210:                cyrsasl_scram_sha384, cyrsasl_scram_sha512],
  211:     DigestSupported = case lists:keyfind(scram_group, 1, Config) of
  212:                           {_, true} -> false;
  213:                           _ -> true
  214:                       end,
  215:     [true, DigestSupported, false, true, true, true, true, true] =
  216:         [ejabberd_auth_http:supports_sasl_module(?HOST_TYPE, Mod) || Mod <- Modules].
  217: 
  218: cert_auth_fail(Config) ->
  219:     Creds = creds_with_cert(Config, <<"cert_user">>),
  220:     {error, not_authorized} = ejabberd_auth_http:authorize(Creds).
  221: 
  222: cert_auth_success(Config) ->
  223:     Creds = creds_with_cert(Config, <<"cert_user">>),
  224:     {ok, _} = ejabberd_auth_http:authorize(Creds).
  225: 
  226: cert_auth_nonexistent(Config) ->
  227:     Creds = creds_with_cert(Config, <<"nonexistent">>),
  228:     {error, not_authorized} = ejabberd_auth_http:authorize(Creds).
  229: 
  230: %%--------------------------------------------------------------------
  231: %% Helpers
  232: %%--------------------------------------------------------------------
  233: creds_with_cert(Config, Username) ->
  234:     Cert = proplists:get_value(der_cert, Config),
  235:     NewCreds = mongoose_credentials:new(?DOMAIN, ?HOST_TYPE, #{}),
  236:     mongoose_credentials:extend(NewCreds, [{der_cert, Cert},
  237:                                            {username, Username}]).
  238: 
  239: set_opts(Config) ->
  240:     PasswordFormat = case lists:keyfind(scram_group, 1, Config) of
  241:                          {_, false} -> plain;
  242:                          _ -> scram
  243:                      end,
  244:     HttpOpts = #{basic_auth => ?BASIC_AUTH},
  245:     mongoose_config:set_opt({auth, ?HOST_TYPE}, #{methods => [http],
  246:                                                   password => #{format => PasswordFormat,
  247:                                                                 scram_iterations => 10},
  248:                                                   http => HttpOpts}).
  249: 
  250: unset_opts() ->
  251:     mongoose_config:unset_opt({auth, ?HOST_TYPE}).
  252: 
  253: do_scram(Pass, Config) ->
  254:     case lists:keyfind(scram_group, 1, Config) of
  255:         {_, true} ->
  256:             Iterations =  mongoose_scram:iterations(?HOST_TYPE),
  257:             Scram = mongoose_scram:password_to_scram(?HOST_TYPE, Pass, Iterations),
  258:             mongoose_scram:serialize(Scram);
  259:         _ ->
  260:             Pass
  261:     end.