Packages
ejabberd
24.2.6
26.4.0
26.3.0
26.2.0
26.1.0
25.10.0
25.8.0
25.7.0
25.4.0
25.3.0
24.12.0
24.10.0
24.7.0
24.6.0
24.2.6
23.10.0
23.4.0
23.1.0
22.10.0
22.5.0
21.12.0
21.7.0
21.4.0
21.1.0
20.12.0
20.7.0
20.4.0
20.3.0
20.2.0
20.1.0
19.9.1
19.9.0
19.8.0
19.5.0
19.2.0
18.12.1
18.12.0
18.6.0
18.4.0
18.3.0
18.1.0
17.11.0
17.9.0
17.6.0
17.3.0
17.1.0
16.12.0-beta1
16.9.0
16.8.0
16.6.2
16.6.1
16.6.0
16.4.1
16.4.0
16.3.0
16.2.0
16.1.0-beta1
Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP, MQTT, SIP Server)
Current section
Files
Jump to
Current section
Files
src/ejabberd_oauth.erl
%%%-------------------------------------------------------------------
%%% File : ejabberd_oauth.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : OAUTH2 support
%%% Created : 20 Mar 2015 by Alexey Shchepin <alexey@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., 59 Temple Place, Suite 330, Boston, MA
%%% 02111-1307 USA
%%%
%%%-------------------------------------------------------------------
-module(ejabberd_oauth).
-behaviour(gen_server).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
-export([start_link/0,
get_client_identity/2,
verify_redirection_uri/3,
authenticate_user/2,
authenticate_client/2,
associate_access_code/3,
associate_access_token/3,
associate_refresh_token/3,
check_token/1,
check_token/4,
check_token/2,
scope_in_scope_list/2,
process/2,
config_reloaded/0,
verify_resowner_scope/3]).
-export([get_commands_spec/0,
oauth_issue_token/3, oauth_list_tokens/0, oauth_revoke_token/1,
oauth_add_client_password/3,
oauth_add_client_implicit/3,
oauth_remove_client/1]).
-include_lib("xmpp/include/xmpp.hrl").
-include("logger.hrl").
-include("ejabberd_http.hrl").
-include("ejabberd_web_admin.hrl").
-include("ejabberd_oauth.hrl").
-include("ejabberd_commands.hrl").
-include("translate.hrl").
-callback init() -> any().
-callback store(#oauth_token{}) -> ok | {error, any()}.
-callback lookup(binary()) -> {ok, #oauth_token{}} | error.
-callback revoke(binary()) -> ok | {error, binary()}.
-callback clean(non_neg_integer()) -> any().
-record(oauth_ctx, {
password :: binary() | admin_generated,
client :: #oauth_client{} | undefined
}).
%% There are two ways to obtain an oauth token:
%% * Using the web form/api results in the token being generated in behalf of the user providing the user/pass
%% * Using the command line and oauth_issue_token command, the token is generated in behalf of ejabberd' sysadmin
%% (as it has access to ejabberd command line).
get_commands_spec() ->
[
#ejabberd_commands{name = oauth_issue_token, tags = [oauth],
desc = "Issue an [OAuth](https://docs.ejabberd.im/developer/ejabberd-api/oauth/) token for the given jid",
module = ?MODULE, function = oauth_issue_token,
args = [{jid, string},{ttl, integer}, {scopes, string}],
policy = restricted,
args_example = ["user@server.com", 3600, "connected_users_number;muc_online_rooms"],
args_desc = ["Jid for which issue token",
"Time to live of generated token in seconds",
"List of scopes to allow, separated by ';'"],
result = {result, {tuple, [{token, string}, {scopes, string}, {expires_in, string}]}}
},
#ejabberd_commands{name = oauth_issue_token, tags = [oauth],
desc = "Issue an [OAuth](https://docs.ejabberd.im/developer/ejabberd-api/oauth/) token for the given jid",
module = ?MODULE, function = oauth_issue_token,
version = 1,
note = "updated in 24.02",
args = [{jid, string}, {ttl, integer}, {scopes, {list, {scope, binary}}}],
policy = restricted,
args_example = ["user@server.com", 3600, ["connected_users_number", "muc_online_rooms"]],
args_desc = ["Jid for which issue token",
"Time to live of generated token in seconds",
"List of scopes to allow"],
result = {result, {tuple, [{token, string}, {scopes, {list, {scope, string}}}, {expires_in, string}]}}
},
#ejabberd_commands{name = oauth_list_tokens, tags = [oauth],
desc = "List [OAuth](https://docs.ejabberd.im/developer/ejabberd-api/oauth/) tokens, user, scope, and seconds to expire (only Mnesia)",
longdesc = "List OAuth tokens, their user and scope, and how many seconds remain until expirity",
module = ?MODULE, function = oauth_list_tokens,
args = [],
policy = restricted,
result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}}
},
#ejabberd_commands{name = oauth_revoke_token, tags = [oauth],
desc = "Revoke authorization for an [OAuth](https://docs.ejabberd.im/developer/ejabberd-api/oauth/) token",
note = "changed in 22.05",
module = ?MODULE, function = oauth_revoke_token,
args = [{token, binary}],
policy = restricted,
result = {res, restuple},
result_desc = "Result code"
},
#ejabberd_commands{name = oauth_add_client_password, tags = [oauth],
desc = "Add [OAuth](https://docs.ejabberd.im/developer/ejabberd-api/oauth/) client_id with password grant type",
module = ?MODULE, function = oauth_add_client_password,
args = [{client_id, binary},
{client_name, binary},
{secret, binary}],
policy = restricted,
result = {res, restuple}
},
#ejabberd_commands{name = oauth_add_client_implicit, tags = [oauth],
desc = "Add [OAuth](https://docs.ejabberd.im/developer/ejabberd-api/oauth/) client_id with implicit grant type",
module = ?MODULE, function = oauth_add_client_implicit,
args = [{client_id, binary},
{client_name, binary},
{redirect_uri, binary}],
policy = restricted,
result = {res, restuple}
},
#ejabberd_commands{name = oauth_remove_client, tags = [oauth],
desc = "Remove [OAuth](https://docs.ejabberd.im/developer/ejabberd-api/oauth/) client_id",
module = ?MODULE, function = oauth_remove_client,
args = [{client_id, binary}],
policy = restricted,
result = {res, restuple}
}
].
oauth_issue_token(Jid, TTLSeconds, [Head|_] = ScopesString) when is_integer(Head) ->
Scopes = [list_to_binary(Scope) || Scope <- string:tokens(ScopesString, ";")],
oauth_issue_token(Jid, TTLSeconds, Scopes);
oauth_issue_token(Jid, TTLSeconds, Scopes) ->
try jid:decode(list_to_binary(Jid)) of
#jid{luser =Username, lserver = Server} ->
Ctx1 = #oauth_ctx{password = admin_generated},
case oauth2:authorize_password({Username, Server}, Scopes, Ctx1) of
{ok, {_Ctx,Authorization}} ->
{ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, [{expiry_time, TTLSeconds}]),
{ok, AccessToken} = oauth2_response:access_token(Response),
{ok, VerifiedScope} = oauth2_response:scope(Response),
{AccessToken, VerifiedScope, integer_to_list(TTLSeconds) ++ " seconds"};
{error, Error} ->
{error, Error}
end
catch _:{bad_jid, _} ->
{error, "Invalid JID: " ++ Jid}
end.
oauth_list_tokens() ->
Tokens = mnesia:dirty_match_object(#oauth_token{_ = '_'}),
{MegaSecs, Secs, _MiniSecs} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
[{Token, jid:encode(jid:make(U,S)), Scope, integer_to_list(Expires - TS) ++ " seconds"} ||
#oauth_token{token=Token, scope=Scope, us= {U,S},expire=Expires} <- Tokens].
oauth_revoke_token(Token) ->
DBMod = get_db_backend(),
case DBMod:revoke(Token) of
ok ->
ets_cache:delete(oauth_cache, Token,
ejabberd_cluster:get_nodes()),
{ok, ""};
Other ->
Other
end.
oauth_add_client_password(ClientID, ClientName, Secret) ->
DBMod = get_db_backend(),
DBMod:store_client(#oauth_client{client_id = ClientID,
client_name = ClientName,
grant_type = password,
options = [{secret, Secret}]}),
{ok, []}.
oauth_add_client_implicit(ClientID, ClientName, RedirectURI) ->
DBMod = get_db_backend(),
DBMod:store_client(#oauth_client{client_id = ClientID,
client_name = ClientName,
grant_type = implicit,
options = [{redirect_uri, RedirectURI}]}),
{ok, []}.
oauth_remove_client(Client) ->
DBMod = get_db_backend(),
DBMod:remove_client(Client),
{ok, []}.
config_reloaded() ->
DBMod = get_db_backend(),
case init_cache(DBMod) of
true ->
ets_cache:setopts(oauth_cache, cache_opts());
false ->
ok
end.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) ->
DBMod = get_db_backend(),
DBMod:init(),
init_cache(DBMod),
Expire = expire(),
application:set_env(oauth2, backend, ejabberd_oauth),
application:set_env(oauth2, expiry_time, Expire div 1000),
application:start(oauth2),
ejabberd_commands:register_commands(get_commands_spec()),
ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50),
erlang:send_after(expire(), self(), clean),
{ok, ok}.
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(clean, State) ->
{MegaSecs, Secs, MiniSecs} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
DBMod = get_db_backend(),
DBMod:clean(TS),
erlang:send_after(trunc(expire() * (1 + MiniSecs / 1000000)),
self(), clean),
{noreply, State};
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50).
code_change(_OldVsn, State, _Extra) -> {ok, State}.
get_client_identity(<<"">>, Ctx) ->
{ok, {Ctx, {client, unknown_client}}};
get_client_identity(ClientID, Ctx) when is_binary(ClientID) ->
{ok, {Ctx, {client, ClientID}}}.
verify_redirection_uri(_ClientID, RedirectURI, Ctx) ->
case Ctx of
#oauth_ctx{client = #oauth_client{grant_type = implicit} = Client} ->
case get_redirect_uri(Client) of
RedirectURI ->
{ok, Ctx};
_ ->
{error, invalid_uri}
end;
#oauth_ctx{client = #oauth_client{}} ->
{error, invalid_client};
_ ->
{ok, Ctx}
end.
authenticate_user({User, Server}, Ctx) ->
case jid:make(User, Server) of
#jid{} = JID ->
Access =
ejabberd_option:oauth_access(JID#jid.lserver),
case acl:match_rule(JID#jid.lserver, Access, JID) of
allow ->
case Ctx of
#oauth_ctx{password = admin_generated} ->
{ok, {Ctx, {user, User, Server}}};
#oauth_ctx{password = Password}
when is_binary(Password) ->
case ejabberd_auth:check_password(User, <<"">>, Server, Password) of
true ->
{ok, {Ctx, {user, User, Server}}};
false ->
{error, badpass}
end
end;
deny ->
{error, badpass}
end;
error ->
{error, badpass}
end.
authenticate_client(ClientID, Ctx) ->
case ejabberd_option:oauth_client_id_check() of
allow ->
{ok, {Ctx, {client, ClientID}}};
deny -> {error, not_allowed};
db ->
DBMod = get_db_backend(),
case DBMod:lookup_client(ClientID) of
{ok, #oauth_client{} = Client} ->
{ok, {Ctx#oauth_ctx{client = Client}, {client, ClientID}}};
_ ->
{error, not_allowed}
end
end.
-spec verify_resowner_scope({user, binary(), binary()}, [binary()], any()) ->
{ok, any(), [binary()]} | {error, any()}.
verify_resowner_scope({user, _User, _Server}, Scope, Ctx) ->
Cmds = [atom_to_binary(Name, utf8) || {Name, _, _} <- ejabberd_commands:list_commands()],
AllowedScopes = [<<"ejabberd:user">>, <<"ejabberd:admin">>, <<"sasl_auth">>] ++ Cmds,
case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
oauth2_priv_set:new(AllowedScopes)) of
true ->
{ok, {Ctx, Scope}};
false ->
{error, badscope}
end;
verify_resowner_scope(_, _, _) ->
{error, badscope}.
%% This is callback for oauth tokens generated through the command line. Only open and admin commands are
%% made available.
%verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) ->
% RegisteredScope = dict:fetch_keys(get_cmd_scopes()),
% case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
% oauth2_priv_set:new(RegisteredScope)) of
% true ->
% {ok, {Ctx, Scope}};
% false ->
% {error, badscope}
% end.
-spec seconds_since_epoch(integer()) -> non_neg_integer().
seconds_since_epoch(Diff) ->
{Mega, Secs, _} = os:timestamp(),
Mega * 1000000 + Secs + Diff.
associate_access_code(_AccessCode, _Context, AppContext) ->
%put(?ACCESS_CODE_TABLE, AccessCode, Context),
{ok, AppContext}.
associate_access_token(AccessToken, Context, AppContext) ->
{user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>),
Expire = case proplists:get_value(expiry_time, AppContext, undefined) of
undefined ->
proplists:get_value(<<"expiry_time">>, Context, 0);
ExpiresIn ->
%% There is no clean way in oauth2 lib to actually override the TTL of the generated token.
%% It always pass the global configured value. Here we use the app context to pass the per-case
%% ttl if we want to override it.
seconds_since_epoch(ExpiresIn)
end,
{user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>),
Scope = proplists:get_value(<<"scope">>, Context, []),
R = #oauth_token{
token = AccessToken,
us = {jid:nodeprep(User), jid:nodeprep(Server)},
scope = Scope,
expire = Expire
},
store(R),
{ok, AppContext}.
associate_refresh_token(_RefreshToken, _Context, AppContext) ->
%put(?REFRESH_TOKEN_TABLE, RefreshToken, Context),
{ok, AppContext}.
scope_in_scope_list(Scope, ScopeList) ->
TokenScopeSet = oauth2_priv_set:new(Scope),
lists:any(fun(Scope2) ->
oauth2_priv_set:is_member(Scope2, TokenScopeSet) end,
ScopeList).
-spec check_token(binary()) -> {ok, {binary(), binary()}, [binary()]} |
{false, expired | not_found}.
check_token(Token) ->
case lookup(Token) of
{ok, #oauth_token{us = US,
scope = TokenScope,
expire = Expire}} ->
{MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
if
Expire > TS ->
{ok, US, TokenScope};
true ->
{false, expired}
end;
_ ->
{false, not_found}
end.
check_token(User, Server, ScopeList, Token) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
case lookup(Token) of
{ok, #oauth_token{us = {LUser, LServer},
scope = TokenScope,
expire = Expire}} ->
{MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
if
Expire > TS ->
TokenScopeSet = oauth2_priv_set:new(TokenScope),
lists:any(fun(Scope) ->
oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
ScopeList);
true ->
{false, expired}
end;
_ ->
{false, not_found}
end.
check_token(ScopeList, Token) ->
case lookup(Token) of
{ok, #oauth_token{us = US,
scope = TokenScope,
expire = Expire}} ->
{MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
if
Expire > TS ->
TokenScopeSet = oauth2_priv_set:new(TokenScope),
case lists:any(fun(Scope) ->
oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
ScopeList) of
true -> {ok, user, US};
false -> {false, no_matching_scope}
end;
true ->
{false, expired}
end;
_ ->
{false, not_found}
end.
store(R) ->
DBMod = get_db_backend(),
case DBMod:store(R) of
ok ->
ets_cache:delete(oauth_cache, R#oauth_token.token,
ejabberd_cluster:get_nodes());
{error, _} = Err ->
Err
end.
lookup(Token) ->
ets_cache:lookup(oauth_cache, Token,
fun() ->
DBMod = get_db_backend(),
DBMod:lookup(Token)
end).
-spec init_cache(module()) -> boolean().
init_cache(DBMod) ->
UseCache = use_cache(DBMod),
case UseCache of
true ->
ets_cache:new(oauth_cache, cache_opts());
false ->
ets_cache:delete(oauth_cache)
end,
UseCache.
use_cache(DBMod) ->
case erlang:function_exported(DBMod, use_cache, 0) of
true -> DBMod:use_cache();
false -> ejabberd_option:oauth_use_cache()
end.
cache_opts() ->
MaxSize = ejabberd_option:oauth_cache_size(),
CacheMissed = ejabberd_option:oauth_cache_missed(),
LifeTime = ejabberd_option:oauth_cache_life_time(),
[{max_size, MaxSize}, {life_time, LifeTime}, {cache_missed, CacheMissed}].
expire() ->
ejabberd_option:oauth_expire().
-define(DIV(Class, Els),
?XAE(<<"div">>, [{<<"class">>, Class}], Els)).
-define(INPUTID(Type, Name, Value),
?XA(<<"input">>,
[{<<"type">>, Type}, {<<"name">>, Name},
{<<"value">>, Value}, {<<"id">>, Name}])).
-define(LABEL(ID, Els),
?XAE(<<"label">>, [{<<"for">>, ID}], Els)).
process(_Handlers,
#request{method = 'GET', q = Q, lang = Lang,
path = [_, <<"authorization_token">>]}) ->
ResponseType = proplists:get_value(<<"response_type">>, Q, <<"">>),
ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>),
JidEls = case proplists:get_value(<<"jid">>, Q, <<"">>) of
<<"">> -> [?INPUTID(<<"email">>, <<"username">>, <<"">>)];
Jid -> [?C(Jid), ?INPUT(<<"hidden">>, <<"username">>, Jid)]
end,
RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>),
Scope = proplists:get_value(<<"scope">>, Q, <<"">>),
State = proplists:get_value(<<"state">>, Q, <<"">>),
Form =
?XAE(<<"form">>,
[{<<"action">>, <<"authorization_token">>},
{<<"method">>, <<"post">>}],
[?LABEL(<<"username">>, [?CT(?T("User (jid)")), ?C(<<": ">>)])
] ++ JidEls ++ [
?BR,
?LABEL(<<"password">>, [?CT(?T("Password")), ?C(<<": ">>)]),
?INPUTID(<<"password">>, <<"password">>, <<"">>),
?INPUT(<<"hidden">>, <<"response_type">>, ResponseType),
?INPUT(<<"hidden">>, <<"client_id">>, ClientId),
?INPUT(<<"hidden">>, <<"redirect_uri">>, RedirectURI),
?INPUT(<<"hidden">>, <<"scope">>, Scope),
?INPUT(<<"hidden">>, <<"state">>, State),
?BR,
?LABEL(<<"ttl">>, [?CT(?T("Token TTL")), ?C(<<": ">>)]),
?XAE(<<"select">>, [{<<"name">>, <<"ttl">>}],
[
?XAC(<<"option">>, [{<<"value">>, <<"3600">>}],<<"1 Hour">>),
?XAC(<<"option">>, [{<<"value">>, <<"86400">>}],<<"1 Day">>),
?XAC(<<"option">>, [{<<"value">>, <<"2592000">>}],<<"1 Month">>),
?XAC(<<"option">>, [{<<"selected">>, <<"selected">>},{<<"value">>, <<"31536000">>}],<<"1 Year">>),
?XAC(<<"option">>, [{<<"value">>, <<"315360000">>}],<<"10 Years">>)]),
?BR,
?INPUTT(<<"submit">>, <<"">>, ?T("Accept"))
]),
Top =
?DIV(<<"section">>,
[?DIV(<<"block">>,
[?A(<<"https://www.ejabberd.im">>,
[?XA(<<"img">>,
[{<<"height">>, <<"32">>},
{<<"src">>, logo()}])]
)])]),
Middle =
?DIV(<<"white section">>,
[?DIV(<<"block">>,
[?XC(<<"h1">>, <<"Authorization request">>),
?XE(<<"p">>,
[?C(<<"Application ">>),
?XC(<<"em">>, ClientId),
?C(<<" wants to access scope ">>),
?XC(<<"em">>, Scope)]),
Form
])]),
Bottom =
?DIV(<<"section">>,
[?DIV(<<"block">>,
[?XAC(<<"a">>,
[{<<"href">>, <<"https://www.ejabberd.im">>},
{<<"title">>, <<"ejabberd XMPP server">>}],
<<"ejabberd">>),
?C(<<" is maintained by ">>),
?XAC(<<"a">>,
[{<<"href">>, <<"https://www.process-one.net">>},
{<<"title">>, <<"ProcessOne - Leader in Instant Messaging and Push Solutions">>}],
<<"ProcessOne">>)
])]),
Body = ?DIV(<<"container">>, [Top, Middle, Bottom]),
ejabberd_web:make_xhtml(web_head(), [Body]);
process(_Handlers,
#request{method = 'POST', q = Q, lang = _Lang,
path = [_, <<"authorization_token">>]}) ->
_ResponseType = proplists:get_value(<<"response_type">>, Q, <<"">>),
ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>),
RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>),
SScope = proplists:get_value(<<"scope">>, Q, <<"">>),
StringJID = proplists:get_value(<<"username">>, Q, <<"">>),
#jid{user = Username, server = Server} = jid:decode(StringJID),
Password = proplists:get_value(<<"password">>, Q, <<"">>),
State = proplists:get_value(<<"state">>, Q, <<"">>),
Scope = str:tokens(SScope, <<" ">>),
TTL = proplists:get_value(<<"ttl">>, Q, <<"">>),
ExpiresIn = case TTL of
<<>> -> undefined;
_ -> binary_to_integer(TTL)
end,
case oauth2:authorize_password({Username, Server},
ClientId,
RedirectURI,
Scope,
#oauth_ctx{password = Password}) of
{ok, {_AppContext, Authorization}} ->
{ok, {_AppContext2, Response}} =
oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]),
{ok, AccessToken} = oauth2_response:access_token(Response),
{ok, Type} = oauth2_response:token_type(Response),
%%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have
%%per-case expirity time.
Expires = case ExpiresIn of
undefined ->
{ok, Ex} = oauth2_response:expires_in(Response),
Ex;
_ ->
ExpiresIn
end,
{ok, VerifiedScope} = oauth2_response:scope(Response),
%oauth2_wrq:redirected_access_token_response(ReqData,
% RedirectURI,
% AccessToken,
% Type,
% Expires,
% VerifiedScope,
% State,
% Context);
{302, [{<<"Location">>,
<<RedirectURI/binary,
"?access_token=", AccessToken/binary,
"&token_type=", Type/binary,
"&expires_in=", (integer_to_binary(Expires))/binary,
"&scope=", (str:join(VerifiedScope, <<" ">>))/binary,
"&state=", State/binary>>
}],
ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])};
{error, Error} when is_atom(Error) ->
%oauth2_wrq:redirected_error_response(
% ReqData, RedirectURI, Error, State, Context)
{302, [{<<"Location">>,
<<RedirectURI/binary,
"?error=", (atom_to_binary(Error, utf8))/binary,
"&state=", State/binary>>
}],
ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])}
end;
process(_Handlers,
#request{method = 'POST', q = Q, lang = _Lang,
auth = HTTPAuth,
path = [_, <<"token">>]}) ->
Access =
case ejabberd_option:oauth_client_id_check() of
allow ->
case proplists:get_value(<<"grant_type">>, Q, <<"">>) of
<<"password">> ->
password;
_ ->
unsupported_grant_type
end;
deny ->
deny;
db ->
{ClientID, Secret} =
case HTTPAuth of
{ClientID1, Secret1} ->
{ClientID1, Secret1};
_ ->
ClientID1 = proplists:get_value(
<<"client_id">>, Q, <<"">>),
Secret1 = proplists:get_value(
<<"client_secret">>, Q, <<"">>),
{ClientID1, Secret1}
end,
DBMod = get_db_backend(),
case DBMod:lookup_client(ClientID) of
{ok, #oauth_client{grant_type = password} = Client} ->
case get_client_secret(Client) of
Secret ->
case proplists:get_value(<<"grant_type">>, Q, <<"">>) of
<<"password">> when
Client#oauth_client.grant_type == password ->
password;
_ ->
unsupported_grant_type
end;
_ ->
deny
end;
_ ->
deny
end
end,
case Access of
password ->
SScope = proplists:get_value(<<"scope">>, Q, <<"">>),
StringJID = proplists:get_value(<<"username">>, Q, <<"">>),
#jid{user = Username, server = Server} = jid:decode(StringJID),
Password = proplists:get_value(<<"password">>, Q, <<"">>),
Scope = str:tokens(SScope, <<" ">>),
TTL = proplists:get_value(<<"ttl">>, Q, <<"">>),
ExpiresIn = case TTL of
<<>> -> undefined;
_ -> binary_to_integer(TTL)
end,
case oauth2:authorize_password({Username, Server},
Scope,
#oauth_ctx{password = Password}) of
{ok, {_AppContext, Authorization}} ->
{ok, {_AppContext2, Response}} =
oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]),
{ok, AccessToken} = oauth2_response:access_token(Response),
{ok, Type} = oauth2_response:token_type(Response),
%%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have
%%per-case expirity time.
Expires = case ExpiresIn of
undefined ->
{ok, Ex} = oauth2_response:expires_in(Response),
Ex;
_ ->
ExpiresIn
end,
{ok, VerifiedScope} = oauth2_response:scope(Response),
json_response(200, {[
{<<"access_token">>, AccessToken},
{<<"token_type">>, Type},
{<<"scope">>, str:join(VerifiedScope, <<" ">>)},
{<<"expires_in">>, Expires}]});
{error, Error} when is_atom(Error) ->
json_error(400, <<"invalid_grant">>, Error)
end;
unsupported_grant_type ->
json_error(400, <<"unsupported_grant_type">>,
unsupported_grant_type);
deny ->
ejabberd_web:error(not_allowed)
end;
process(_Handlers, _Request) ->
ejabberd_web:error(not_found).
-spec get_db_backend() -> module().
get_db_backend() ->
DBType = ejabberd_option:oauth_db_type(),
list_to_existing_atom("ejabberd_oauth_" ++ atom_to_list(DBType)).
get_client_secret(#oauth_client{grant_type = password, options = Options}) ->
proplists:get_value(secret, Options, false).
get_redirect_uri(#oauth_client{grant_type = implicit, options = Options}) ->
proplists:get_value(redirect_uri, Options, false).
%% Headers as per RFC 6749
json_response(Code, Body) ->
{Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>},
{<<"Cache-Control">>, <<"no-store">>},
{<<"Pragma">>, <<"no-cache">>}],
jiffy:encode(Body)}.
%% OAauth error are defined in:
%% https://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-5.2
json_error(Code, Error, Reason) ->
Desc = json_error_desc(Reason),
Body = {[{<<"error">>, Error},
{<<"error_description">>, Desc}]},
json_response(Code, Body).
json_error_desc(access_denied) -> <<"Access denied">>;
json_error_desc(badpass) -> <<"Bad password">>;
json_error_desc(unsupported_grant_type) -> <<"Unsupported grant type">>;
json_error_desc(invalid_scope) -> <<"Invalid scope">>.
web_head() ->
[?XA(<<"meta">>, [{<<"http-equiv">>, <<"X-UA-Compatible">>},
{<<"content">>, <<"IE=edge">>}]),
?XA(<<"meta">>, [{<<"name">>, <<"viewport">>},
{<<"content">>,
<<"width=device-width, initial-scale=1">>}]),
?XC(<<"title">>, <<"Authorization request">>),
?XC(<<"style">>, css())
].
css() ->
case misc:read_css("oauth.css") of
{ok, Data} -> Data;
{error, _} -> <<>>
end.
logo() ->
case misc:read_img("oauth-logo.png") of
{ok, Img} ->
B64Img = base64:encode(Img),
<<"data:image/png;base64,", B64Img/binary>>;
{error, _} ->
<<>>
end.