Support OAUTH client authentication

This commit is contained in:
Alexey Shchepin 2019-09-27 20:36:35 +03:00
parent 47d0eed3f1
commit 8f7fa38949
10 changed files with 274 additions and 43 deletions

View File

@ -24,3 +24,10 @@
scope = [] :: [binary()] | '_', scope = [] :: [binary()] | '_',
expire :: integer() | '$1' | '_' expire :: integer() | '$1' | '_'
}). }).
-record(oauth_client, {
client = <<"">> :: binary() | '_',
secret = <<"">> :: binary() | '_',
grant_type = password :: password | '_',
options :: [any()] | '_'
}).

View File

@ -338,6 +338,13 @@ CREATE TABLE oauth_token (
expire bigint NOT NULL expire bigint NOT NULL
); );
CREATE TABLE oauth_client (
client text PRIMARY KEY,
secret text NOT NULL,
grant_type text NOT NULL,
options text NOT NULL
);
CREATE TABLE route ( CREATE TABLE route (
domain text NOT NULL, domain text NOT NULL,
server_host text NOT NULL, server_host text NOT NULL,

View File

@ -354,6 +354,13 @@ CREATE TABLE oauth_token (
expire bigint NOT NULL expire bigint NOT NULL
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE oauth_client (
client varchar(191) NOT NULL PRIMARY KEY,
secret text NOT NULL,
grant_type text NOT NULL,
options text NOT NULL
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE route ( CREATE TABLE route (
domain text NOT NULL, domain text NOT NULL,
server_host text NOT NULL, server_host text NOT NULL,

View File

@ -358,6 +358,13 @@ CREATE TABLE oauth_token (
CREATE UNIQUE INDEX i_oauth_token_token ON oauth_token USING btree (token); CREATE UNIQUE INDEX i_oauth_token_token ON oauth_token USING btree (token);
CREATE TABLE oauth_client (
client text PRIMARY KEY,
secret text NOT NULL,
grant_type text NOT NULL,
options text NOT NULL
);
CREATE TABLE route ( CREATE TABLE route (
domain text NOT NULL, domain text NOT NULL,
server_host text NOT NULL, server_host text NOT NULL,

View File

@ -49,7 +49,8 @@
verify_resowner_scope/3]). verify_resowner_scope/3]).
-export([get_commands_spec/0, -export([get_commands_spec/0,
oauth_issue_token/3, oauth_list_tokens/0, oauth_revoke_token/1]). oauth_issue_token/3, oauth_list_tokens/0, oauth_revoke_token/1,
oauth_add_client/3, oauth_remove_client/1]).
-include("xmpp.hrl"). -include("xmpp.hrl").
-include("logger.hrl"). -include("logger.hrl").
@ -97,6 +98,22 @@ get_commands_spec() ->
policy = restricted, policy = restricted,
result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}}, result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}},
result_desc = "List of remaining tokens" result_desc = "List of remaining tokens"
},
#ejabberd_commands{name = oauth_add_client, tags = [oauth],
desc = "Add OAUTH client_id",
module = ?MODULE, function = oauth_add_client,
args = [{client_id, binary},
{secret, binary},
{grant_type, binary}],
policy = restricted,
result = {res, restuple}
},
#ejabberd_commands{name = oauth_remove_client, tags = [oauth],
desc = "Remove OAUTH client_id",
module = ?MODULE, function = oauth_remove_client,
args = [{client_id, binary}],
policy = restricted,
result = {res, restuple}
} }
]. ].
@ -129,6 +146,24 @@ oauth_revoke_token(Token) ->
ok = mnesia:dirty_delete(oauth_token, list_to_binary(Token)), ok = mnesia:dirty_delete(oauth_token, list_to_binary(Token)),
oauth_list_tokens(). oauth_list_tokens().
oauth_add_client(Client, Secret, SGrantType) ->
case SGrantType of
<<"password">> ->
DBMod = get_db_backend(),
DBMod:store_client(#oauth_client{client = Client,
secret = Secret,
grant_type = password,
options = []}),
{ok, []};
_ ->
{error, "Unsupported grant type"}
end.
oauth_remove_client(Client) ->
DBMod = get_db_backend(),
DBMod:remove_client(Client),
{ok, []}.
config_reloaded() -> config_reloaded() ->
DBMod = get_db_backend(), DBMod = get_db_backend(),
case init_cache(DBMod) of case init_cache(DBMod) of
@ -535,48 +570,89 @@ process(_Handlers,
end; end;
process(_Handlers, process(_Handlers,
#request{method = 'POST', q = Q, lang = _Lang, #request{method = 'POST', q = Q, lang = _Lang,
auth = HTTPAuth,
path = [_, <<"token">>]}) -> path = [_, <<"token">>]}) ->
case proplists:get_value(<<"grant_type">>, Q, <<"">>) of Access =
<<"password">> -> case ejabberd_option:oauth_client_id_check() of
SScope = proplists:get_value(<<"scope">>, Q, <<"">>), allow ->
StringJID = proplists:get_value(<<"username">>, Q, <<"">>), case proplists:get_value(<<"grant_type">>, Q, <<"">>) of
#jid{user = Username, server = Server} = jid:decode(StringJID), <<"password">> ->
Password = proplists:get_value(<<"password">>, Q, <<"">>), password;
Scope = str:tokens(SScope, <<" ">>), _ ->
TTL = proplists:get_value(<<"ttl">>, Q, <<"">>), unsupported_grant_type
ExpiresIn = case TTL of end;
<<>> -> undefined; deny ->
_ -> binary_to_integer(TTL) 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, end,
case oauth2:authorize_password({Username, Server}, DBMod = get_db_backend(),
Scope, case DBMod:lookup_client(ClientID) of
{password, Password}) of {ok, #oauth_client{secret = Secret} = Client} ->
{ok, {_AppContext, Authorization}} -> case proplists:get_value(<<"grant_type">>, Q, <<"">>) of
{ok, {_AppContext2, Response}} = <<"password">> when
oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]), Client#oauth_client.grant_type == password ->
{ok, AccessToken} = oauth2_response:access_token(Response), password;
{ok, Type} = oauth2_response:token_type(Response), _ ->
%%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have unsupported_grant_type
%%per-case expirity time. end;
Expires = case ExpiresIn of _ ->
undefined -> deny
{ok, Ex} = oauth2_response:expires_in(Response), end
Ex; end,
_ -> case Access of
ExpiresIn password ->
end, SScope = proplists:get_value(<<"scope">>, Q, <<"">>),
{ok, VerifiedScope} = oauth2_response:scope(Response), StringJID = proplists:get_value(<<"username">>, Q, <<"">>),
json_response(200, {[ #jid{user = Username, server = Server} = jid:decode(StringJID),
{<<"access_token">>, AccessToken}, Password = proplists:get_value(<<"password">>, Q, <<"">>),
{<<"token_type">>, Type}, Scope = str:tokens(SScope, <<" ">>),
{<<"scope">>, str:join(VerifiedScope, <<" ">>)}, TTL = proplists:get_value(<<"ttl">>, Q, <<"">>),
{<<"expires_in">>, Expires}]}); ExpiresIn = case TTL of
{error, Error} when is_atom(Error) -> <<>> -> undefined;
json_error(400, <<"invalid_grant">>, Error) _ -> binary_to_integer(TTL)
end; end,
_OtherGrantType -> case oauth2:authorize_password({Username, Server},
json_error(400, <<"unsupported_grant_type">>, unsupported_grant_type) Scope,
end; {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) -> process(_Handlers, _Request) ->
ejabberd_web:error(not_found). ejabberd_web:error(not_found).

View File

@ -31,6 +31,9 @@
store/1, store/1,
lookup/1, lookup/1,
clean/1, clean/1,
lookup_client/1,
store_client/1,
remove_client/1,
use_cache/0]). use_cache/0]).
-include("ejabberd_oauth.hrl"). -include("ejabberd_oauth.hrl").
@ -40,6 +43,10 @@ init() ->
[{disc_only_copies, [node()]}, [{disc_only_copies, [node()]},
{attributes, {attributes,
record_info(fields, oauth_token)}]), record_info(fields, oauth_token)}]),
ejabberd_mnesia:create(?MODULE, oauth_client,
[{disc_copies, [node()]},
{attributes,
record_info(fields, oauth_client)}]),
ok. ok.
use_cache() -> use_cache() ->
@ -71,3 +78,17 @@ clean(TS) ->
lists:foreach(fun mnesia:delete_object/1, Ts) lists:foreach(fun mnesia:delete_object/1, Ts)
end, end,
mnesia:async_dirty(F). mnesia:async_dirty(F).
lookup_client(ClientID) ->
case catch mnesia:dirty_read(oauth_client, ClientID) of
[R] ->
{ok, R};
_ ->
error
end.
remove_client(ClientID) ->
mnesia:dirty_delete(oauth_client, ClientID).
store_client(R) ->
mnesia:dirty_write(R).

