From 5d549dca96c643345ba92e67504e67eb1b6b0681 Mon Sep 17 00:00:00 2001 From: Alexey Shchepin Date: Thu, 3 Oct 2019 06:18:07 +0300 Subject: [PATCH] Check redirect_uri for OAUTH implicit grant --- include/ejabberd_oauth.hrl | 6 +- sql/lite.sql | 4 +- sql/mysql.new.sql | 7 ++ sql/mysql.sql | 4 +- sql/pg.new.sql | 7 ++ sql/pg.sql | 4 +- src/ejabberd_oauth.erl | 131 ++++++++++++++++++++++++++---------- src/ejabberd_oauth_rest.erl | 42 +++++++----- src/ejabberd_oauth_sql.erl | 40 ++++++----- 9 files changed, 167 insertions(+), 78 deletions(-) diff --git a/include/ejabberd_oauth.hrl b/include/ejabberd_oauth.hrl index 9254da1e5..2daee39d9 100644 --- a/include/ejabberd_oauth.hrl +++ b/include/ejabberd_oauth.hrl @@ -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()] | '_' }). diff --git a/sql/lite.sql b/sql/lite.sql index c77922c20..0b6bb12c1 100644 --- a/sql/lite.sql +++ b/sql/lite.sql @@ -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 ); diff --git a/sql/mysql.new.sql b/sql/mysql.new.sql index b2f9acfd4..15843fc7d 100644 --- a/sql/mysql.new.sql +++ b/sql/mysql.new.sql @@ -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, diff --git a/sql/mysql.sql b/sql/mysql.sql index 7f415a2e4..7afc2cf1a 100644 --- a/sql/mysql.sql +++ b/sql/mysql.sql @@ -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; diff --git a/sql/pg.new.sql b/sql/pg.new.sql index c585fd307..1a3d52316 100644 --- a/sql/pg.new.sql +++ b/sql/pg.new.sql @@ -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, diff --git a/sql/pg.sql b/sql/pg.sql index 0f87fd5d8..34d9a4867 100644 --- a/sql/pg.sql +++ b/sql/pg.sql @@ -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 ); diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 4060b4b7b..27e211f13 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -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}, + {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}, - {secret, binary}, - {grant_type, 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) -> diff --git a/src/ejabberd_oauth_rest.erl b/src/ejabberd_oauth_rest.erl index b15fc904b..6af67996b 100644 --- a/src/ejabberd_oauth_rest.erl +++ b/src/ejabberd_oauth_rest.erl @@ -91,22 +91,25 @@ path(Path) -> Base = ejabberd_option:ext_api_path_oauth(), <>. -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 -> diff --git a/src/ejabberd_oauth_sql.erl b/src/ejabberd_oauth_sql.erl index 724017af4..fb91a8813 100644 --- a/src/ejabberd_oauth_sql.erl +++ b/src/ejabberd_oauth_sql.erl @@ -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 -> -- 2.40.0