]> granicus.if.org Git - ejabberd/commitdiff
Support oauth password grant type
authorPablo Polvorin <pablo@work>
Fri, 22 Jul 2016 22:17:12 +0000 (19:17 -0300)
committerPablo Polvorin <pablo@work>
Fri, 22 Jul 2016 22:17:12 +0000 (19:17 -0300)
As in https://tools.ietf.org/html/rfc6749#section-4.3

src/ejabberd_oauth.erl
test/mod_http_api_mock_test.exs

index b2192a78147d37b5ba72e8577098b0fbdb3acfe6..81b5f4156bdfdfd68e2e4968c7908d577ef5cbd6 100644 (file)
@@ -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() ->
index db8761887f1517ede51ea6b20deb6d35516bb97a..fcfdfee13d6b7f268be76a7a15745e962c57b021 100644 (file)
@@ -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