├── .github └── workflows │ └── run_test_cases.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── TODO ├── etc └── emqx_auth_username.conf ├── include └── emqx_auth_username.hrl ├── priv └── emqx_auth_username.schema ├── rebar.config ├── rebar.config.script ├── src ├── emqx_auth_username.app.src ├── emqx_auth_username.app.src.script ├── emqx_auth_username.erl ├── emqx_auth_username_api.erl └── emqx_auth_username_app.erl └── test └── emqx_auth_username_SUITE.erl /.github/workflows/run_test_cases.yaml: -------------------------------------------------------------------------------- 1 | name: Run test cases 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run_test_cases: 7 | runs-on: ubuntu-latest 8 | 9 | container: 10 | image: erlang:22.1 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: run test cases 15 | run: | 16 | make xref 17 | make eunit 18 | make ct 19 | make cover 20 | - uses: actions/upload-artifact@v1 21 | if: always() 22 | with: 23 | name: logs 24 | path: _build/test/logs 25 | - uses: actions/upload-artifact@v1 26 | with: 27 | name: cover 28 | path: _build/test/cover 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | *.o 4 | *.beam 5 | *.plt 6 | erl_crash.dump 7 | ebin 8 | rel/example_project 9 | .concrete/DEV_MODE 10 | .rebar 11 | .erlang.mk/ 12 | emqx_auth_username.d 13 | data/ 14 | _build/ 15 | .DS_Store 16 | cover/ 17 | ct.coverdata 18 | eunit.coverdata 19 | logs/ 20 | test/ct.cover.spec 21 | rebar.lock 22 | rebar3.crashdump 23 | erlang.mk 24 | .*.swp 25 | .rebar3/ 26 | etc/emqx_auth_username.conf.rendered 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ## shallow clone for speed 2 | 3 | REBAR_GIT_CLONE_OPTIONS += --depth 1 4 | export REBAR_GIT_CLONE_OPTIONS 5 | 6 | REBAR = rebar3 7 | all: compile 8 | 9 | compile: 10 | $(REBAR) compile 11 | 12 | ct: compile 13 | $(REBAR) as test ct -v 14 | 15 | eunit: compile 16 | $(REBAR) as test eunit 17 | 18 | xref: 19 | $(REBAR) xref 20 | 21 | cover: 22 | $(REBAR) cover 23 | 24 | clean: distclean 25 | 26 | distclean: 27 | @rm -rf _build 28 | @rm -f data/app.*.config data/vm.*.args rebar.lock 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | emqx_auth_username 2 | ================== 3 | 4 | EMQX Authentication with Username and Password 5 | 6 | Build 7 | ----- 8 | 9 | ``` 10 | make && make tests 11 | ``` 12 | 13 | Configuration 14 | ------------- 15 | 16 | etc/emqx_auth_username.conf: 17 | 18 | ``` 19 | ## Password hash. 20 | ## 21 | ## Value: plain | md5 | sha | sha256 22 | auth.user.password_hash = sha256 23 | ``` 24 | 25 | [REST API](https://developer.emqx.io/docs/emq/v3/en/rest.html) 26 | ------------ 27 | 28 | List all usernames 29 | ``` 30 | # Request 31 | GET api/v4/auth_username 32 | 33 | # Response 34 | { 35 | "code": 0, 36 | "data": ["username1"] 37 | } 38 | ``` 39 | 40 | Add a username: 41 | ``` 42 | # Request 43 | POST api/v4/auth_username 44 | { 45 | "username": "some_name", 46 | "password": "password" 47 | } 48 | 49 | # Response 50 | { 51 | "code": 0 52 | } 53 | ``` 54 | 55 | Update password for a username: 56 | ``` 57 | # Request 58 | PUT api/v4/auth_username/$NAME 59 | { 60 | "password": "password" 61 | } 62 | 63 | # Response 64 | { 65 | "code", 0 66 | } 67 | ``` 68 | 69 | Lookup a username info: 70 | ``` 71 | # Request 72 | GET api/v4/auth_username/$NAME 73 | 74 | # Response 75 | { 76 | "code": 0, 77 | "data": { 78 | "username": "some_username", 79 | "password": "hashed_password" 80 | } 81 | } 82 | ``` 83 | 84 | Delete a username: 85 | ``` 86 | # Request 87 | DELETE api/v4/auth_username/$NAME 88 | 89 | # Response 90 | { 91 | "code": 0 92 | } 93 | ``` 94 | 95 | Load the Plugin 96 | --------------- 97 | 98 | ``` 99 | ./bin/emqx_ctl plugins load emqx_auth_username 100 | ``` 101 | 102 | License 103 | ------- 104 | 105 | Apache License Version 2.0 106 | 107 | Author 108 | ------ 109 | 110 | EMQX Team. 111 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Upgrade test cases for 3.0 2 | -------------------------------------------------------------------------------- /etc/emqx_auth_username.conf: -------------------------------------------------------------------------------- 1 | ##-------------------------------------------------------------------- 2 | ## Username Authentication Plugin 3 | ##-------------------------------------------------------------------- 4 | 5 | ## Examples: 6 | ##auth.user.1.username = admin 7 | ##auth.user.1.password = public 8 | ##auth.user.2.username = feng@emqtt.io 9 | ##auth.user.2.password = public 10 | ##auth.user.3.username = name~!@#$%^&*()_+ 11 | ##auth.user.3.password = pwsswd~!@#$%^&*()_+ 12 | 13 | ## Password hash. 14 | ## 15 | ## Value: plain | md5 | sha | sha256 16 | auth.user.password_hash = sha256 17 | -------------------------------------------------------------------------------- /include/emqx_auth_username.hrl: -------------------------------------------------------------------------------- 1 | -define(APP, emqx_auth_username). 2 | 3 | -record(auth_metrics, { 4 | success = 'client.auth.success', 5 | failure = 'client.auth.failure', 6 | ignore = 'client.auth.ignore' 7 | }). 8 | 9 | -define(METRICS(Type), tl(tuple_to_list(#Type{}))). 10 | -define(METRICS(Type, K), #Type{}#Type.K). 11 | 12 | -define(AUTH_METRICS, ?METRICS(auth_metrics)). 13 | -define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)). 14 | 15 | -------------------------------------------------------------------------------- /priv/emqx_auth_username.schema: -------------------------------------------------------------------------------- 1 | %%-*- mode: erlang -*- 2 | %% emqx_auth_username config mapping 3 | 4 | {mapping, "auth.user.password_hash", "emqx_auth_username.password_hash", [ 5 | {default, sha256}, 6 | {datatype, {enum, [plain, md5, sha, sha256]}} 7 | ]}. 8 | 9 | {mapping, "auth.user.$id.username", "emqx_auth_username.userlist", [ 10 | {datatype, string} 11 | ]}. 12 | 13 | {mapping, "auth.user.$id.password", "emqx_auth_username.userlist", [ 14 | {datatype, string} 15 | ]}. 16 | 17 | {translation, "emqx_auth_username.userlist", fun(Conf) -> 18 | Userlist = cuttlefish_variable:filter_by_prefix("auth.user", Conf), 19 | lists:foldl( 20 | fun({["auth", "user", Id, "username"], Username}, AccIn) -> 21 | [{Username, cuttlefish:conf_get("auth.user." ++ Id ++ ".password", Conf)} | AccIn]; 22 | (_, AccIn) -> 23 | AccIn 24 | end, [], Userlist) 25 | end}. 26 | 27 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {deps, 2 | [{emqx_passwd, {git, "https://github.com/emqx/emqx-passwd.git", {tag, "v1.1.1"}}}, 3 | {minirest, {git, "https://github.com/emqx/minirest.git", {tag, "0.3.2"}}} 4 | ]}. 5 | 6 | {profiles, 7 | [{test, 8 | [{deps, 9 | [{emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "v1.2.2"}}} 10 | ]} 11 | ]} 12 | ]}. 13 | 14 | {erl_opts, [warn_unused_vars, 15 | warn_shadow_vars, 16 | warn_unused_import, 17 | warn_obsolete_guard, 18 | debug_info, 19 | {parse_transform}]}. 20 | 21 | {xref_checks, [undefined_function_calls, undefined_functions, 22 | locals_not_used, deprecated_function_calls, 23 | warnings_as_errors, deprecated_functions]}. 24 | {cover_enabled, true}. 25 | {cover_opts, [verbose]}. 26 | {cover_export_enabled, true}. 27 | 28 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | %%-*- mode: erlang -*- 2 | 3 | DEPS = case lists:keyfind(deps, 1, CONFIG) of 4 | {_, Deps} -> Deps; 5 | _ -> [] 6 | end, 7 | 8 | ComparingFun = fun 9 | _Fun([C1|R1], [C2|R2]) when is_list(C1), is_list(C2); 10 | is_integer(C1), is_integer(C2) -> C1 < C2 orelse _Fun(R1, R2); 11 | _Fun([C1|R1], [C2|R2]) when is_integer(C1), is_list(C2) -> _Fun(R1, R2); 12 | _Fun([C1|R1], [C2|R2]) when is_list(C1), is_integer(C2) -> true; 13 | _Fun(_, _) -> false 14 | end, 15 | 16 | SortFun = fun(T1, T2) -> 17 | C = fun(T) -> 18 | [case catch list_to_integer(E) of 19 | I when is_integer(I) -> I; 20 | _ -> E 21 | end || E <- re:split(string:sub_string(T, 2), "[.-]", [{return, list}])] 22 | end, 23 | ComparingFun(C(T1), C(T2)) 24 | end, 25 | 26 | VTags = string:tokens(os:cmd("git tag -l \"v*\" --points-at $(git rev-parse $(git describe --abbrev=0 --tags))"), "\n"), 27 | 28 | Tags = case VTags of 29 | [] -> string:tokens(os:cmd("git tag -l \"e*\" --points-at $(git rev-parse $(git describe --abbrev=0 --tags))"), "\n"); 30 | _ -> VTags 31 | end, 32 | 33 | LatestTag = lists:last(lists:sort(SortFun, Tags)), 34 | 35 | Branch = case os:getenv("GITHUB_RUN_ID") of 36 | false -> os:cmd("git branch | grep -e '^*' | cut -d' ' -f 2") -- "\n"; 37 | _ -> re:replace(os:getenv("GITHUB_REF"), "^refs/heads/|^refs/tags/", "", [global, {return ,list}]) 38 | end, 39 | 40 | GitDescribe = case re:run(Branch, "master|^dev/|^hotfix/", [{capture, none}]) of 41 | match -> {branch, Branch}; 42 | _ -> {tag, LatestTag} 43 | end, 44 | 45 | UrlPrefix = "https://github.com/emqx/", 46 | 47 | EMQX_DEP = {emqx, {git, UrlPrefix ++ "emqx", GitDescribe}}, 48 | EMQX_MGMT_DEP = {emqx_management, {git, UrlPrefix ++ "emqx-management", GitDescribe}}, 49 | 50 | NewDeps = [EMQX_DEP, EMQX_MGMT_DEP | DEPS], 51 | 52 | CONFIG1 = lists:keystore(deps, 1, CONFIG, {deps, NewDeps}), 53 | 54 | CONFIG1. 55 | -------------------------------------------------------------------------------- /src/emqx_auth_username.app.src: -------------------------------------------------------------------------------- 1 | {application, emqx_auth_username, 2 | [{description, "EMQX Authentication with Username and Password"}, 3 | {vsn, "git"}, 4 | {modules, []}, 5 | {registered, []}, 6 | {applications, [kernel,stdlib]}, 7 | {mod, {emqx_auth_username_app,[]}}, 8 | {env, []}, 9 | {licenses, ["Apache-2.0"]}, 10 | {maintainers, ["EMQX Team "]}, 11 | {links, [{"Homepage", "https://emqx.io/"}, 12 | {"Github", "https://github.com/emqx/emqx-auth-username"} 13 | ]} 14 | ]}. 15 | -------------------------------------------------------------------------------- /src/emqx_auth_username.app.src.script: -------------------------------------------------------------------------------- 1 | %%-*- mode: erlang -*- 2 | %% .app.src.script 3 | 4 | RemoveLeadingV = 5 | fun(Tag) -> 6 | case re:run(Tag, "^[v|e]?[0-9]\.[0-9]\.([0-9]|(rc|beta|alpha)\.[0-9])", [{capture, none}]) of 7 | nomatch -> 8 | re:replace(Tag, "/", "-", [{return ,list}]); 9 | _ -> 10 | %% if it is a version number prefixed by 'v' or 'e', then remove it 11 | re:replace(Tag, "[v|e]", "", [{return ,list}]) 12 | end 13 | end, 14 | 15 | case os:getenv("EMQX_DEPS_DEFAULT_VSN") of 16 | false -> CONFIG; % env var not defined 17 | [] -> CONFIG; % env var set to empty string 18 | Tag -> 19 | [begin 20 | AppConf0 = lists:keystore(vsn, 1, AppConf, {vsn, RemoveLeadingV(Tag)}), 21 | {application, App, AppConf0} 22 | end || Conf = {application, App, AppConf} <- CONFIG] 23 | end. 24 | 25 | -------------------------------------------------------------------------------- /src/emqx_auth_username.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 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 | 17 | -module(emqx_auth_username). 18 | 19 | -include("emqx_auth_username.hrl"). 20 | -include_lib("emqx/include/emqx.hrl"). 21 | 22 | %% CLI callbacks 23 | -export([cli/1]). 24 | 25 | %% APIs 26 | -export([ add_user/2 27 | , update_password/2 28 | , remove_user/1 29 | , lookup_user/1 30 | , all_users/0 31 | ]). 32 | 33 | -export([unwrap_salt/1]). 34 | 35 | %% Auth callbacks 36 | -export([ init/1 37 | , register_metrics/0 38 | , check/3 39 | , description/0 40 | ]). 41 | 42 | -define(TAB, ?MODULE). 43 | 44 | -record(?TAB, {username, password}). 45 | 46 | %%-------------------------------------------------------------------- 47 | %% CLI 48 | %%-------------------------------------------------------------------- 49 | 50 | cli(["list"]) -> 51 | Usernames = mnesia:dirty_all_keys(?TAB), 52 | [emqx_ctl:print("~s~n", [Username]) || Username <- Usernames]; 53 | 54 | cli(["add", Username, Password]) -> 55 | Ok = add_user(iolist_to_binary(Username), iolist_to_binary(Password)), 56 | emqx_ctl:print("~p~n", [Ok]); 57 | 58 | cli(["update", Username, NewPassword]) -> 59 | Ok = update_password(iolist_to_binary(Username), iolist_to_binary(NewPassword)), 60 | emqx_ctl:print("~p~n", [Ok]); 61 | 62 | cli(["del", Username]) -> 63 | emqx_ctl:print("~p~n", [remove_user(iolist_to_binary(Username))]); 64 | 65 | cli(_) -> 66 | emqx_ctl:usage([{"users list", "List users"}, 67 | {"users add ", "Add User"}, 68 | {"users update ", "Update User"}, 69 | {"users del ", "Delete User"}]). 70 | 71 | %%-------------------------------------------------------------------- 72 | %% API 73 | %%-------------------------------------------------------------------- 74 | 75 | %% @doc Add User 76 | -spec(add_user(binary(), binary()) -> ok | {error, any()}). 77 | add_user(Username, Password) -> 78 | User = #?TAB{username = Username, password = encrypted_data(Password)}, 79 | ret(mnesia:transaction(fun insert_user/1, [User])). 80 | 81 | insert_user(User = #?TAB{username = Username}) -> 82 | case mnesia:read(?TAB, Username) of 83 | [] -> mnesia:write(User); 84 | [_|_] -> mnesia:abort(existed) 85 | end. 86 | 87 | %% @doc Update User 88 | -spec(update_password(binary(), binary()) -> ok | {error, any()}). 89 | update_password(Username, NewPassword) -> 90 | User = #?TAB{username = Username, password = encrypted_data(NewPassword)}, 91 | ret(mnesia:transaction(fun do_update_password/1, [User])). 92 | 93 | do_update_password(User = #?TAB{username = Username}) -> 94 | case mnesia:read(?TAB, Username) of 95 | [_|_] -> mnesia:write(User); 96 | [] -> mnesia:abort(noexisted) 97 | end. 98 | 99 | %% @doc Lookup user by username 100 | -spec(lookup_user(binary()) -> list()). 101 | lookup_user(Username) -> 102 | mnesia:dirty_read(?TAB, Username). 103 | 104 | %% @doc Remove user 105 | -spec(remove_user(binary()) -> ok | {error, any()}). 106 | remove_user(Username) -> 107 | ret(mnesia:transaction(fun mnesia:delete/1, [{?TAB, Username}])). 108 | 109 | ret({atomic, ok}) -> ok; 110 | ret({aborted, Error}) -> {error, Error}. 111 | 112 | %% @doc All usernames 113 | -spec(all_users() -> list()). 114 | all_users() -> mnesia:dirty_all_keys(?TAB). 115 | 116 | unwrap_salt(<<_Salt:4/binary, HashPasswd/binary>>) -> 117 | HashPasswd. 118 | 119 | %%-------------------------------------------------------------------- 120 | %% Auth callbacks 121 | %%-------------------------------------------------------------------- 122 | 123 | init(DefaultUsers) -> 124 | ok = ekka_mnesia:create_table(?TAB, [ 125 | {disc_copies, [node()]}, 126 | {attributes, record_info(fields, ?TAB)}, 127 | {storage_properties, [{ets, [{read_concurrency, true}]}]}]), 128 | ok = lists:foreach(fun add_default_user/1, DefaultUsers), 129 | ok = ekka_mnesia:copy_table(?TAB, disc_copies). 130 | 131 | %% @private 132 | add_default_user({Username, Password}) -> 133 | add_user(iolist_to_binary(Username), iolist_to_binary(Password)). 134 | 135 | -spec(register_metrics() -> ok). 136 | register_metrics() -> 137 | lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS). 138 | 139 | check(#{username := Username, password := Password}, AuthResult, #{hash_type := HashType}) -> 140 | case mnesia:dirty_read(?TAB, Username) of 141 | [] -> emqx_metrics:inc(?AUTH_METRICS(ignore)); 142 | [#?TAB{password = <>}] -> 143 | case Hash =:= hash(Password, Salt, HashType) of 144 | true -> 145 | ok = emqx_metrics:inc(?AUTH_METRICS(success)), 146 | {stop, AuthResult#{auth_result => success, anonymous => false}}; 147 | false -> 148 | ok = emqx_metrics:inc(?AUTH_METRICS(failure)), 149 | {stop, AuthResult#{auth_result => not_authorized, anonymous => false}} 150 | end 151 | end. 152 | 153 | description() -> 154 | "Username password Authentication Module". 155 | 156 | %%-------------------------------------------------------------------- 157 | %% Internal functions 158 | %%-------------------------------------------------------------------- 159 | 160 | encrypted_data(Password) -> 161 | HashType = application:get_env(emqx_auth_username, password_hash, sha256), 162 | SaltBin = salt(), 163 | <>. 164 | 165 | hash(undefined, SaltBin, HashType) -> 166 | hash(<<>>, SaltBin, HashType); 167 | hash(Password, SaltBin, HashType) -> 168 | emqx_passwd:hash(HashType, <>). 169 | 170 | salt() -> 171 | rand:seed(exsplus, erlang:timestamp()), 172 | Salt = rand:uniform(16#ffffffff), <>. 173 | 174 | -------------------------------------------------------------------------------- /src/emqx_auth_username_api.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 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 | 17 | -module(emqx_auth_username_api). 18 | 19 | -include("emqx_auth_username.hrl"). 20 | 21 | -import(proplists, [get_value/2]). 22 | 23 | -import(minirest, [return/0, return/1]). 24 | 25 | -rest_api(#{name => list_username, 26 | method => 'GET', 27 | path => "/auth_username", 28 | func => list, 29 | descr => "List available username in the cluster" 30 | }). 31 | 32 | -rest_api(#{name => lookup_username, 33 | method => 'GET', 34 | path => "/auth_username/:bin:username", 35 | func => lookup, 36 | descr => "Lookup username in the cluster" 37 | }). 38 | 39 | -rest_api(#{name => add_username, 40 | method => 'POST', 41 | path => "/auth_username", 42 | func => add, 43 | descr => "Add username in the cluster" 44 | }). 45 | 46 | -rest_api(#{name => update_username, 47 | method => 'PUT', 48 | path => "/auth_username/:bin:username", 49 | func => update, 50 | descr => "Update username in the cluster" 51 | }). 52 | 53 | -rest_api(#{name => delete_username, 54 | method => 'DELETE', 55 | path => "/auth_username/:bin:username", 56 | func => delete, 57 | descr => "Delete username in the cluster" 58 | }). 59 | 60 | -export([ list/2 61 | , lookup/2 62 | , add/2 63 | , update/2 64 | , delete/2 65 | ]). 66 | 67 | list(_Bindings, _Params) -> 68 | return({ok, emqx_auth_username:all_users()}). 69 | 70 | lookup(#{username := Username}, _Params) -> 71 | return({ok, format(emqx_auth_username:lookup_user(Username))}). 72 | 73 | add(_Bindings, Params) -> 74 | Username = get_value(<<"username">>, Params), 75 | Password = get_value(<<"password">>, Params), 76 | case validate([username, password], [Username, Password]) of 77 | ok -> 78 | case emqx_auth_username:add_user(Username, Password) of 79 | ok -> return(); 80 | Err -> return(Err) 81 | end; 82 | Err -> return(Err) 83 | end. 84 | 85 | update(#{username := Username}, Params) -> 86 | Password = get_value(<<"password">>, Params), 87 | case validate([password], [Password]) of 88 | ok -> 89 | case emqx_auth_username:update_password(Username, Password) of 90 | ok -> return(); 91 | Err -> return(Err) 92 | end; 93 | Err -> return(Err) 94 | end. 95 | 96 | delete(#{username := Username}, _) -> 97 | ok = emqx_auth_username:remove_user(Username), 98 | return(). 99 | 100 | %%------------------------------------------------------------------------------ 101 | %% Interval Funcs 102 | %%------------------------------------------------------------------------------ 103 | 104 | format([{?APP, Username, Password}]) -> 105 | #{username => Username, 106 | password => emqx_auth_username:unwrap_salt(Password)}. 107 | 108 | validate([], []) -> 109 | ok; 110 | validate([K|Keys], [V|Values]) -> 111 | case validation(K, V) of 112 | false -> {error, K}; 113 | true -> validate(Keys, Values) 114 | end. 115 | 116 | validation(username, V) when is_binary(V) 117 | andalso byte_size(V) > 0 -> 118 | true; 119 | validation(password, V) when is_binary(V) 120 | andalso byte_size(V) > 0 -> 121 | true; 122 | validation(_, _) -> 123 | false. 124 | -------------------------------------------------------------------------------- /src/emqx_auth_username_app.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 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 | 17 | -module(emqx_auth_username_app). 18 | 19 | -include("emqx_auth_username.hrl"). 20 | 21 | -behaviour(application). 22 | -behaviour(supervisor). 23 | 24 | -emqx_plugin(auth). 25 | 26 | -export([ start/2 27 | , stop/1 28 | ]). 29 | -export([init/1]). 30 | 31 | start(_Type, _Args) -> 32 | emqx_ctl:register_command(users, {?APP, cli}, []), 33 | ok = emqx_auth_username:register_metrics(), 34 | HashType = application:get_env(?APP, password_hash, sha256), 35 | Params = #{hash_type => HashType}, 36 | emqx:hook('client.authenticate', fun emqx_auth_username:check/3, [Params]), 37 | DefaultUsers = application:get_env(?APP, userlist, []), 38 | ok = emqx_auth_username:init(DefaultUsers), 39 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 40 | 41 | stop(_State) -> 42 | emqx:unhook('client.authenticate', fun emqx_auth_username:check/3), 43 | emqx_ctl:unregister_command(users). 44 | 45 | %%-------------------------------------------------------------------- 46 | 47 | init([]) -> 48 | {ok, { {one_for_all, 1, 10}, []} }. 49 | 50 | -------------------------------------------------------------------------------- /test/emqx_auth_username_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 2 | %% 3 | %% Licensed under the Apache License, Version 2.0 (the "License"); 4 | %% you may not use this file except in compliance with the License. 5 | %% You may obtain a copy of the License at 6 | %% 7 | %% http://www.apache.org/licenses/LICENSE-2.0 8 | %% 9 | %% Unless required by applicable law or agreed to in writing, software 10 | %% distributed under the License is distributed on an "AS IS" BASIS, 11 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | %% See the License for the specific language governing permissions and 13 | %% limitations under the License. 14 | 15 | -module(emqx_auth_username_SUITE). 16 | 17 | -compile(nowarn_export_all). 18 | -compile(export_all). 19 | 20 | -include_lib("emqx/include/emqx.hrl"). 21 | 22 | -include_lib("eunit/include/eunit.hrl"). 23 | -include_lib("common_test/include/ct.hrl"). 24 | 25 | -define(TAB, emqx_auth_username). 26 | 27 | all() -> 28 | emqx_ct:all(?MODULE). 29 | 30 | init_per_suite(Config) -> 31 | emqx_ct_helpers:start_apps([emqx_auth_username], fun set_special_configs/1), 32 | Config. 33 | 34 | end_per_suite(_Config) -> 35 | emqx_ct_helpers:stop_apps([emqx_auth_username]). 36 | 37 | set_special_configs(emqx) -> 38 | application:set_env(emqx, allow_anonymous, true), 39 | application:set_env(emqx, enable_acl_cache, false), 40 | LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]), 41 | application:set_env(emqx, plugins_loaded_file, 42 | emqx_ct_helpers:deps_path(emqx, LoadedPluginPath)); 43 | 44 | set_special_configs(_App) -> 45 | ok. 46 | 47 | %%------------------------------------------------------------------------------ 48 | %% Testcases 49 | %%------------------------------------------------------------------------------ 50 | 51 | t_managing(_Config) -> 52 | clean_all_users(), 53 | 54 | ok = emqx_auth_username:add_user(<<"test_username">>, <<"password">>), 55 | [{?TAB, <<"test_username">>, _HashedPass}] = 56 | emqx_auth_username:lookup_user(<<"test_username">>), 57 | User1 = #{username => <<"test_username">>, 58 | password => <<"password">>, 59 | zone => external}, 60 | 61 | {ok, #{auth_result := success, 62 | anonymous := false}} = emqx_access_control:authenticate(User1), 63 | 64 | ok = emqx_auth_username:remove_user(<<"test_username">>), 65 | {ok, #{auth_result := success, 66 | anonymous := true }} = emqx_access_control:authenticate(User1). 67 | 68 | t_rest_api(_Config) -> 69 | clean_all_users(), 70 | 71 | Username = <<"username">>, 72 | Password = <<"password">>, 73 | Password1 = <<"password1">>, 74 | User = #{username => Username, zone => external}, 75 | 76 | ?assertEqual(return(), 77 | emqx_auth_username_api:add(#{}, rest_params(Username, Password))), 78 | ?assertEqual(return({error, existed}), 79 | emqx_auth_username_api:add(#{}, rest_params(Username, Password))), 80 | ?assertEqual(return([Username]), 81 | emqx_auth_username_api:list(#{}, [])), 82 | 83 | {ok, #{code := 0, data := Data}} = 84 | emqx_auth_username_api:lookup(rest_binding(Username), []), 85 | ?assertEqual(true, match_password(maps:get(username, Data), Password)), 86 | 87 | {ok, _} = emqx_access_control:authenticate(User#{password => Password}), 88 | 89 | ?assertEqual(return(), 90 | emqx_auth_username_api:update(rest_binding(Username), rest_params(Password))), 91 | ?assertEqual(return({error, noexisted}), 92 | emqx_auth_username_api:update(#{username => <<"another_user">>}, rest_params(<<"another_passwd">>))), 93 | 94 | {error, _} = emqx_access_control:authenticate(User#{password => Password1}), 95 | 96 | ?assertEqual(return(), 97 | emqx_auth_username_api:delete(rest_binding(Username), [])), 98 | {ok, #{auth_result := success, 99 | anonymous := true}} = emqx_access_control:authenticate(User#{password => Password}). 100 | 101 | t_cli(_Config) -> 102 | clean_all_users(), 103 | 104 | emqx_auth_username:cli(["add", "username", "password"]), 105 | ?assertEqual(true, match_password(<<"username">>, <<"password">>)), 106 | 107 | emqx_auth_username:cli(["update", "username", "newpassword"]), 108 | ?assertEqual(true, match_password(<<"username">>, <<"newpassword">>)), 109 | 110 | emqx_auth_username:cli(["del", "username"]), 111 | [] = emqx_auth_username:lookup_user(<<"username">>), 112 | emqx_auth_username:cli(["add", "user1", "pass1"]), 113 | emqx_auth_username:cli(["add", "user2", "pass2"]), 114 | UserList = emqx_auth_username:cli(["list"]), 115 | 2 = length(UserList), 116 | emqx_auth_username:cli(usage). 117 | 118 | t_conf_not_override_existed(_) -> 119 | clean_all_users(), 120 | 121 | Username = <<"username">>, 122 | Password = <<"password">>, 123 | NPassword = <<"password1">>, 124 | User = #{username => Username, zone => external}, 125 | 126 | application:stop(emqx_auth_username), 127 | application:set_env(emqx_auth_username, userlist, [{Username, Password}]), 128 | application:ensure_all_started(emqx_auth_username), 129 | 130 | {ok, _} = emqx_access_control:authenticate(User#{password => Password}), 131 | emqx_auth_username:cli(["update", Username, NPassword]), 132 | 133 | {error, _} = emqx_access_control:authenticate(User#{password => Password}), 134 | {ok, _} = emqx_access_control:authenticate(User#{password => NPassword}), 135 | 136 | application:stop(emqx_auth_username), 137 | application:ensure_all_started(emqx_auth_username), 138 | {ok, _} = emqx_access_control:authenticate(User#{password => NPassword}), 139 | 140 | ?assertEqual(return(), 141 | emqx_auth_username_api:update(rest_binding(Username), rest_params(Password))), 142 | application:stop(emqx_auth_username), 143 | application:ensure_all_started(emqx_auth_username), 144 | {ok, _} = emqx_access_control:authenticate(User#{password => Password}). 145 | 146 | %%------------------------------------------------------------------------------ 147 | %% Helpers 148 | %%------------------------------------------------------------------------------ 149 | 150 | clean_all_users() -> 151 | [ mnesia:dirty_delete({emqx_auth_username, Username}) 152 | || Username <- mnesia:dirty_all_keys(emqx_auth_username)]. 153 | 154 | match_password(Username, PlainPassword) -> 155 | HashType = application:get_env(emqx_auth_username, password_hash, sha256), 156 | [{?TAB, Username, <>}] = 157 | emqx_auth_username:lookup_user(Username), 158 | Hash =:= emqx_passwd:hash(HashType, <>). 159 | 160 | rest_params(Passwd) -> 161 | [{<<"password">>, Passwd}]. 162 | 163 | rest_params(Username, Passwd) -> 164 | [{<<"username">>, Username}, 165 | {<<"password">>, Passwd}]. 166 | 167 | rest_binding(Username) -> 168 | #{username => Username}. 169 | 170 | return() -> 171 | {ok, #{code => 0}}. 172 | return({error, Err}) -> 173 | {ok, #{message => Err}}; 174 | return(Data) -> 175 | {ok, #{code => 0, data => Data}}. 176 | 177 | --------------------------------------------------------------------------------