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