summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexey Shchepin <alexey@process-one.net>2019-09-27 20:36:35 +0300
committerAlexey Shchepin <alexey@process-one.net>2019-09-27 20:36:58 +0300
commit8f7fa3894902ed2181b45723388ee9087b92c18a (patch)
treeb4d448d361f7de5ea3886592a840a6b7d252e73e
parentAdvertise muc#roominfo_logs in disco#info of the room (diff)
Support OAUTH client authentication
Diffstat (limited to '')
-rw-r--r--include/ejabberd_oauth.hrl7
-rw-r--r--sql/lite.sql7
-rw-r--r--sql/mysql.sql7
-rw-r--r--sql/pg.sql7
-rw-r--r--src/ejabberd_oauth.erl158
-rw-r--r--src/ejabberd_oauth_mnesia.erl21
-rw-r--r--src/ejabberd_oauth_rest.erl51
-rw-r--r--src/ejabberd_oauth_sql.erl48
-rw-r--r--src/ejabberd_option.erl8
-rw-r--r--src/ejabberd_options.erl3
10 files changed, 274 insertions, 43 deletions
diff --git a/include/ejabberd_oauth.hrl b/include/ejabberd_oauth.hrl
index 51e77636..9254da1e 100644
--- a/include/ejabberd_oauth.hrl
+++ b/include/ejabberd_oauth.hrl
@@ -24,3 +24,10 @@
scope = [] :: [binary()] | '_',
expire :: integer() | '$1' | '_'
}).
+
+-record(oauth_client, {
+ client = <<"">> :: binary() | '_',
+ secret = <<"">> :: binary() | '_',
+ grant_type = password :: password | '_',
+ options :: [any()] | '_'
+ }).
diff --git a/sql/lite.sql b/sql/lite.sql
index e8755029..c77922c2 100644
--- a/sql/lite.sql
+++ b/sql/lite.sql
@@ -338,6 +338,13 @@ CREATE TABLE oauth_token (
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 (
domain text NOT NULL,
server_host text NOT NULL,
diff --git a/sql/mysql.sql b/sql/mysql.sql
index a05f8c86..7f415a2e 100644
--- a/sql/mysql.sql
+++ b/sql/mysql.sql
@@ -354,6 +354,13 @@ CREATE TABLE oauth_token (
expire bigint NOT NULL
) 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 (
domain text NOT NULL,
server_host text NOT NULL,
diff --git a/sql/pg.sql b/sql/pg.sql
index eae98d3f..0f87fd5d 100644
--- a/sql/pg.sql
+++ b/sql/pg.sql
@@ -358,6 +358,13 @@ CREATE TABLE oauth_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 (
domain text NOT NULL,
server_host text NOT NULL,
diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl
index d9b16c70..4060b4b7 100644
--- a/src/ejabberd_oauth.erl
+++ b/src/ejabberd_oauth.erl
@@ -49,7 +49,8 @@
verify_resowner_scope/3]).
-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("logger.hrl").
@@ -97,6 +98,22 @@ get_commands_spec() ->
policy = restricted,
result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}},
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)),
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() ->
DBMod = get_db_backend(),
case init_cache(DBMod) of
@@ -535,48 +570,89 @@ process(_Handlers,
end;
process(_Handlers,
#request{method = 'POST', q = Q, lang = _Lang,
+ auth = HTTPAuth,
path = [_, <<"token">>]}) ->
- case proplists:get_value(<<"grant_type">>, Q, <<"">>) 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)
+ 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,
- case oauth2:authorize_password({Username, Server},
- Scope,
- {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;
- _OtherGrantType ->
- json_error(400, <<"unsupported_grant_type">>, unsupported_grant_type)
- 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, <<"">>),
+ 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,
+ {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).
diff --git a/src/ejabberd_oauth_mnesia.erl b/src/ejabberd_oauth_mnesia.erl
index dcc70505..de851f1e 100644
--- a/src/ejabberd_oauth_mnesia.erl
+++ b/src/ejabberd_oauth_mnesia.erl
@@ -31,6 +31,9 @@
store/1,
lookup/1,
clean/1,
+ lookup_client/1,
+ store_client/1,
+ remove_client/1,
use_cache/0]).
-include("ejabberd_oauth.hrl").
@@ -40,6 +43,10 @@ init() ->
[{disc_only_copies, [node()]},
{attributes,
record_info(fields, oauth_token)}]),
+ ejabberd_mnesia:create(?MODULE, oauth_client,
+ [{disc_copies, [node()]},
+ {attributes,
+ record_info(fields, oauth_client)}]),
ok.
use_cache() ->
@@ -71,3 +78,17 @@ clean(TS) ->
lists:foreach(fun mnesia:delete_object/1, Ts)
end,
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).
diff --git a/src/ejabberd_oauth_rest.erl b/src/ejabberd_oauth_rest.erl
index 8ebfecf5..b15fc904 100644
--- a/src/ejabberd_oauth_rest.erl
+++ b/src/ejabberd_oauth_rest.erl
@@ -30,7 +30,9 @@
-export([init/0,
store/1,
lookup/1,
- clean/1]).
+ clean/1,
+ lookup_client/1,
+ store_client/1]).
-include("ejabberd_oauth.hrl").
-include("logger.hrl").
@@ -88,3 +90,50 @@ clean(_TS) ->
path(Path) ->
Base = ejabberd_option:ext_api_path_oauth(),
<<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.
diff --git a/src/ejabberd_oauth_sql.erl b/src/ejabberd_oauth_sql.erl
index 8ce2bc57..724017af 100644
--- a/src/ejabberd_oauth_sql.erl
+++ b/src/ejabberd_oauth_sql.erl
@@ -30,7 +30,10 @@
-export([init/0,
store/1,
lookup/1,
- clean/1]).
+ clean/1,
+ lookup_client/1,
+ store_client/1,
+ remove_client/1]).
-include("ejabberd_oauth.hrl").
-include("ejabberd_sql_pt.hrl").
@@ -80,3 +83,46 @@ clean(TS) ->
ejabberd_config:get_myname(),
?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")).
diff --git a/src/ejabberd_option.erl b/src/ejabberd_option.erl
index e559db20..94e79d80 100644
--- a/src/ejabberd_option.erl
+++ b/src/ejabberd_option.erl
@@ -83,6 +83,7 @@
-export([oauth_cache_life_time/0]).
-export([oauth_cache_missed/0]).
-export([oauth_cache_size/0]).
+-export([oauth_client_id_check/0, oauth_client_id_check/1]).
-export([oauth_db_type/0]).
-export([oauth_expire/0]).
-export([oauth_use_cache/0]).
@@ -620,6 +621,13 @@ oauth_cache_missed() ->
oauth_cache_size() ->
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().
oauth_db_type() ->
ejabberd_config:get_option({oauth_db_type, global}).
diff --git a/src/ejabberd_options.erl b/src/ejabberd_options.erl
index 8468d181..7bfa06ee 100644
--- a/src/ejabberd_options.erl
+++ b/src/ejabberd_options.erl
@@ -234,6 +234,8 @@ opt_type(oauth_expire) ->
econf:non_neg_int();
opt_type(oauth_use_cache) ->
econf:bool();
+opt_type(oauth_client_id_check) ->
+ econf:enum([allow, deny, db]);
opt_type(oom_killer) ->
econf:bool();
opt_type(oom_queue) ->
@@ -546,6 +548,7 @@ options() ->
{oauth_expire, 4294967},
{oauth_use_cache,
fun(Host) -> ejabberd_config:get_option({use_cache, Host}) end},
+ {oauth_client_id_check, allow},
{oom_killer, true},
{oom_queue, 10000},
{oom_watermark, 80},