diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index b2192a781..81b5f4156 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -497,10 +497,65 @@ process(_Handlers, }], ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])} end; +process(_Handlers, + #request{method = 'POST', q = Q, lang = _Lang, + 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:from_string(StringJID), + Password = proplists:get_value(<<"password">>, Q, <<"">>), + Scope = str:tokens(SScope, <<" ">>), + TTL = proplists:get_value(<<"ttl">>, Q, <<"">>), + ExpiresIn = case TTL of + <<>> -> undefined; + _ -> jlib: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_response(400, {[ + {<<"error">>, <<"invalid_grant">>}, + {<<"error_description">>, Error}]}) + end; + _OtherGrantType -> + json_response(400, {[ + {<<"error">>, <<"unsupported_grant_type">>}]}) + end; + process(_Handlers, _Request) -> ejabberd_web:error(not_found). +%% Headers as per RFC 6749 +json_response(Code, Body) -> + {Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}, + {<<"Cache-Control">>, <<"no-store">>}, + {<<"Pragma">>, <<"no-cache">>}], + jiffy:encode(Body)}. + web_head() -> diff --git a/test/mod_http_api_mock_test.exs b/test/mod_http_api_mock_test.exs index db8761887..fcfdfee13 100644 --- a/test/mod_http_api_mock_test.exs +++ b/test/mod_http_api_mock_test.exs @@ -58,6 +58,7 @@ defmodule ModHttpApiMockTest do setup do :meck.unload :meck.new :ejabberd_commands + :meck.new(:acl, [:passthrough]) # Need to fake acl to allow oauth EjabberdAuthMock.init :ok end @@ -206,5 +207,69 @@ defmodule ModHttpApiMockTest do #assert :ok = :meck.history(:ejabberd_commands) end + test "Request oauth token, resource owner password credentials" do + EjabberdAuthMock.create_user @user, @domain, @userpass + :application.set_env(:oauth2, :backend, :ejabberd_oauth) + :application.start(:oauth2) + + # Mock a simple command() -> :ok + :meck.expect(:ejabberd_commands, :get_command_format, + fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) -> + {[], {:res, :rescode}} + end) + :meck.expect(:ejabberd_commands, :get_command_policy_and_scope, + fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end) + :meck.expect(:ejabberd_commands, :get_commands, + fn () -> [@acommand] end) + :meck.expect(:ejabberd_commands, :execute_command, + fn (:undefined, {@user, @domain, {:oauth, _token}, false}, + @acommand, [], @version, _) -> + :ok + end) + + #Mock acl to allow oauth authorizations + :meck.expect(:acl, :match_rule, fn(_Server, _Access, _Jid) -> :allow end) + + + # Correct password + req = request(method: :POST, + path: ["oauth", "token"], + q: [{"grant_type", "password"}, {"scope", @command}, {"username", @user<>"@"<>@domain}, {"ttl", "4000"}, {"password", @userpass}], + ip: {{127,0,0,1},60000}, + host: @domain) + result = :ejabberd_oauth.process([], req) + assert 200 = elem(result, 0) #http code + {kv} = :jiffy.decode(elem(result,2)) + assert {_, "bearer"} = List.keyfind(kv, "token_type", 0) + assert {_, @command} = List.keyfind(kv, "scope", 0) + assert {_, 4000} = List.keyfind(kv, "expires_in", 0) + {"access_token", _token} = List.keyfind(kv, "access_token", 0) + + #missing grant_type + req = request(method: :POST, + path: ["oauth", "token"], + q: [{"scope", @command}, {"username", @user<>"@"<>@domain}, {"password", @userpass}], + ip: {{127,0,0,1},60000}, + host: @domain) + result = :ejabberd_oauth.process([], req) + assert 400 = elem(result, 0) #http code + {kv} = :jiffy.decode(elem(result,2)) + assert {_, "unsupported_grant_type"} = List.keyfind(kv, "error", 0) + + + # incorrect user/pass + req = request(method: :POST, + path: ["oauth", "token"], + q: [{"grant_type", "password"}, {"scope", @command}, {"username", @user<>"@"<>@domain}, {"password", @userpass<>"aa"}], + ip: {{127,0,0,1},60000}, + host: @domain) + result = :ejabberd_oauth.process([], req) + assert 400 = elem(result, 0) #http code + {kv} = :jiffy.decode(elem(result,2)) + assert {_, "invalid_grant"} = List.keyfind(kv, "error", 0) + + assert :meck.validate :ejabberd_auth + assert :meck.validate :ejabberd_commands + end end