View File

@ -30,7 +30,9 @@
-export([init/0, -export([init/0,
store/1, store/1,
lookup/1, lookup/1,
clean/1]). clean/1,
lookup_client/1,
store_client/1]).
-include("ejabberd_oauth.hrl"). -include("ejabberd_oauth.hrl").
-include("logger.hrl"). -include("logger.hrl").
@ -88,3 +90,50 @@ clean(_TS) ->
path(Path) -> path(Path) ->
Base = ejabberd_option:ext_api_path_oauth(), Base = ejabberd_option:ext_api_path_oauth(),
<<Base/binary, "/", Path/binary>>. <<Base/binary, "/", Path/binary>>.
store_client(#oauth_client{client = Client,
secret = Secret,
grant_type = GrantType} = R) ->
Path = path(<<"store_client">>),
%% Retry 2 times, with a backoff of 500millisec
SGrantType =
case GrantType of
password -> <<"password">>
end,
case rest:with_retry(
post,
[ejabberd_config:get_myname(), Path, [],
{[{<<"client">>, Client},
{<<"secret">>, Secret},
{<<"grant_type">>, SGrantType},
{<<"options">>, []}
]}], 2, 500) of
{ok, Code, _} when Code == 200 orelse Code == 201 ->
ok;
Err ->
?ERROR_MSG("Failed to store oauth record ~p: ~p", [R, Err]),
{error, db_failure}
end.
lookup_client(Client) ->
Path = path(<<"lookup_client">>),
case rest:with_retry(post, [ejabberd_config:get_myname(), Path, [],
{[{<<"client">>, Client}]}],
2, 500) of
{ok, 200, {Data}} ->
Secret = proplists:get_value(<<"secret">>, Data, <<>>),
SGrantType = proplists:get_value(<<"grant_type">>, Data, <<>>),
GrantType =
case SGrantType of
<<"password">> -> password
end,
{ok, #oauth_client{client = Client,
secret = Secret,
grant_type = GrantType,
options = []}};
{ok, 404, _Resp} ->
error;
Other ->
?ERROR_MSG("Unexpected response for oauth lookup: ~p", [Other]),
error
end.

