emqx_authentication_SUITE.erl 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
  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. -module(emqx_authentication_SUITE).
  17. -behaviour(hocon_schema).
  18. -behaviour(emqx_authentication).
  19. -compile(export_all).
  20. -compile(nowarn_export_all).
  21. -include_lib("emqx/include/emqx_hooks.hrl").
  22. -include_lib("common_test/include/ct.hrl").
  23. -include_lib("eunit/include/eunit.hrl").
  24. -include("emqx_authentication.hrl").
  25. -define(AUTHN, emqx_authentication).
  26. -define(config(KEY),
  27. (fun() ->
  28. {KEY, _V_} = lists:keyfind(KEY, 1, Config),
  29. _V_
  30. end)()
  31. ).
  32. -define(CONF_ROOT, ?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM).
  33. -define(NOT_SUPERUSER, #{is_superuser => false}).
  34. -define(assertAuthSuccessForUser(User),
  35. ?assertMatch(
  36. {ok, _},
  37. emqx_access_control:authenticate(ClientInfo#{username => atom_to_binary(User)})
  38. )
  39. ).
  40. -define(assertAuthFailureForUser(User),
  41. ?assertMatch(
  42. {error, _},
  43. emqx_access_control:authenticate(ClientInfo#{username => atom_to_binary(User)})
  44. )
  45. ).
  46. %%------------------------------------------------------------------------------
  47. %% Callbacks
  48. %%------------------------------------------------------------------------------
  49. create(_AuthenticatorID, _Config) ->
  50. {ok, #{mark => 1}}.
  51. update(_Config, _State) ->
  52. {ok, #{mark => 2}}.
  53. authenticate(#{username := <<"good">>}, _State) ->
  54. {ok, #{is_superuser => true}};
  55. authenticate(#{username := <<"ignore">>}, _State) ->
  56. ignore;
  57. authenticate(#{username := <<"emqx_authn_ignore_for_hook_good">>}, _State) ->
  58. ignore;
  59. authenticate(#{username := <<"emqx_authn_ignore_for_hook_bad">>}, _State) ->
  60. ignore;
  61. authenticate(#{username := _}, _State) ->
  62. {error, bad_username_or_password}.
  63. hook_authenticate(#{username := <<"hook_user_good">>}, _AuthResult) ->
  64. {ok, {ok, ?NOT_SUPERUSER}};
  65. hook_authenticate(#{username := <<"hook_user_bad">>}, _AuthResult) ->
  66. {ok, {error, invalid_username}};
  67. hook_authenticate(#{username := <<"hook_user_finally_good">>}, _AuthResult) ->
  68. {stop, {ok, ?NOT_SUPERUSER}};
  69. hook_authenticate(#{username := <<"hook_user_finally_bad">>}, _AuthResult) ->
  70. {stop, {error, invalid_username}};
  71. hook_authenticate(#{username := <<"emqx_authn_ignore_for_hook_good">>}, _AuthResult) ->
  72. {ok, {ok, ?NOT_SUPERUSER}};
  73. hook_authenticate(#{username := <<"emqx_authn_ignore_for_hook_bad">>}, _AuthResult) ->
  74. {stop, {error, invalid_username}};
  75. hook_authenticate(_ClientId, AuthResult) ->
  76. {ok, AuthResult}.
  77. destroy(_State) ->
  78. ok.
  79. all() ->
  80. emqx_common_test_helpers:all(?MODULE).
  81. init_per_suite(Config) ->
  82. LogLevel = emqx_logger:get_primary_log_level(),
  83. ok = emqx_logger:set_log_level(debug),
  84. application:set_env(ekka, strict_mode, true),
  85. emqx_config:erase_all(),
  86. emqx_common_test_helpers:stop_apps([]),
  87. emqx_common_test_helpers:boot_modules(all),
  88. emqx_common_test_helpers:start_apps([]),
  89. [{log_level, LogLevel} | Config].
  90. end_per_suite(Config) ->
  91. emqx_common_test_helpers:stop_apps([]),
  92. LogLevel = ?config(log_level),
  93. emqx_logger:set_log_level(LogLevel),
  94. ok.
  95. init_per_testcase(Case, Config) ->
  96. ?MODULE:Case({'init', Config}).
  97. end_per_testcase(Case, Config) ->
  98. _ = ?MODULE:Case({'end', Config}),
  99. ok.
  100. %%=================================================================================
  101. %% Testcases
  102. %%=================================================================================
  103. t_chain({'init', Config}) ->
  104. Config;
  105. t_chain(Config) when is_list(Config) ->
  106. % CRUD of authentication chain
  107. ChainName = 'test',
  108. ?assertMatch({ok, []}, ?AUTHN:list_chains()),
  109. ?assertMatch({ok, []}, ?AUTHN:list_chain_names()),
  110. %% to create a chain we need create an authenticator
  111. AuthenticatorConfig = #{
  112. mechanism => password_based,
  113. backend => built_in_database,
  114. enable => true
  115. },
  116. register_provider({password_based, built_in_database}, ?MODULE),
  117. ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig),
  118. ?assertMatch({ok, #{name := ChainName, authenticators := [_]}}, ?AUTHN:lookup_chain(ChainName)),
  119. ?assertMatch({ok, [#{name := ChainName}]}, ?AUTHN:list_chains()),
  120. ?assertEqual({ok, [ChainName]}, ?AUTHN:list_chain_names()),
  121. ?assertEqual(ok, ?AUTHN:delete_chain(ChainName)),
  122. ?assertMatch({error, {not_found, {chain, ChainName}}}, ?AUTHN:lookup_chain(ChainName)),
  123. ok;
  124. t_chain({'end', _Config}) ->
  125. ?AUTHN:delete_chain(test),
  126. ?AUTHN:deregister_providers([{password_based, built_in_database}]),
  127. ok.
  128. t_authenticator({'init', Config}) ->
  129. [
  130. {"auth1", {password_based, built_in_database}},
  131. {"auth2", {password_based, mysql}}
  132. | Config
  133. ];
  134. t_authenticator(Config) when is_list(Config) ->
  135. ChainName = 'test',
  136. AuthenticatorConfig1 = #{
  137. mechanism => password_based,
  138. backend => built_in_database,
  139. enable => true
  140. },
  141. % Create an authenticator when the provider does not exist
  142. ?assertEqual(
  143. {error, {no_available_provider_for, {password_based, built_in_database}}},
  144. ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)
  145. ),
  146. AuthNType1 = ?config("auth1"),
  147. register_provider(AuthNType1, ?MODULE),
  148. ID1 = <<"password_based:built_in_database">>,
  149. % CRUD of authenticator
  150. ?assertMatch(
  151. {ok, #{id := ID1, state := #{mark := 1}}},
  152. ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)
  153. ),
  154. ?assertMatch({ok, #{id := ID1}}, ?AUTHN:lookup_authenticator(ChainName, ID1)),
  155. ?assertMatch({ok, [#{id := ID1}]}, ?AUTHN:list_authenticators(ChainName)),
  156. ?assertEqual(
  157. {error, {already_exists, {authenticator, ID1}}},
  158. ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)
  159. ),
  160. ?assertMatch(
  161. {ok, #{id := ID1, state := #{mark := 2}}},
  162. ?AUTHN:update_authenticator(ChainName, ID1, AuthenticatorConfig1)
  163. ),
  164. ?assertEqual(ok, ?AUTHN:delete_authenticator(ChainName, ID1)),
  165. ?assertEqual(
  166. {error, {not_found, {chain, test}}},
  167. ?AUTHN:update_authenticator(ChainName, ID1, AuthenticatorConfig1)
  168. ),
  169. ?assertMatch(
  170. {error, {not_found, {chain, ChainName}}},
  171. ?AUTHN:list_authenticators(ChainName)
  172. ),
  173. % Multiple authenticators exist at the same time
  174. AuthNType2 = ?config("auth2"),
  175. register_provider(AuthNType2, ?MODULE),
  176. ID2 = <<"password_based:mysql">>,
  177. AuthenticatorConfig2 = #{
  178. mechanism => 'password_based',
  179. backend => mysql,
  180. enable => true
  181. },
  182. ?assertMatch(
  183. {ok, #{id := ID1}},
  184. ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)
  185. ),
  186. ?assertMatch(
  187. {ok, #{id := ID2}},
  188. ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig2)
  189. ),
  190. % Move authenticator
  191. ?assertMatch({ok, [#{id := ID1}, #{id := ID2}]}, ?AUTHN:list_authenticators(ChainName)),
  192. ?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, ?CMD_MOVE_FRONT)),
  193. ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(ChainName)),
  194. ?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, ?CMD_MOVE_REAR)),
  195. ?assertMatch({ok, [#{id := ID1}, #{id := ID2}]}, ?AUTHN:list_authenticators(ChainName)),
  196. ?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, ?CMD_MOVE_BEFORE(ID1))),
  197. ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(ChainName)),
  198. ?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, ?CMD_MOVE_AFTER(ID1))),
  199. ?assertMatch({ok, [#{id := ID1}, #{id := ID2}]}, ?AUTHN:list_authenticators(ChainName));
  200. t_authenticator({'end', Config}) ->
  201. ?AUTHN:delete_chain(test),
  202. ?AUTHN:deregister_providers([?config("auth1"), ?config("auth2")]),
  203. ok.
  204. t_authenticate({init, Config}) ->
  205. [
  206. {listener_id, 'tcp:default'},
  207. {authn_type, {password_based, built_in_database}}
  208. | Config
  209. ];
  210. t_authenticate(Config) when is_list(Config) ->
  211. ListenerID = ?config(listener_id),
  212. AuthNType = ?config(authn_type),
  213. ClientInfo = #{
  214. zone => default,
  215. listener => ListenerID,
  216. protocol => mqtt,
  217. username => <<"good">>,
  218. password => <<"any">>
  219. },
  220. ?assertEqual({ok, #{is_superuser => false}}, emqx_access_control:authenticate(ClientInfo)),
  221. register_provider(AuthNType, ?MODULE),
  222. AuthenticatorConfig = #{
  223. mechanism => password_based,
  224. backend => built_in_database,
  225. enable => true
  226. },
  227. ?assertMatch({ok, _}, ?AUTHN:create_authenticator(ListenerID, AuthenticatorConfig)),
  228. ?assertEqual(
  229. {ok, #{is_superuser => true}},
  230. emqx_access_control:authenticate(ClientInfo)
  231. ),
  232. ?assertEqual(
  233. {error, bad_username_or_password},
  234. emqx_access_control:authenticate(ClientInfo#{username => <<"bad">>})
  235. );
  236. t_authenticate({'end', Config}) ->
  237. ?AUTHN:delete_chain(?config(listener_id)),
  238. ?AUTHN:deregister_provider(?config(authn_type)),
  239. ok.
  240. t_update_config({init, Config}) ->
  241. Global = 'mqtt:global',
  242. AuthNType1 = {password_based, built_in_database},
  243. AuthNType2 = {password_based, mysql},
  244. [
  245. {global, Global},
  246. {"auth1", AuthNType1},
  247. {"auth2", AuthNType2}
  248. | Config
  249. ];
  250. t_update_config(Config) when is_list(Config) ->
  251. emqx_config_handler:add_handler([?CONF_ROOT], emqx_authentication_config),
  252. ok = emqx_config_handler:add_handler(
  253. [listeners, '?', '?', ?CONF_ROOT], emqx_authentication_config
  254. ),
  255. ok = register_provider(?config("auth1"), ?MODULE),
  256. ok = register_provider(?config("auth2"), ?MODULE),
  257. Global = ?config(global),
  258. AuthenticatorConfig1 = #{
  259. mechanism => password_based,
  260. backend => built_in_database,
  261. enable => true
  262. },
  263. AuthenticatorConfig2 = #{
  264. mechanism => password_based,
  265. backend => mysql,
  266. enable => true
  267. },
  268. ID1 = <<"password_based:built_in_database">>,
  269. ID2 = <<"password_based:mysql">>,
  270. ?assertMatch({ok, []}, ?AUTHN:list_chains()),
  271. ?assertMatch(
  272. {ok, _},
  273. update_config([?CONF_ROOT], {create_authenticator, Global, AuthenticatorConfig1})
  274. ),
  275. ?assertMatch(
  276. {ok, #{id := ID1, state := #{mark := 1}}},
  277. ?AUTHN:lookup_authenticator(Global, ID1)
  278. ),
  279. ?assertMatch(
  280. {ok, _},
  281. update_config([?CONF_ROOT], {create_authenticator, Global, AuthenticatorConfig2})
  282. ),
  283. ?assertMatch(
  284. {ok, #{id := ID2, state := #{mark := 1}}},
  285. ?AUTHN:lookup_authenticator(Global, ID2)
  286. ),
  287. ?assertMatch(
  288. {ok, _},
  289. update_config(
  290. [?CONF_ROOT],
  291. {update_authenticator, Global, ID1, AuthenticatorConfig1#{<<"enable">> => false}}
  292. )
  293. ),
  294. ?assertMatch(
  295. {ok, #{id := ID1, state := #{mark := 2}}},
  296. ?AUTHN:lookup_authenticator(Global, ID1)
  297. ),
  298. ?assertMatch(
  299. {ok, _},
  300. update_config([?CONF_ROOT], {move_authenticator, Global, ID2, ?CMD_MOVE_FRONT})
  301. ),
  302. ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(Global)),
  303. [Raw2, Raw1] = emqx:get_raw_config([?CONF_ROOT]),
  304. ?assertMatch({ok, _}, update_config([?CONF_ROOT], [Raw1, Raw2])),
  305. ?assertMatch({ok, [#{id := ID1}, #{id := ID2}]}, ?AUTHN:list_authenticators(Global)),
  306. ?assertMatch({ok, _}, update_config([?CONF_ROOT], {delete_authenticator, Global, ID1})),
  307. ?assertEqual(
  308. {error, {not_found, {authenticator, ID1}}},
  309. ?AUTHN:lookup_authenticator(Global, ID1)
  310. ),
  311. ?assertMatch(
  312. {ok, _},
  313. update_config([?CONF_ROOT], {delete_authenticator, Global, ID2})
  314. ),
  315. ?assertEqual(
  316. {error, {not_found, {chain, Global}}},
  317. ?AUTHN:lookup_authenticator(Global, ID2)
  318. ),
  319. ListenerID = 'tcp:default',
  320. ConfKeyPath = [listeners, tcp, default, ?CONF_ROOT],
  321. ?assertMatch(
  322. {ok, _},
  323. update_config(
  324. ConfKeyPath,
  325. {create_authenticator, ListenerID, AuthenticatorConfig1}
  326. )
  327. ),
  328. ?assertMatch(
  329. {ok, #{id := ID1, state := #{mark := 1}}},
  330. ?AUTHN:lookup_authenticator(ListenerID, ID1)
  331. ),
  332. ?assertMatch(
  333. {ok, _},
  334. update_config(
  335. ConfKeyPath,
  336. {create_authenticator, ListenerID, AuthenticatorConfig2}
  337. )
  338. ),
  339. ?assertMatch(
  340. {ok, #{id := ID2, state := #{mark := 1}}},
  341. ?AUTHN:lookup_authenticator(ListenerID, ID2)
  342. ),
  343. ?assertMatch(
  344. {ok, _},
  345. update_config(
  346. ConfKeyPath,
  347. {update_authenticator, ListenerID, ID1, AuthenticatorConfig1#{<<"enable">> => false}}
  348. )
  349. ),
  350. ?assertMatch(
  351. {ok, #{id := ID1, state := #{mark := 2}}},
  352. ?AUTHN:lookup_authenticator(ListenerID, ID1)
  353. ),
  354. ?assertMatch(
  355. {ok, _},
  356. update_config(ConfKeyPath, {move_authenticator, ListenerID, ID2, ?CMD_MOVE_FRONT})
  357. ),
  358. ?assertMatch(
  359. {ok, [#{id := ID2}, #{id := ID1}]},
  360. ?AUTHN:list_authenticators(ListenerID)
  361. ),
  362. [LRaw2, LRaw1] = emqx:get_raw_config(ConfKeyPath),
  363. ?assertMatch({ok, _}, update_config(ConfKeyPath, [LRaw1, LRaw2])),
  364. ?assertMatch(
  365. {ok, [#{id := ID1}, #{id := ID2}]},
  366. ?AUTHN:list_authenticators(ListenerID)
  367. ),
  368. ?assertMatch(
  369. {ok, _},
  370. update_config(ConfKeyPath, {delete_authenticator, ListenerID, ID1})
  371. ),
  372. ?assertEqual(
  373. {error, {not_found, {authenticator, ID1}}},
  374. ?AUTHN:lookup_authenticator(ListenerID, ID1)
  375. );
  376. t_update_config({'end', Config}) ->
  377. ?AUTHN:delete_chain(?config(global)),
  378. ?AUTHN:deregister_providers([?config("auth1"), ?config("auth2")]),
  379. ok.
  380. t_restart({'init', Config}) ->
  381. Config;
  382. t_restart(Config) when is_list(Config) ->
  383. ?assertEqual({ok, []}, ?AUTHN:list_chain_names()),
  384. %% to create a chain we need create an authenticator
  385. AuthenticatorConfig = #{
  386. mechanism => password_based,
  387. backend => built_in_database,
  388. enable => true
  389. },
  390. register_provider({password_based, built_in_database}, ?MODULE),
  391. ?AUTHN:create_authenticator(test_chain, AuthenticatorConfig),
  392. ?assertEqual({ok, [test_chain]}, ?AUTHN:list_chain_names()),
  393. ok = supervisor:terminate_child(emqx_authentication_sup, ?AUTHN),
  394. {ok, _} = supervisor:restart_child(emqx_authentication_sup, ?AUTHN),
  395. ?assertEqual({ok, [test_chain]}, ?AUTHN:list_chain_names());
  396. t_restart({'end', _Config}) ->
  397. ?AUTHN:delete_chain(test_chain),
  398. ?AUTHN:deregister_providers([{password_based, built_in_database}]),
  399. ok.
  400. t_combine_authn_and_callback({init, Config}) ->
  401. [
  402. {listener_id, 'tcp:default'},
  403. {authn_type, {password_based, built_in_database}}
  404. | Config
  405. ];
  406. t_combine_authn_and_callback(Config) when is_list(Config) ->
  407. ListenerID = ?config(listener_id),
  408. ClientInfo = #{
  409. zone => default,
  410. listener => ListenerID,
  411. protocol => mqtt,
  412. password => <<"any">>
  413. },
  414. %% no emqx_authentication authenticators, anonymous is allowed
  415. ?assertAuthSuccessForUser(bad),
  416. AuthNType = ?config(authn_type),
  417. register_provider(AuthNType, ?MODULE),
  418. AuthenticatorConfig = #{
  419. mechanism => password_based,
  420. backend => built_in_database,
  421. enable => true
  422. },
  423. {ok, _} = ?AUTHN:create_authenticator(ListenerID, AuthenticatorConfig),
  424. %% emqx_authentication alone
  425. ?assertAuthSuccessForUser(good),
  426. ?assertAuthFailureForUser(ignore),
  427. ?assertAuthFailureForUser(bad),
  428. %% add hook with higher priority
  429. ok = hook(?HP_AUTHN + 1),
  430. %% for hook unrelataed users everything is the same
  431. ?assertAuthSuccessForUser(good),
  432. ?assertAuthFailureForUser(ignore),
  433. ?assertAuthFailureForUser(bad),
  434. %% higher-priority hook can permit access with {ok,...},
  435. %% then emqx_authentication overrides the result
  436. ?assertAuthFailureForUser(hook_user_good),
  437. ?assertAuthFailureForUser(hook_user_bad),
  438. %% higher-priority hook can permit and return {stop,...},
  439. %% then emqx_authentication cannot override the result
  440. ?assertAuthSuccessForUser(hook_user_finally_good),
  441. ?assertAuthFailureForUser(hook_user_finally_bad),
  442. ok = unhook(),
  443. %% add hook with lower priority
  444. ok = hook(?HP_AUTHN - 1),
  445. %% for hook unrelataed users
  446. ?assertAuthSuccessForUser(good),
  447. ?assertAuthFailureForUser(bad),
  448. ?assertAuthFailureForUser(ignore),
  449. %% lower-priority hook can overrride emqx_authentication result
  450. %% for ignored users
  451. ?assertAuthSuccessForUser(emqx_authn_ignore_for_hook_good),
  452. ?assertAuthFailureForUser(emqx_authn_ignore_for_hook_bad),
  453. %% lower-priority hook cannot overrride
  454. %% successful/unsuccessful emqx_authentication result
  455. ?assertAuthFailureForUser(hook_user_finally_good),
  456. ?assertAuthFailureForUser(hook_user_finally_bad),
  457. ?assertAuthFailureForUser(hook_user_good),
  458. ?assertAuthFailureForUser(hook_user_bad),
  459. ok = unhook();
  460. t_combine_authn_and_callback({'end', Config}) ->
  461. ?AUTHN:delete_chain(?config(listener_id)),
  462. ?AUTHN:deregister_provider(?config(authn_type)),
  463. ok.
  464. %%=================================================================================
  465. %% Helpers fns
  466. %%=================================================================================
  467. hook(Priority) ->
  468. ok = emqx_hooks:put(
  469. 'client.authenticate', {?MODULE, hook_authenticate, []}, Priority
  470. ).
  471. unhook() ->
  472. ok = emqx_hooks:del('client.authenticate', {?MODULE, hook_authenticate}).
  473. update_config(Path, ConfigRequest) ->
  474. emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}).
  475. certs(Certs) ->
  476. CertsPath = emqx_common_test_helpers:deps_path(emqx, "etc/certs"),
  477. lists:foldl(
  478. fun({Key, Filename}, Acc) ->
  479. {ok, Bin} = file:read_file(filename:join([CertsPath, Filename])),
  480. Acc#{Key => Bin}
  481. end,
  482. #{},
  483. Certs
  484. ).
  485. register_provider(Type, Module) ->
  486. ok = ?AUTHN:register_providers([{Type, Module}]).