25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-11-20 16:15:59 +01:00

Support oauth password grant type

As in https://tools.ietf.org/html/rfc6749#section-4.3
This commit is contained in:
Pablo Polvorin 2016-07-22 19:17:12 -03:00
parent 57aeef74d5
commit 4332dddbc4
2 changed files with 120 additions and 0 deletions

View File

@ -497,10 +497,65 @@ process(_Handlers,
}], }],
ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])} ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])}
end; 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) -> process(_Handlers, _Request) ->
ejabberd_web:error(not_found). 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() -> web_head() ->

View File

@ -58,6 +58,7 @@ defmodule ModHttpApiMockTest do
setup do setup do
:meck.unload :meck.unload
:meck.new :ejabberd_commands :meck.new :ejabberd_commands
:meck.new(:acl, [:passthrough]) # Need to fake acl to allow oauth
EjabberdAuthMock.init EjabberdAuthMock.init
:ok :ok
end end
@ -206,5 +207,69 @@ defmodule ModHttpApiMockTest do
#assert :ok = :meck.history(:ejabberd_commands) #assert :ok = :meck.history(:ejabberd_commands)
end 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 end