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, {
client = <<"">> :: binary() | '_',
secret = <<"">> :: binary() | '_',
grant_type = password :: password | '_',
client_id = <<"">> :: binary() | '_',
client_name = <<"">> :: binary() | '_',
grant_type :: password | implicit | '_',
options :: [any()] | '_'
}).

View File

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

View File

@ -384,6 +384,13 @@ CREATE TABLE oauth_token (
expire bigint NOT NULL
) 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 (
domain text 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;
CREATE TABLE oauth_client (
client varchar(191) NOT NULL PRIMARY KEY,
secret text NOT NULL,
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;

View File

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

View File

@ -50,7 +50,9 @@
-export([get_commands_spec/0,
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("logger.hrl").
@ -65,6 +67,11 @@
-callback lookup(binary()) -> {ok, #oauth_token{}} | error.
-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:
%% * 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
@ -99,12 +106,21 @@ get_commands_spec() ->
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,
#ejabberd_commands{name = oauth_add_client_password, tags = [oauth],
desc = "Add OAUTH client_id with password grant type",
module = ?MODULE, function = oauth_add_client_password,
args = [{client_id, binary},
{secret, binary},
{grant_type, binary}],
{client_name, 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,
result = {res, restuple}
},
@ -146,18 +162,21 @@ 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_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(),
@ -216,9 +235,23 @@ terminate(_Reason, _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) ->
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
allow ->
case Ctx of
{password, Password} ->
case ejabberd_auth:check_password(User, <<"">>, Server, Password) of
true ->
#oauth_ctx{password = admin_generated} ->
{ok, {Ctx, {user, User, Server}}};
false ->
{error, badpass}
end;
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}
@ -245,7 +279,20 @@ authenticate_user({User, Server}, Ctx) ->
{error, badpass}
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()) ->
{ok, any(), [binary()]} | {error, any()}.
@ -525,7 +572,7 @@ process(_Handlers,
ClientId,
RedirectURI,
Scope,
{password, Password}) of
#oauth_ctx{password = Password}) of
{ok, {_AppContext, Authorization}} ->
{ok, {_AppContext2, Response}} =
oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]),
@ -597,13 +644,18 @@ process(_Handlers,
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;
{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;
_ ->
unsupported_grant_type
deny
end;
_ ->
deny
@ -623,7 +675,7 @@ process(_Handlers,
end,
case oauth2:authorize_password({Username, Server},
Scope,
{password, Password}) of
#oauth_ctx{password = Password}) of
{ok, {_AppContext, Authorization}} ->
{ok, {_AppContext2, Response}} =
oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]),
@ -663,6 +715,11 @@ 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) ->

View File

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

View File

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