./ct_report/coverage/mod_auth_token.COVER.html

1 -module(mod_auth_token).
2
3 -behaviour(gen_mod).
4 -behaviour(mongoose_module_metrics).
5
6 -include("mongoose.hrl").
7 -include("ejabberd_commands.hrl").
8 -include("jlib.hrl").
9 -include("mod_auth_token.hrl").
10 -include("mongoose_config_spec.hrl").
11
12 %% gen_mod callbacks
13 -export([start/2]).
14 -export([stop/1]).
15 -export([supported_features/0]).
16 -export([config_spec/0]).
17
18 %% Hook handlers
19 -export([clean_tokens/3,
20 disco_local_features/1]).
21
22 %% gen_iq_handler handlers
23 -export([process_iq/5]).
24
25 %% Public API
26 -export([authenticate/2,
27 revoke/2,
28 token/3]).
29
30 %% Token serialization
31 -export([deserialize/1,
32 serialize/1]).
33
34 %% Command-line interface
35 -export([revoke_token_command/1]).
36
37 %% Test only!
38 -export([datetime_to_seconds/1,
39 seconds_to_datetime/1]).
40 -export([expiry_datetime/3,
41 get_key_for_host_type/2,
42 token_with_mac/2]).
43
44 -export([config_metrics/1]).
45
46 -export_type([period/0,
47 sequence_no/0,
48 token/0,
49 token_type/0]).
50
51 -ignore_xref([
52 behaviour_info/1, clean_tokens/3, datetime_to_seconds/1, deserialize/1,
53 disco_local_features/1, expiry_datetime/3, get_key_for_host_type/2, process_iq/5,
54 revoke/2, revoke_token_command/1, seconds_to_datetime/1, serialize/1, token/3,
55 token_with_mac/2
56 ]).
57
58 -type error() :: error | {error, any()}.
59 -type period() :: #{value := non_neg_integer(),
60 unit := days | hours | minutes | seconds}.
61 -type sequence_no() :: integer().
62 -type serialized() :: binary().
63 -type token() :: #token{}.
64 -type token_type() :: access | refresh | provision.
65 -type validation_result() :: {ok, module(), jid:user()}
66 | {ok, module(), jid:user(), binary()}
67 | error().
68
69 -define(A2B(A), atom_to_binary(A, utf8)).
70
71 -define(I2B(I), integer_to_binary(I)).
72 -define(B2I(B), binary_to_integer(B)).
73
74 %%
75 %% gen_mod callbacks
76 %%
77
78 -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok.
79 start(HostType, #{iqdisc := IQDisc} = Opts) ->
80
:-(
mod_auth_token_backend:start(HostType, Opts),
81
:-(
ejabberd_hooks:add(hooks(HostType)),
82
:-(
gen_iq_handler:add_iq_handler_for_domain(
83 HostType, ?NS_ESL_TOKEN_AUTH, ejabberd_sm,
84 fun ?MODULE:process_iq/5, #{}, IQDisc),
85
:-(
ejabberd_commands:register_commands(commands()),
86
:-(
ok.
87
88 -spec stop(mongooseim:host_type()) -> ok.
89 stop(HostType) ->
90
:-(
gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_ESL_TOKEN_AUTH, ejabberd_sm),
91
:-(
ejabberd_hooks:delete(hooks(HostType)),
92
:-(
ok.
93
94 hooks(HostType) ->
95
:-(
[{remove_user, HostType, ?MODULE, clean_tokens, 50},
96 {disco_local_features, HostType, ?MODULE, disco_local_features, 90}].
97
98 -spec supported_features() -> [atom()].
99 supported_features() ->
100
:-(
[dynamic_domains].
101
102 -spec config_spec() -> mongoose_config_spec:config_section().
103 config_spec() ->
104 164 #section{
105 items = #{<<"backend">> => #option{type = atom,
106 validate = {module, mod_auth_token}},
107 <<"validity_period">> => validity_periods_spec(),
108 <<"iqdisc">> => mongoose_config_spec:iqdisc()},
109 defaults = #{<<"backend">> => rdbms,
110 <<"iqdisc">> => no_queue},
111 format_items = map
112 }.
113
114 validity_periods_spec() ->
115 164 #section{
116 items = #{<<"access">> => validity_period_spec(),
117 <<"refresh">> => validity_period_spec()},
118 defaults = #{<<"access">> => #{value => 1, unit => hours},
119 <<"refresh">> => #{value => 25, unit => days}},
120 format_items = map,
121 include = always
122 }.
123
124 validity_period_spec() ->
125 328 #section{
126 items = #{<<"value">> => #option{type = integer,
127 validate = non_negative},
128 <<"unit">> => #option{type = atom,
129 validate = {enum, [days, hours, minutes, seconds]}}
130 },
131 required = all,
132 format_items = map
133 }.
134
135 -spec commands() -> [ejabberd_commands:cmd()].
136 commands() ->
137
:-(
[#ejabberd_commands{ name = revoke_token, tags = [tokens],
138 desc = "Revoke REFRESH token",
139 module = ?MODULE, function = revoke_token_command,
140 args = [{owner, binary}], result = {res, restuple} }].
141
142 %%
143 %% Other stuff
144 %%
145
146 -spec serialize(token()) -> serialized().
147
:-(
serialize(#token{mac_signature = undefined} = T) -> error(incomplete_token, [T]);
148
:-(
serialize(#token{token_body = undefined} = T) -> error(incomplete_token, [T]);
149 serialize(#token{token_body = Body, mac_signature = MAC}) ->
150
:-(
<<Body/bytes, (field_separator()), (base16:encode(MAC))/bytes>>.
151
152 %% #token{} contains fields which are:
153 %% - primary - these have to be supplied on token creation,
154 %% - dependent - these are computed based on the primary fields.
155 %% `token_with_mac/2` computes dependent fields and stores them in the record
156 %% based on a record with just the primary fields.
157 -spec token_with_mac(mongooseim:host_type(), token()) -> token().
158 token_with_mac(HostType, #token{mac_signature = undefined, token_body = undefined} = T) ->
159
:-(
Body = join_fields(T),
160
:-(
MAC = keyed_hash(Body, hmac_opts(HostType, T#token.type)),
161
:-(
T#token{token_body = Body, mac_signature = MAC}.
162
163 -spec hmac_opts(mongooseim:host_type(), token_type()) -> [{any(), any()}].
164 hmac_opts(HostType, TokenType) ->
165
:-(
lists:keystore(key, 1, hmac_opts(),
166 {key, get_key_for_host_type(HostType, TokenType)}).
167
168
:-(
field_separator() -> 0.
169
170 join_fields(T) ->
171
:-(
Sep = field_separator(),
172
:-(
#token{type = Type, expiry_datetime = Expiry, user_jid = JID,
173 sequence_no = SeqNo, vcard = VCard} = T,
174
:-(
case {Type, SeqNo} of
175 {access, undefined} ->
176
:-(
<<(?A2B(Type))/bytes, Sep,
177 (jid:to_binary(JID))/bytes, Sep,
178 (?I2B(datetime_to_seconds(Expiry)))/bytes>>;
179 {refresh, _} ->
180
:-(
<<(?A2B(Type))/bytes, Sep,
181 (jid:to_binary(JID))/bytes, Sep,
182 (?I2B(datetime_to_seconds(Expiry)))/bytes, Sep,
183 (?I2B(SeqNo))/bytes>>;
184 {provision, undefined} ->
185
:-(
<<(?A2B(Type))/bytes, Sep,
186 (jid:to_binary(JID))/bytes, Sep,
187 (?I2B(datetime_to_seconds(Expiry)))/bytes, Sep,
188 (exml:to_binary(VCard))/bytes>>
189 end.
190
191 keyed_hash(Data, Opts) ->
192
:-(
Type = proplists:get_value(hmac_type, Opts, sha384),
193
:-(
{key, Key} = lists:keyfind(key, 1, Opts),
194
:-(
crypto:mac(hmac, Type, Key, Data).
195
196 hmac_opts() ->
197
:-(
[].
198
199 -spec deserialize(serialized()) -> token().
200 deserialize(Serialized) when is_binary(Serialized) ->
201
:-(
get_token_as_record(Serialized).
202
203 -spec revoke(mongooseim:host_type(), jid:jid()) -> ok | not_found | error.
204 revoke(HostType, Owner) ->
205
:-(
try
206
:-(
mod_auth_token_backend:revoke(HostType, Owner)
207 catch
208 Class:Reason:Stacktrace ->
209
:-(
?LOG_ERROR(#{what => auth_token_revoke_failed,
210 user => Owner#jid.luser, server => Owner#jid.lserver,
211
:-(
class => Class, reason => Reason, stacktrace => Stacktrace}),
212
:-(
error
213 end.
214
215 -spec authenticate(mongooseim:host_type(), serialized()) -> validation_result().
216 authenticate(HostType, SerializedToken) ->
217
:-(
try
218
:-(
do_authenticate(HostType, SerializedToken)
219 catch
220
:-(
_:_ -> {error, internal_server_error}
221 end.
222
223 do_authenticate(HostType, SerializedToken) ->
224
:-(
#token{user_jid = Owner} = Token = deserialize(SerializedToken),
225
:-(
{Criteria, Result} = validate_token(HostType, Token),
226
:-(
?LOG_INFO(#{what => auth_token_validate,
227 user => Owner#jid.luser, server => Owner#jid.lserver,
228
:-(
criteria => Criteria, result => Result}),
229
:-(
case {Result, Token#token.type} of
230 {ok, access} ->
231
:-(
{ok, mod_auth_token, Owner#jid.luser};
232 {ok, refresh} ->
233
:-(
case token(HostType, Owner, access) of
234 #token{} = T ->
235
:-(
{ok, mod_auth_token, Owner#jid.luser, serialize(T)};
236 {error, R} ->
237
:-(
{error, R}
238 end;
239 {ok, provision} ->
240
:-(
case set_vcard(HostType, Owner, Token#token.vcard) of
241 {error, Reason} ->
242
:-(
?LOG_WARNING(#{what => auth_token_set_vcard_failed,
243 reason => Reason, token_vcard => Token#token.vcard,
244 user => Owner#jid.luser, server => Owner#jid.lserver,
245
:-(
criteria => Criteria, result => Result}),
246
:-(
{ok, mod_auth_token, Owner#jid.luser};
247 ok ->
248
:-(
{ok, mod_auth_token, Owner#jid.luser}
249 end;
250 {error, _} ->
251
:-(
{error, {Owner#jid.luser, [ Criterion
252
:-(
|| {_, false} = Criterion <- Criteria ]}}
253 end.
254
255 set_vcard(HostType, #jid{} = User, #xmlel{} = VCard) ->
256
:-(
mongoose_hooks:set_vcard(HostType, User, VCard).
257
258 validate_token(HostType, Token) ->
259
:-(
Criteria = [{mac_valid, is_mac_valid(HostType, Token)},
260 {not_expired, is_not_expired(Token)},
261 {not_revoked, not is_revoked(Token, HostType)}],
262
:-(
Result = case Criteria of
263
:-(
[{_, true}, {_, true}, {_, true}] -> ok;
264
:-(
_ -> error
265 end,
266
:-(
{Criteria, Result}.
267
268 is_mac_valid(HostType, #token{type = Type, token_body = Body, mac_signature = ReceivedMAC}) ->
269
:-(
ComputedMAC = keyed_hash(Body, hmac_opts(HostType, Type)),
270
:-(
ReceivedMAC =:= ComputedMAC.
271
272 is_not_expired(#token{expiry_datetime = Expiry}) ->
273
:-(
utc_now_as_seconds() < datetime_to_seconds(Expiry).
274
275 is_revoked(#token{type = T}, _) when T =:= access;
276 T =:= provision ->
277
:-(
false;
278 is_revoked(#token{type = refresh, sequence_no = TokenSeqNo} = T, HostType) ->
279
:-(
try
280
:-(
ValidSeqNo = mod_auth_token_backend:get_valid_sequence_number(HostType, T#token.user_jid),
281
:-(
TokenSeqNo < ValidSeqNo
282 catch
283 Class:Reason:Stacktrace ->
284
:-(
?LOG_ERROR(#{what => auth_token_revocation_check_failed,
285 text => <<"Error checking revocation status">>,
286 token_seq_no => TokenSeqNo,
287
:-(
class => Class, reason => Reason, stacktrace => Stacktrace}),
288
:-(
true
289 end.
290
291 -spec process_iq(mongoose_acc:t(), jid:jid(), jid:jid(), jlib:iq(), any()) ->
292 {mongoose_acc:t(), jlib:iq()} | error().
293 process_iq(Acc, From, To, #iq{xmlns = ?NS_ESL_TOKEN_AUTH} = IQ, _Extra) ->
294
:-(
IQResp = process_local_iq(Acc, From, To, IQ),
295
:-(
{Acc, IQResp};
296 process_iq(Acc, _From, _To, #iq{} = IQ, _Extra) ->
297
:-(
{Acc, iq_error(IQ, [mongoose_xmpp_errors:bad_request()])}.
298
299 process_local_iq(Acc, From, _To, IQ) ->
300
:-(
try create_token_response(Acc, From, IQ) of
301
:-(
#iq{} = Response -> Response;
302
:-(
{error, Reason} -> iq_error(IQ, [Reason])
303 catch
304
:-(
_:_ -> iq_error(IQ, [mongoose_xmpp_errors:internal_server_error()])
305 end.
306
307 iq_error(IQ, SubElements) when is_list(SubElements) ->
308
:-(
IQ#iq{type = error, sub_el = SubElements}.
309
310 create_token_response(Acc, From, IQ) ->
311
:-(
HostType = mongoose_acc:host_type(Acc),
312
:-(
case {token(HostType, From, access), token(HostType, From, refresh)} of
313 {#token{} = AccessToken, #token{} = RefreshToken} ->
314
:-(
IQ#iq{type = result,
315 sub_el = [#xmlel{name = <<"items">>,
316 attrs = [{<<"xmlns">>, ?NS_ESL_TOKEN_AUTH}],
317 children = [token_to_xmlel(AccessToken),
318 token_to_xmlel(RefreshToken)]}]};
319
:-(
{_, _} -> {error, mongoose_xmpp_errors:internal_server_error()}
320 end.
321
322 -spec datetime_to_seconds(calendar:datetime()) -> non_neg_integer().
323 datetime_to_seconds(DateTime) ->
324
:-(
calendar:datetime_to_gregorian_seconds(DateTime).
325
326 -spec seconds_to_datetime(non_neg_integer()) -> calendar:datetime().
327 seconds_to_datetime(Seconds) ->
328
:-(
calendar:gregorian_seconds_to_datetime(Seconds).
329
330 utc_now_as_seconds() ->
331
:-(
datetime_to_seconds(calendar:universal_time()).
332
333 -spec token(mongooseim:host_type(), jid:jid(), token_type()) -> token() | error().
334 token(HostType, User, Type) ->
335
:-(
ExpiryTime = expiry_datetime(HostType, Type, utc_now_as_seconds()),
336
:-(
T = #token{type = Type, expiry_datetime = ExpiryTime, user_jid = User},
337
:-(
try
338
:-(
T2 = case Type of
339
:-(
access -> T;
340 refresh ->
341
:-(
ValidSeqNo = mod_auth_token_backend:get_valid_sequence_number(HostType, User),
342
:-(
T#token{sequence_no = ValidSeqNo}
343 end,
344
:-(
token_with_mac(HostType, T2)
345 catch
346 Class:Reason:Stacktrace ->
347
:-(
?LOG_ERROR(#{what => auth_token_revocation_check_failed,
348 text => <<"Error creating token sequence number">>,
349 token_type => Type, expiry_datetime => ExpiryTime,
350 user => User#jid.luser, server => User#jid.lserver,
351
:-(
class => Class, reason => Reason, stacktrace => Stacktrace}),
352
:-(
{error, {Class, Reason}}
353 end.
354
355 -spec expiry_datetime(mongooseim:host_type(), token_type(), non_neg_integer()) ->
356 calendar:datetime().
357 expiry_datetime(HostType, Type, UTCSeconds) ->
358
:-(
#{value := Value, unit := Unit} = get_validity_period(HostType, Type),
359
:-(
seconds_to_datetime(UTCSeconds + period_to_seconds(Value, Unit)).
360
361 -spec get_validity_period(mongooseim:host_type(), token_type()) -> period().
362 get_validity_period(HostType, Type) ->
363
:-(
gen_mod:get_module_opt(HostType, ?MODULE, [validity_period, Type]).
364
365
:-(
period_to_seconds(Days, days) -> 24 * 3600 * Days;
366
:-(
period_to_seconds(Hours, hours) -> 3600 * Hours;
367
:-(
period_to_seconds(Minutes, minutes) -> 60 * Minutes;
368
:-(
period_to_seconds(Seconds, seconds) -> Seconds.
369
370 token_to_xmlel(#token{type = Type} = T) ->
371
:-(
#xmlel{name = case Type of
372
:-(
access -> <<"access_token">>;
373
:-(
refresh -> <<"refresh_token">>
374 end,
375 attrs = [{<<"xmlns">>, ?NS_ESL_TOKEN_AUTH}],
376 children = [#xmlcdata{content = jlib:encode_base64(serialize(T))}]}.
377
378 %% args: Token with Mac decoded from transport, #token
379 %% is shared between tokens. Introduce other container types if
380 %% they start to differ more than a few fields.
381 -spec get_token_as_record(BToken) -> Token when
382 BToken :: serialized(),
383 Token :: token().
384 get_token_as_record(BToken) ->
385
:-(
[BType, User, Expiry | Rest] = binary:split(BToken, <<(field_separator())>>, [global]),
386
:-(
T = #token{type = decode_token_type(BType),
387 expiry_datetime = seconds_to_datetime(binary_to_integer(Expiry)),
388 user_jid = jid:from_binary(User)},
389
:-(
T1 = case {BType, Rest} of
390 {<<"access">>, [BMAC]} ->
391
:-(
T#token{mac_signature = base16:decode(BMAC)};
392 {<<"refresh">>, [BSeqNo, BMAC]} ->
393
:-(
T#token{sequence_no = ?B2I(BSeqNo),
394 mac_signature = base16:decode(BMAC)};
395 {<<"provision">>, [BVCard, BMAC]} ->
396
:-(
{ok, VCard} = exml:parse(BVCard),
397
:-(
T#token{vcard = VCard,
398 mac_signature = base16:decode(BMAC)}
399 end,
400
:-(
T1#token{token_body = join_fields(T1)}.
401
402 -spec decode_token_type(binary()) -> token_type().
403 decode_token_type(<<"access">>) ->
404
:-(
access;
405 decode_token_type(<<"refresh">>) ->
406
:-(
refresh;
407 decode_token_type(<<"provision">>) ->
408
:-(
provision.
409
410 -spec get_key_for_host_type(mongooseim:host_type(), token_type()) -> binary().
411 get_key_for_host_type(HostType, TokenType) ->
412
:-(
KeyName = key_name(TokenType),
413
:-(
[{{KeyName, _UsersHost}, RawKey}] = mongoose_hooks:get_key(HostType, KeyName),
414
:-(
RawKey.
415
416 -spec key_name(token_type()) -> token_secret | provision_pre_shared.
417
:-(
key_name(access) -> token_secret;
418
:-(
key_name(refresh) -> token_secret;
419
:-(
key_name(provision) -> provision_pre_shared.
420
421 -spec revoke_token_command(Owner) -> ResTuple when
422 Owner :: binary(),
423 ResCode :: ok | not_found | error,
424 ResTuple :: {ResCode, string()}.
425 revoke_token_command(Owner) ->
426
:-(
#jid{lserver = LServer} = Jid = jid:from_binary(Owner),
427
:-(
{ok, HostType} = mongoose_domain_api:get_domain_host_type(LServer),
428
:-(
try revoke(HostType, Jid) of
429 not_found ->
430
:-(
{not_found, "User or token not found."};
431 ok ->
432
:-(
{ok, "Revoked."};
433 error ->
434
:-(
{error, "Internal server error"}
435 catch _:_ ->
436
:-(
{error, "Internal server error"}
437 end.
438
439 -spec clean_tokens(mongoose_acc:t(), User :: jid:user(), Server :: jid:server()) ->
440 mongoose_acc:t().
441 clean_tokens(Acc, User, Server) ->
442
:-(
HostType = mongoose_acc:host_type(Acc),
443
:-(
Owner = jid:make(User, Server, <<>>),
444
:-(
try
445
:-(
mod_auth_token_backend:clean_tokens(HostType, Owner)
446 catch
447 Class:Reason:Stacktrace ->
448
:-(
?LOG_ERROR(#{what => auth_token_clean_tokens_failed,
449 text => <<"Error in clean_tokens backend">>,
450 user => User, server => Server, acc => Acc,
451
:-(
class => Class, reason => Reason, stacktrace => Stacktrace}),
452
:-(
{error, {Class, Reason}}
453 end,
454
:-(
Acc.
455
456 -spec config_metrics(mongooseim:host_type()) -> [{gen_mod:opt_key(), gen_mod:opt_value()}].
457 config_metrics(HostType) ->
458
:-(
mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]).
459
460 -spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc().
461 disco_local_features(Acc = #{node := <<>>}) ->
462
:-(
mongoose_disco:add_features([?NS_ESL_TOKEN_AUTH], Acc);
463 disco_local_features(Acc) ->
464
:-(
Acc.
Line Hits Source