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