25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-12-22 17:28:25 +01:00

Check redirect_uri for OAUTH implicit grant

This commit is contained in:
Alexey Shchepin 2019-10-03 06:18:07 +03:00
parent 949e71efb6
commit 5d549dca96
9 changed files with 167 additions and 78 deletions

View File

@ -26,8 +26,8 @@
}). }).
-record(oauth_client, { -record(oauth_client, {
client = <<"">> :: binary() | '_', client_id = <<"">> :: binary() | '_',
secret = <<"">> :: binary() | '_', client_name = <<"">> :: binary() | '_',
grant_type = password :: password | '_', grant_type :: password | implicit | '_',
options :: [any()] | '_' options :: [any()] | '_'
}). }).

View File

@ -339,8 +339,8 @@ CREATE TABLE oauth_token (
); );
CREATE TABLE oauth_client ( CREATE TABLE oauth_client (
client text PRIMARY KEY, client_id text PRIMARY KEY,
secret text NOT NULL, client_name text NOT NULL,
grant_type text NOT NULL, grant_type text NOT NULL,
options text NOT NULL options text NOT NULL
); );

View File

@ -384,6 +384,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_id varchar(191) NOT NULL PRIMARY KEY,
client_name 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 varchar(191) NOT NULL, server_host varchar(191) NOT NULL,

View File