View File

@ -30,7 +30,10 @@
-export([init/0, -export([init/0,
store/1, store/1,
lookup/1, lookup/1,
clean/1]). clean/1,
lookup_client/1,
store_client/1,
remove_client/1]).
-include("ejabberd_oauth.hrl"). -include("ejabberd_oauth.hrl").
-include("ejabberd_sql_pt.hrl"). -include("ejabberd_sql_pt.hrl").
@ -80,3 +83,46 @@ clean(TS) ->
ejabberd_config:get_myname(), ejabberd_config:get_myname(),
?SQL("delete from oauth_token where expire < %(TS)d")). ?SQL("delete from oauth_token where expire < %(TS)d")).
lookup_client(Client) ->
case ejabberd_sql:sql_query(
ejabberd_config:get_myname(),
?SQL("select @(secret)s, @(grant_type)s"
" from oauth_client where client=%(Client)s")) of
{selected, [{Secret, SGrantType}]} ->
GrantType =
case SGrantType of
<<"password">> -> password
end,
{ok, #oauth_client{client = Client,
secret = Secret,
grant_type = GrantType,
options = []}};
_ ->
error
end.
store_client(#oauth_client{client = Client,
secret = Secret,
grant_type = GrantType}) ->
SGrantType =
case GrantType of
password -> <<"password">>
end,
SOptions = <<"">>,
case ?SQL_UPSERT(
ejabberd_config:get_myname(),
"oauth_client",
["!client=%(Client)s",
"secret=%(Secret)s",
"grant_type=%(SGrantType)s",
"options=%(SOptions)s"]) of
ok ->
ok;
_ ->
{error, db_failure}
end.
remove_client(Client) ->
ejabberd_sql:sql_query(
ejabberd_config:get_myname(),
?SQL("delete from oauth_client where client=%(Client)s")).

