mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-24 16:23:40 +01:00
Support OAUTH client authentication
This commit is contained in:
parent
47d0eed3f1
commit
8f7fa38949
@ -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()] | '_'
|
||||||
|
}).
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,9 +570,47 @@ 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">>]}) ->
|
||||||
|
Access =
|
||||||
|
case ejabberd_option:oauth_client_id_check() of
|
||||||
|
allow ->
|
||||||
case proplists:get_value(<<"grant_type">>, Q, <<"">>) of
|
case proplists:get_value(<<"grant_type">>, Q, <<"">>) of
|
||||||
<<"password">> ->
|
<<"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{secret = Secret} = Client} ->
|
||||||
|
case proplists:get_value(<<"grant_type">>, Q, <<"">>) of
|
||||||
|
<<"password">> when
|
||||||
|
Client#oauth_client.grant_type == password ->
|
||||||
|
password;
|
||||||
|
_ ->
|
||||||
|
unsupported_grant_type
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
deny
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case Access of
|
||||||
|
password ->
|
||||||
SScope = proplists:get_value(<<"scope">>, Q, <<"">>),
|
SScope = proplists:get_value(<<"scope">>, Q, <<"">>),
|
||||||
StringJID = proplists:get_value(<<"username">>, Q, <<"">>),
|
StringJID = proplists:get_value(<<"username">>, Q, <<"">>),
|
||||||
#jid{user = Username, server = Server} = jid:decode(StringJID),
|
#jid{user = Username, server = Server} = jid:decode(StringJID),
|
||||||
@ -574,8 +647,11 @@ process(_Handlers,
|
|||||||
{error, Error} when is_atom(Error) ->
|
{error, Error} when is_atom(Error) ->
|
||||||
json_error(400, <<"invalid_grant">>, Error)
|
json_error(400, <<"invalid_grant">>, Error)
|
||||||
end;
|
end;
|
||||||
_OtherGrantType ->
|
unsupported_grant_type ->
|
||||||
json_error(400, <<"unsupported_grant_type">>, unsupported_grant_type)
|
json_error(400, <<"unsupported_grant_type">>,
|
||||||
|
unsupported_grant_type);
|
||||||
|
deny ->
|
||||||
|
ejabberd_web:error(not_allowed)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
process(_Handlers, _Request) ->
|
process(_Handlers, _Request) ->
|
||||||
|
@ -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).
|
||||||
|
@ -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.
|
||||||
|
@ -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")).
|
||||||
|
@ -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}).
|
||||||
|
@ -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},
|
||||||
|
Loading…
Reference in New Issue
Block a user