mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-24 16:23:40 +01:00
155 lines
5.8 KiB
Erlang
155 lines
5.8 KiB
Erlang
%%%----------------------------------------------------------------------
|
|
%%% File : ejabberd_auth_jwt.erl
|
|
%%% Author : Mickael Remond <mremond@process-one.net>
|
|
%%% Purpose : Authentication using JWT tokens
|
|
%%% Created : 16 Mar 2019 by Mickael Remond <mremond@process-one.net>
|
|
%%%
|
|
%%%
|
|
%%% ejabberd, Copyright (C) 2002-2024 ProcessOne
|
|
%%%
|
|
%%% This program is free software; you can redistribute it and/or
|
|
%%% modify it under the terms of the GNU General Public License as
|
|
%%% published by the Free Software Foundation; either version 2 of the
|
|
%%% License, or (at your option) any later version.
|
|
%%%
|
|
%%% This program is distributed in the hope that it will be useful,
|
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
%%% General Public License for more details.
|
|
%%%
|
|
%%% You should have received a copy of the GNU General Public License along
|
|
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
|
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
%%%
|
|
%%%----------------------------------------------------------------------
|
|
|
|
-module(ejabberd_auth_jwt).
|
|
|
|
-author('mremond@process-one.net').
|
|
|
|
-behaviour(ejabberd_auth).
|
|
|
|
-export([start/1, stop/1, check_password/4,
|
|
store_type/1, plain_password_required/1,
|
|
user_exists/2, use_cache/1
|
|
]).
|
|
%% 'ejabberd_hooks' callback:
|
|
-export([check_decoded_jwt/5]).
|
|
|
|
-include_lib("xmpp/include/xmpp.hrl").
|
|
-include("logger.hrl").
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% API
|
|
%%%----------------------------------------------------------------------
|
|
start(Host) ->
|
|
%% We add our default JWT verifier with hook priority 100.
|
|
%% So if you need to check or verify your custom JWT before the
|
|
%% default verifier, It's better to use this hook with priority
|
|
%% little than 100 and return bool() or {stop, bool()} in your own
|
|
%% callback function.
|
|
ejabberd_hooks:add(check_decoded_jwt, Host, ?MODULE, check_decoded_jwt, 100),
|
|
case ejabberd_option:jwt_key(Host) of
|
|
undefined ->
|
|
?ERROR_MSG("Option jwt_key is not configured for ~ts: "
|
|
"JWT authentication won't work", [Host]);
|
|
_ ->
|
|
ok
|
|
end.
|
|
|
|
stop(Host) ->
|
|
ejabberd_hooks:delete(check_decoded_jwt, Host, ?MODULE, check_decoded_jwt, 100).
|
|
|
|
plain_password_required(_Host) -> true.
|
|
|
|
store_type(_Host) -> external.
|
|
|
|
-spec check_password(binary(), binary(), binary(), binary()) -> {ets_cache:tag(), boolean() | {stop, boolean()}}.
|
|
check_password(User, AuthzId, Server, Token) ->
|
|
%% MREMOND: Should we move the AuthzId check at a higher level in
|
|
%% the call stack?
|
|
if AuthzId /= <<>> andalso AuthzId /= User ->
|
|
{nocache, false};
|
|
true ->
|
|
if Token == <<"">> -> {nocache, false};
|
|
true ->
|
|
Res = check_jwt_token(User, Server, Token),
|
|
Rule = ejabberd_option:jwt_auth_only_rule(Server),
|
|
case acl:match_rule(Server, Rule,
|
|
jid:make(User, Server, <<"">>)) of
|
|
deny ->
|
|
{nocache, Res};
|
|
allow ->
|
|
{nocache, {stop, Res}}
|
|
end
|
|
end
|
|
end.
|
|
|
|
user_exists(User, Host) ->
|
|
%% Checking that the user has an active session
|
|
%% If the session was negociated by the JWT auth method then we define that the user exists
|
|
%% Any other cases will return that the user doesn't exist
|
|
{nocache, case ejabberd_sm:get_user_info(User, Host) of
|
|
[{_, Info}] -> proplists:get_value(auth_module, Info) == ejabberd_auth_jwt;
|
|
_ -> false
|
|
end}.
|
|
|
|
use_cache(_) ->
|
|
false.
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% 'ejabberd_hooks' callback
|
|
%%%----------------------------------------------------------------------
|
|
check_decoded_jwt(true, Fields, _Signature, Server, User) ->
|
|
JidField = ejabberd_option:jwt_jid_field(Server),
|
|
case maps:find(JidField, Fields) of
|
|
{ok, SJid} when is_binary(SJid) ->
|
|
try
|
|
JID = jid:decode(SJid),
|
|
JID#jid.luser == User andalso JID#jid.lserver == Server
|
|
catch error:{bad_jid, _} ->
|
|
false
|
|
end;
|
|
_ -> % error | {ok, _UnknownType}
|
|
false
|
|
end;
|
|
check_decoded_jwt(Acc, _, _, _, _) ->
|
|
Acc.
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% Internal functions
|
|
%%%----------------------------------------------------------------------
|
|
check_jwt_token(User, Server, Token) ->
|
|
JWK = ejabberd_option:jwt_key(Server),
|
|
try jose_jwt:verify(JWK, Token) of
|
|
{true, {jose_jwt, Fields}, Signature} ->
|
|
Now = erlang:system_time(second),
|
|
?DEBUG("jwt verify at system timestamp ~p: ~p - ~p~n", [Now, Fields, Signature]),
|
|
case maps:find(<<"exp">>, Fields) of
|
|
error ->
|
|
%% No expiry in token => We consider token invalid:
|
|
false;
|
|
{ok, Exp} ->
|
|
if
|
|
Exp > Now ->
|
|
ejabberd_hooks:run_fold(
|
|
check_decoded_jwt,
|
|
Server,
|
|
true,
|
|
[Fields, Signature, Server, User]
|
|
);
|
|
true ->
|
|
%% return false, if token has expired
|
|
false
|
|
end
|
|
end;
|
|
{false, _, _} ->
|
|
false
|
|
catch
|
|
A:B ->
|
|
?DEBUG("jose_jwt:verify failed ~n for account ~p@~p~n "
|
|
" JWK and token: ~p~n with error: ~p",
|
|
[User, Server, {JWK, Token}, {A, B}]),
|
|
false
|
|
end.
|