1 |
|
%%%---------------------------------------------------------------------- |
2 |
|
%%% File : ejabberd_auth_jwt.erl |
3 |
|
%%% Author : Astro <astro@spaceboyz.net> |
4 |
|
%%% Purpose : Authentification with JSON Web Tokens |
5 |
|
%%% Created : 02 Aug 2016 by Stephan Maka <stephan@spaceboyz.net> |
6 |
|
%%% |
7 |
|
%%% |
8 |
|
%%% MongooseIM, Copyright (C) 2016 CostaDigital |
9 |
|
%%% |
10 |
|
%%% This program is free software; you can redistribute it and/or |
11 |
|
%%% modify it under the terms of the GNU General Public License as |
12 |
|
%%% published by the Free Software Foundation; either version 2 of the |
13 |
|
%%% License, or (at your option) any later version. |
14 |
|
%%% |
15 |
|
%%% This program is distributed in the hope that it will be useful, |
16 |
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of |
17 |
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
18 |
|
%%% General Public License for more details. |
19 |
|
%%% |
20 |
|
%%% You should have received a copy of the GNU General Public License |
21 |
|
%%% along with this program; if not, write to the Free Software |
22 |
|
%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
23 |
|
%%% |
24 |
|
%%%---------------------------------------------------------------------- |
25 |
|
|
26 |
|
-module(ejabberd_auth_jwt). |
27 |
|
-author('astro@spaceboyz.net'). |
28 |
|
|
29 |
|
%% External exports |
30 |
|
-behaviour(mongoose_gen_auth). |
31 |
|
|
32 |
|
-export([start/1, |
33 |
|
stop/1, |
34 |
|
config_spec/0, |
35 |
|
authorize/1, |
36 |
|
check_password/4, |
37 |
|
check_password/6, |
38 |
|
does_user_exist/3, |
39 |
|
supports_sasl_module/2, |
40 |
|
supported_features/0 |
41 |
|
]). |
42 |
|
|
43 |
|
%% Config spec callbacks |
44 |
|
-export([process_jwt_secret/1]). |
45 |
|
|
46 |
|
-include("mongoose.hrl"). |
47 |
|
-include("mongoose_config_spec.hrl"). |
48 |
|
|
49 |
|
%%%---------------------------------------------------------------------- |
50 |
|
%%% API |
51 |
|
%%%---------------------------------------------------------------------- |
52 |
|
|
53 |
|
-spec start(HostType :: mongooseim:host_type()) -> ok. |
54 |
|
start(HostType) -> |
55 |
:-( |
JWTSecret = get_jwt_secret(HostType), |
56 |
:-( |
persistent_term:put({?MODULE, HostType, jwt_secret}, JWTSecret), |
57 |
:-( |
ok. |
58 |
|
|
59 |
|
-spec stop(HostType :: mongooseim:host_type()) -> ok. |
60 |
|
stop(_HostType) -> |
61 |
:-( |
persistent_term:erase(jwt_secret), |
62 |
:-( |
ok. |
63 |
|
|
64 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
65 |
|
config_spec() -> |
66 |
84 |
#section{ |
67 |
|
items = #{<<"secret">> => jwt_secret_config_spec(), |
68 |
|
<<"algorithm">> => #option{type = binary, |
69 |
|
validate = {enum, algorithms()}}, |
70 |
|
<<"username_key">> => #option{type = atom, |
71 |
|
validate = non_empty} |
72 |
|
}, |
73 |
|
required = all |
74 |
|
}. |
75 |
|
|
76 |
|
jwt_secret_config_spec() -> |
77 |
84 |
#section{ |
78 |
|
items = #{<<"file">> => #option{type = string, |
79 |
|
validate = filename}, |
80 |
|
<<"env">> => #option{type = string, |
81 |
|
validate = non_empty}, |
82 |
|
<<"value">> => #option{type = binary}}, |
83 |
|
format_items = list, |
84 |
|
process = fun ?MODULE:process_jwt_secret/1 |
85 |
|
}. |
86 |
|
|
87 |
:-( |
process_jwt_secret([V]) -> V. |
88 |
|
|
89 |
|
-spec supports_sasl_module(binary(), cyrsasl:sasl_module()) -> boolean(). |
90 |
:-( |
supports_sasl_module(_, Module) -> Module =:= cyrsasl_plain. |
91 |
|
|
92 |
|
-spec authorize(mongoose_credentials:t()) -> {ok, mongoose_credentials:t()} |
93 |
|
| {error, any()}. |
94 |
|
authorize(Creds) -> |
95 |
:-( |
ejabberd_auth:authorize_with_check_password(?MODULE, Creds). |
96 |
|
|
97 |
|
-spec check_password(HostType :: mongooseim:host_type(), |
98 |
|
LUser :: jid:luser(), |
99 |
|
LServer :: jid:lserver(), |
100 |
|
Password :: binary()) -> boolean(). |
101 |
|
check_password(HostType, LUser, LServer, Password) -> |
102 |
:-( |
Key = case persistent_term:get({?MODULE, HostType, jwt_secret}) of |
103 |
:-( |
Key1 when is_binary(Key1) -> Key1; |
104 |
:-( |
{env, Var} -> list_to_binary(os:getenv(Var)) |
105 |
|
end, |
106 |
:-( |
BinAlg = mongoose_config:get_opt([{auth, HostType}, jwt, algorithm]), |
107 |
:-( |
Alg = binary_to_atom(jid:str_tolower(BinAlg), utf8), |
108 |
:-( |
case jwerl:verify(Password, Alg, Key) of |
109 |
|
{ok, TokenData} -> |
110 |
:-( |
UserKey = mongoose_config:get_opt([{auth,HostType}, jwt, username_key]), |
111 |
:-( |
case maps:find(UserKey, TokenData) of |
112 |
|
{ok, LUser} -> |
113 |
|
%% Login username matches $token_user_key in TokenData |
114 |
:-( |
?LOG_INFO(#{what => jwt_success_auth, |
115 |
|
text => <<"Successfully authenticated with JWT">>, |
116 |
|
user => LUser, server => LServer, |
117 |
:-( |
token => TokenData}), |
118 |
:-( |
true; |
119 |
|
{ok, ExpectedUser} -> |
120 |
:-( |
?LOG_WARNING(#{what => wrong_jwt_user, |
121 |
|
text => <<"JWT contains wrond user">>, |
122 |
|
expected_user => ExpectedUser, |
123 |
:-( |
user => LUser, server => LServer}), |
124 |
:-( |
false; |
125 |
|
error -> |
126 |
:-( |
?LOG_WARNING(#{what => missing_jwt_key, |
127 |
|
text => <<"Missing key {user_key} in JWT data">>, |
128 |
|
user_key => UserKey, token => TokenData, |
129 |
:-( |
user => LUser, server => LServer}), |
130 |
:-( |
false |
131 |
|
end; |
132 |
|
{error, Reason} -> |
133 |
:-( |
?LOG_WARNING(#{what => jwt_verification_failed, |
134 |
|
text => <<"Cannot verify JWT for user">>, |
135 |
|
reason => Reason, |
136 |
:-( |
user => LUser, server => LServer}), |
137 |
:-( |
false |
138 |
|
end. |
139 |
|
|
140 |
|
|
141 |
|
-spec check_password(HostType :: mongooseim:host_type(), |
142 |
|
LUser :: jid:luser(), |
143 |
|
LServer :: jid:lserver(), |
144 |
|
Password :: binary(), |
145 |
|
Digest :: binary(), |
146 |
|
DigestGen :: fun()) -> boolean(). |
147 |
|
check_password(HostType, LUser, LServer, Password, _Digest, _DigestGen) -> |
148 |
:-( |
check_password(HostType, LUser, LServer, Password). |
149 |
|
|
150 |
|
-spec does_user_exist(HostType :: mongooseim:host_type(), |
151 |
|
LUser :: jid:luser(), |
152 |
|
LServer :: jid:lserver()) -> boolean() | {error, atom()}. |
153 |
|
does_user_exist(_HostType, _LUser, _LServer) -> |
154 |
:-( |
true. |
155 |
|
|
156 |
|
-spec supported_features() -> [atom()]. |
157 |
:-( |
supported_features() -> [dynamic_domains]. |
158 |
|
|
159 |
|
%%%---------------------------------------------------------------------- |
160 |
|
%%% Internal helpers |
161 |
|
%%%---------------------------------------------------------------------- |
162 |
|
|
163 |
|
% A direct path to a file is read only once during startup, |
164 |
|
% a path in environment variable is read on every auth request. |
165 |
|
-spec get_jwt_secret(mongooseim:host_type()) -> binary() | {env, string()}. |
166 |
|
get_jwt_secret(HostType) -> |
167 |
:-( |
case mongoose_config:get_opt([{auth, HostType}, jwt, secret]) of |
168 |
|
{value, JWTSecret} -> |
169 |
:-( |
JWTSecret; |
170 |
|
{env, Env} -> |
171 |
:-( |
{env, Env}; |
172 |
|
{file, Path} -> |
173 |
:-( |
{ok, JWTSecret} = file:read_file(Path), |
174 |
:-( |
JWTSecret |
175 |
|
end. |
176 |
|
|
177 |
|
algorithms() -> |
178 |
84 |
[<<"HS256">>, <<"RS256">>, <<"ES256">>, |
179 |
|
<<"HS386">>, <<"RS386">>, <<"ES386">>, |
180 |
|
<<"HS512">>, <<"RS512">>, <<"ES512">>]. |