processone / ejabberd

Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP, MQTT, SIP Server)
https://www.process-one.net/ejabberd/
Other
6.12k stars 1.51k forks source link

Add custom metadata from JWT fields to c2s state #3237

Open pouriya opened 4 years ago

pouriya commented 4 years ago

Describe the solution you'd like something like:

{NewC2SState, NewAuthResult} = ejabberd_hooks:run_fold(
    jwt_check,
    LServer,
    {C2SState, AuthResult=true},
    [JWTFields]
)

So I can check my custom metadata in given fields and update state and so on.

Describe alternatives you've considered Since ejabberd_c2s uses ejabberd_auth:* API and the entire API does not accept C2SState, We can not put above hook here. So I think the fastest (and dirtiest) way is using for example c2s_handle_recv hook and check if auth method for detected server is JWT, if yes then I have to check JWT myself and it's error prone !

pouriya commented 4 years ago

ping

badlop commented 3 years ago

Did you add that to your local ejabberd deployment? In what file did you add it?

Neustradamus commented 3 years ago

@pouriya: Have you seen the @badlop comment?

pouriya commented 3 years ago

Hi. Sorry for late reply.
@badlop I currently use process dictionary to keep decoded JWT in ejabberd_auth_jwt and add it in c2s state after success authentication AND I KNOW it's dirty 👎🏼 .

But I am thinking of:

% ejabberd_c2s.erl

% ...

check_password_fun(<<"X-OAUTH2">>, #{lserver := LServer}) ->
    Pid = self(),
    fun(User, _AuthzId, Token) ->
        Res = case ejabberd_oauth:check_token(User, LServer, [<<"sasl_auth">>], Token) of
            true -> {true, ejabberd_oauth};
            _ -> {false, ejabberd_oauth}
        end,
        ejabberd_hooks:run(c2s_check_password_result, LServer, [Res, User, LServer, Token, Pid]),
        Res
    end;
check_password_fun(_Mech, #{lserver := LServer}) ->
    Pid = self(),
    fun(U, AuthzId, P) ->
            Res = ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P),
            ejabberd_hooks:run(c2s_check_password_result, LServer, [Res, User, LServer, P, Pid]),
            Res
    end.

check_password_digest_fun(_Mech, #{lserver := LServer}) ->
    Pid = self(),
    fun(U, AuthzId, P, D, DG) ->
        Res = ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P, D, DG),
        ejabberd_hooks:run(c2s_check_password_result, LServer, [Res, User, LServer, P, Pid]),
        Res
    end.

% ...

And:

% ejabberd_auth_jwt.erl

% ...

start(Host) ->
    % ...
    ejabberd_hooks:add(c2s_check_password_result, Host, ?MODULE, send_decoded_jwt_fields, 50),
    ejabberd_hooks:add(c2s_handle_info, Host, ?MODULE, c2s_handle_info_jwt_fields, 50),
    % ...
    .

send_decoded_jwt_fields({true, ?MODULE}=Ret, User, Server, Password, Pid) ->
    {true, {jose_jwt, Fields}, _} = jose_jwt:verify(JWK, Password), 
    ejabberd_cluster:send(Pid, {jwt_fields, Fields}),
    Ret;
send_decoded_jwt_fields(Acc, _, _, _, _) ->
    Acc.

c2s_handle_info_jwt_fields(State, {jwt_fields, Fields}) ->
    {stop, ejabberd_hooks:run_fold(process_decoded_jwt, LServer, State, [Fields])};
c2s_handle_info_jwt_fields(Acc, _) ->
    Acc.

% ...

Keeping Fields in process dictionary currently works for me. I'm not sure but I think in above code I'm sending Fields to self(). If this is correct, So we can leave ejabberd_c2s.erl unchanged and simply do:

% ejabberd_auth_jwt.erl

% ...

start(Host) ->
    % ...
    ejabberd_hooks:add(c2s_handle_info, Host, ?MODULE, c2s_handle_info_jwt_fields, 50),
    % ...
    .

% ...

c2s_handle_info_jwt_fields(State, {jwt_fields, Fields}) ->
    {stop, ejabberd_hooks:run_fold(process_decoded_jwt, LServer, State, [Fields])};
c2s_handle_info_jwt_fields(Acc, _) ->
    Acc.

% ...

%%%----------------------------------------------------------------------
%%% Internal functions
%%%----------------------------------------------------------------------
check_jwt_token(User, Server, Token) ->
    % ...
    Ret = ejabberd_hooks:run_fold(
        check_decoded_jwt,
        Server,
        true,
        [Fields, Signature, Server, User]
    ),
    Ret == true andalso ejabberd_cluster:send(self(), {jwt_fields, Fields}),
    Ret
    % ...
    .
badlop commented 3 years ago

If the only file to change is ejabberd_auth_jwt.erl, then it will be quite easy and safe to include. Check if that's the case.

If ejabberd_c2s.erl requires changes, and they are only adding calls to ejabberd_hooks:run, it seems small changes and could quite safe too.