View File

@ -83,6 +83,7 @@
-export([oauth_cache_life_time/0]). -export([oauth_cache_life_time/0]).
-export([oauth_cache_missed/0]). -export([oauth_cache_missed/0]).
-export([oauth_cache_size/0]). -export([oauth_cache_size/0]).
-export([oauth_client_id_check/0, oauth_client_id_check/1]).
-export([oauth_db_type/0]). -export([oauth_db_type/0]).
-export([oauth_expire/0]). -export([oauth_expire/0]).
-export([oauth_use_cache/0]). -export([oauth_use_cache/0]).
@ -620,6 +621,13 @@ oauth_cache_missed() ->
oauth_cache_size() -> oauth_cache_size() ->
ejabberd_config:get_option({oauth_cache_size, global}). ejabberd_config:get_option({oauth_cache_size, global}).
-spec oauth_client_id_check() -> 'allow' | 'db' | 'deny'.
oauth_client_id_check() ->
oauth_client_id_check(global).
-spec oauth_client_id_check(global | binary()) -> 'allow' | 'db' | 'deny'.
oauth_client_id_check(Host) ->
ejabberd_config:get_option({oauth_client_id_check, Host}).
-spec oauth_db_type() -> atom(). -spec oauth_db_type() -> atom().
oauth_db_type() -> oauth_db_type() ->
ejabberd_config:get_option({oauth_db_type, global}). ejabberd_config:get_option({oauth_db_type, global}).

View File

@ -234,6 +234,8 @@ opt_type(oauth_expire) ->
econf:non_neg_int(); econf:non_neg_int();
opt_type(oauth_use_cache) -> opt_type(oauth_use_cache) ->
econf:bool(); econf:bool();
opt_type(oauth_client_id_check) ->
econf:enum([allow, deny, db]);
opt_type(oom_killer) -> opt_type(oom_killer) ->
econf:bool(); econf:bool();
opt_type(oom_queue) -> opt_type(oom_queue) ->
@ -546,6 +548,7 @@ options() ->
{oauth_expire, 4294967}, {oauth_expire, 4294967},
{oauth_use_cache, {oauth_use_cache,
fun(Host) -> ejabberd_config:get_option({use_cache, Host}) end}, fun(Host) -> ejabberd_config:get_option({use_cache, Host}) end},
{oauth_client_id_check, allow},
{oom_killer, true}, {oom_killer, true},
{oom_queue, 10000}, {oom_queue, 10000},
{oom_watermark, 80}, {oom_watermark, 80},