@ -355,8 +355,8 @@ CREATE TABLE oauth_token (
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE oauth_client ( CREATE TABLE oauth_client (
client varchar(191) NOT NULL PRIMARY KEY, client_id varchar(191) NOT NULL PRIMARY KEY,
secret text NOT NULL, client_name text NOT NULL,
grant_type text NOT NULL, grant_type text NOT NULL,
options text NOT NULL options text NOT NULL
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@ -529,6 +529,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_id text PRIMARY KEY,
client_name 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

@ -359,8 +359,8 @@ 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 ( CREATE TABLE oauth_client (
client text PRIMARY KEY, client_id text PRIMARY KEY,
secret text NOT NULL, client_name text NOT NULL,
grant_type text NOT NULL, grant_type text NOT NULL,
options text NOT NULL options text NOT NULL
); );

View File

@ -50,7 +50,9 @@
-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]). oauth_add_client_password/3,
oauth_add_client_implicit/3,
oauth_remove_client/1]).
-include("xmpp.hrl"). -include("xmpp.hrl").
-include("logger.hrl"). -include("logger.hrl").
@ -65,6 +67,11 @@
-callback lookup(binary()) -> {ok, #oauth_token{}} | error. -callback lookup(binary()) -> {ok, #oauth_token{}} | error.
-callback clean(non_neg_integer()) -> any(). -callback clean(non_neg_integer()) -> any().
-record(oauth_ctx, {
password :: binary() | admin_generated,
client :: #oauth_client{}
}).
%% There are two ways to obtain an oauth token: %% 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 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 %% * Using the command line and oauth_issue_token command, the token is generated in behalf of ejabberd' sysadmin
@ -99,12 +106,21 @@ get_commands_spec() ->
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], #ejabberd_commands{name = oauth_add_client_password, tags = [oauth],
desc = "Add OAUTH client_id", desc = "Add OAUTH client_id with password grant type",
module = ?MODULE, function = oauth_add_client, module = ?MODULE, function = oauth_add_client_password,
args = [{client_id, binary}, args = [{client_id, binary},
{secret, binary}, {client_name, binary},
{grant_type, binary}], {secret, binary}],
policy = restricted,
result = {res, restuple}
},
#ejabberd_commands{name = oauth_add_client_implicit, tags = [oauth],
desc = "Add 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, policy = restricted,
result = {res, restuple} result = {res, restuple}
}, },
@ -146,18 +162,21 @@ 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) -> oauth_add_client_password(ClientID, ClientName, Secret) ->
case SGrantType of DBMod = get_db_backend(),
<<"password">> -> DBMod:store_client(#oauth_client{client_id = ClientID,
DBMod = get_db_backend(), client_name = ClientName,
DBMod:store_client(#oauth_client{client = Client, grant_type = password,
secret = Secret, options = [{secret, Secret}]}),
grant_type = password, {ok, []}.
options = []}),
{ok, []}; oauth_add_client_implicit(ClientID, ClientName, RedirectURI) ->
_ -> DBMod = get_db_backend(),
{error, "Unsupported grant type"} DBMod:store_client(#oauth_client{client_id = ClientID,
end. client_name = ClientName,
grant_type = implicit,
options = [{redirect_uri, RedirectURI}]}),
{ok, []}.
oauth_remove_client(Client) -> oauth_remove_client(Client) ->
DBMod = get_db_backend(), DBMod = get_db_backend(),
@ -216,9 +235,23 @@ terminate(_Reason, _State) ->
code_change(_OldVsn, State, _Extra) -> {ok, State}. code_change(_OldVsn, State, _Extra) -> {ok, State}.
get_client_identity(Client, Ctx) -> {ok, {Ctx, {client, Client}}}. get_client_identity({client, ClientID}, Ctx) ->
{ok, {Ctx, {client, ClientID}}}.
verify_redirection_uri(_, _, Ctx) -> {ok, Ctx}. 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) -> authenticate_user({User, Server}, Ctx) ->
case jid:make(User, Server) of case jid:make(User, Server) of
@ -228,15 +261,16 @@ authenticate_user({User, Server}, Ctx) ->
case acl:match_rule(JID#jid.lserver, Access, JID) of case acl:match_rule(JID#jid.lserver, Access, JID) of
allow -> allow ->
case Ctx of case Ctx of
{password, Password} -> #oauth_ctx{password = admin_generated} ->
case ejabberd_auth:check_password(User, <<"">>, Server, Password) of
true ->
{ok, {Ctx, {user, User, Server}}}; {ok, {Ctx, {user, User, Server}}};
false -> #oauth_ctx{password = Password}
{error, badpass} when is_binary(Password) ->
end; case ejabberd_auth:check_password(User, <<"">>, Server, Password) of
admin_generated -> true ->
{ok, {Ctx, {user, User, Server}}} {ok, {Ctx, {user, User, Server}}};
false ->
{error, badpass}
end
end; end;
deny -> deny ->
{error, badpass} {error, badpass}
@ -245,7 +279,20 @@ authenticate_user({User, Server}, Ctx) ->
{error, badpass} {error, badpass}
end. end.
authenticate_client(Client, Ctx) -> {ok, {Ctx, {client, Client}}}. 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()) -> -spec verify_resowner_scope({user, binary(), binary()}, [binary()], any()) ->
{ok, any(), [binary()]} | {error, any()}. {ok, any(), [binary()]} | {error, any()}.
@ -525,7 +572,7 @@ process(_Handlers,
ClientId, ClientId,
RedirectURI, RedirectURI,
Scope, Scope,
{password, Password}) of #oauth_ctx{password = Password}) of
{ok, {_AppContext, Authorization}} -> {ok, {_AppContext, Authorization}} ->
{ok, {_AppContext2, Response}} = {ok, {_AppContext2, Response}} =
oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]), oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]),
@ -597,13 +644,18 @@ process(_Handlers,
end, end,
DBMod = get_db_backend(), DBMod = get_db_backend(),
case DBMod:lookup_client(ClientID) of case DBMod:lookup_client(ClientID) of
{ok, #oauth_client{secret = Secret} = Client} -> {ok, #oauth_client{grant_type = password} = Client} ->
case proplists:get_value(<<"grant_type">>, Q, <<"">>) of case get_client_secret(Client) of
<<"password">> when Secret ->
Client#oauth_client.grant_type == password -> case proplists:get_value(<<"grant_type">>, Q, <<"">>) of
password; <<"password">> when
Client#oauth_client.grant_type == password ->
password;
_ ->
unsupported_grant_type
end;
_ -> _ ->
unsupported_grant_type deny
end; end;
_ -> _ ->
deny deny
@ -623,7 +675,7 @@ process(_Handlers,
end, end,
case oauth2:authorize_password({Username, Server}, case oauth2:authorize_password({Username, Server},
Scope, Scope,
{password, Password}) of #oauth_ctx{password = Password}) of
{ok, {_AppContext, Authorization}} -> {ok, {_AppContext, Authorization}} ->
{ok, {_AppContext2, Response}} = {ok, {_AppContext2, Response}} =
oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]), oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]),
@ -663,6 +715,11 @@ get_db_backend() ->
DBType = ejabberd_option:oauth_db_type(), DBType = ejabberd_option:oauth_db_type(),
list_to_existing_atom("ejabberd_oauth_" ++ atom_to_list(DBType)). 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 %% Headers as per RFC 6749
json_response(Code, Body) -> json_response(Code, Body) ->

View File

@ -91,22 +91,25 @@ 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, store_client(#oauth_client{client_id = ClientID,
secret = Secret, client_name = ClientName,
grant_type = GrantType} = R) -> grant_type = GrantType,
options = Options} = R) ->
Path = path(<<"store_client">>), Path = path(<<"store_client">>),
%% Retry 2 times, with a backoff of 500millisec
SGrantType = SGrantType =
case GrantType of case GrantType of
password -> <<"password">> password -> <<"password">>;
implicit -> <<"implicit">>
end, end,
SOptions = misc:term_to_base64(Options),
%% Retry 2 times, with a backoff of 500millisec
case rest:with_retry( case rest:with_retry(
post, post,
[ejabberd_config:get_myname(), Path, [], [ejabberd_config:get_myname(), Path, [],
{[{<<"client">>, Client}, {[{<<"client_id">>, ClientID},
{<<"secret">>, Secret}, {<<"client_name">>, ClientName},
{<<"grant_type">>, SGrantType}, {<<"grant_type">>, SGrantType},
{<<"options">>, []} {<<"options">>, SOptions}
]}], 2, 500) of ]}], 2, 500) of
{ok, Code, _} when Code == 200 orelse Code == 201 -> {ok, Code, _} when Code == 200 orelse Code == 201 ->
ok; ok;
@ -115,22 +118,29 @@ store_client(#oauth_client{client = Client,
{error, db_failure} {error, db_failure}
end. end.
lookup_client(Client) -> lookup_client(ClientID) ->
Path = path(<<"lookup_client">>), Path = path(<<"lookup_client">>),
case rest:with_retry(post, [ejabberd_config:get_myname(), Path, [], case rest:with_retry(post, [ejabberd_config:get_myname(), Path, [],
{[{<<"client">>, Client}]}], {[{<<"client_id">>, ClientID}]}],
2, 500) of 2, 500) of
{ok, 200, {Data}} -> {ok, 200, {Data}} ->
Secret = proplists:get_value(<<"secret">>, Data, <<>>), ClientName = proplists:get_value(<<"client_name">>, Data, <<>>),
SGrantType = proplists:get_value(<<"grant_type">>, Data, <<>>), SGrantType = proplists:get_value(<<"grant_type">>, Data, <<>>),
GrantType = GrantType =
case SGrantType of case SGrantType of
<<"password">> -> password <<"password">> -> password;
<<"implicit">> -> implicit
end, end,
{ok, #oauth_client{client = Client, SOptions = proplists:get_value(<<"options">>, Data, <<>>),
secret = Secret, case misc:base64_to_term(SOptions) of
grant_type = GrantType, {term, Options} ->
options = []}}; {ok, #oauth_client{client_id = ClientID,
client_name = ClientName,
grant_type = GrantType,
options = Options}};
_ ->
error
end;
{ok, 404, _Resp} -> {ok, 404, _Resp} ->
error; error;
Other -> Other ->

View File

@ -83,37 +83,45 @@ 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) -> lookup_client(ClientID) ->
case ejabberd_sql:sql_query( case ejabberd_sql:sql_query(
ejabberd_config:get_myname(), ejabberd_config:get_myname(),
?SQL("select @(secret)s, @(grant_type)s" ?SQL("select @(client_name)s, @(grant_type)s, @(options)s"
" from oauth_client where client=%(Client)s")) of " from oauth_client where client_id=%(ClientID)s")) of
{selected, [{Secret, SGrantType}]} -> {selected, [{ClientName, SGrantType, SOptions}]} ->
GrantType = GrantType =
case SGrantType of case SGrantType of
<<"password">> -> password <<"password">> -> password;
<<"implicit">> -> implicit
end, end,
{ok, #oauth_client{client = Client, case misc:base64_to_term(SOptions) of
secret = Secret, {term, Options} ->
grant_type = GrantType, {ok, #oauth_client{client_id = ClientID,
options = []}}; client_name = ClientName,
grant_type = GrantType,
options = Options}};
_ ->
error
end;
_ -> _ ->
error error
end. end.
store_client(#oauth_client{client = Client, store_client(#oauth_client{client_id = ClientID,
secret = Secret, client_name = ClientName,
grant_type = GrantType}) -> grant_type = GrantType,
options = Options}) ->
SGrantType = SGrantType =
case GrantType of case GrantType of
password -> <<"password">> password -> <<"password">>;
implicit -> <<"implicit">>
end, end,
SOptions = <<"">>, SOptions = misc:term_to_base64(Options),
case ?SQL_UPSERT( case ?SQL_UPSERT(
ejabberd_config:get_myname(), ejabberd_config:get_myname(),
"oauth_client", "oauth_client",
["!client=%(Client)s", ["!client_id=%(ClientID)s",
"secret=%(Secret)s", "client_name=%(ClientName)s",
"grant_type=%(SGrantType)s", "grant_type=%(SGrantType)s",
"options=%(SOptions)s"]) of "options=%(SOptions)s"]) of
ok -> ok ->