1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.OAuth.OAuthControllerTest do
6 use Pleroma.Web.ConnCase
10 alias Pleroma.Registration
12 alias Pleroma.Web.OAuth.Authorization
13 alias Pleroma.Web.OAuth.Token
15 @oauth_config_path [:oauth2, :issue_new_refresh_token]
19 signing_salt: "cooldude"
22 describe "in OAuth consumer mode, " do
24 oauth_consumer_strategies_path = [:auth, :oauth_consumer_strategies]
25 oauth_consumer_strategies = Pleroma.Config.get(oauth_consumer_strategies_path)
26 Pleroma.Config.put(oauth_consumer_strategies_path, ~w(twitter facebook))
29 Pleroma.Config.put(oauth_consumer_strategies_path, oauth_consumer_strategies)
33 app: insert(:oauth_app),
36 |> Plug.Session.call(Plug.Session.init(@session_opts))
41 test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{
50 "response_type" => "code",
51 "client_id" => app.client_id,
52 "redirect_uri" => app.redirect_uris,
57 assert response = html_response(conn, 200)
58 assert response =~ "Sign in with Twitter"
59 assert response =~ o_auth_path(conn, :prepare_request)
62 test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{
69 "/oauth/prepare_request",
71 "provider" => "twitter",
73 "scope" => "read follow",
74 "client_id" => app.client_id,
75 "redirect_uri" => app.redirect_uris,
81 assert response = html_response(conn, 302)
83 redirect_query = URI.parse(redirected_to(conn)).query
84 assert %{"state" => state_param} = URI.decode_query(redirect_query)
85 assert {:ok, state_components} = Poison.decode(state_param)
87 expected_client_id = app.client_id
88 expected_redirect_uri = app.redirect_uris
91 "scope" => "read follow",
92 "client_id" => ^expected_client_id,
93 "redirect_uri" => ^expected_redirect_uri,
98 test "with user-bound registration, GET /oauth/<provider>/callback redirects to `redirect_uri` with `code`",
99 %{app: app, conn: conn} do
100 registration = insert(:registration)
103 "scope" => Enum.join(app.scopes, " "),
104 "client_id" => app.client_id,
105 "redirect_uri" => app.redirect_uris,
109 with_mock Pleroma.Web.Auth.Authenticator,
110 get_registration: fn _ -> {:ok, registration} end do
114 "/oauth/twitter/callback",
116 "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
117 "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
118 "provider" => "twitter",
119 "state" => Poison.encode!(state_params)
123 assert response = html_response(conn, 302)
124 assert redirected_to(conn) =~ ~r/#{app.redirect_uris}\?code=.+/
128 test "with user-unbound registration, GET /oauth/<provider>/callback renders registration_details page",
129 %{app: app, conn: conn} do
130 registration = insert(:registration, user: nil)
133 "scope" => "read write",
134 "client_id" => app.client_id,
135 "redirect_uri" => app.redirect_uris,
139 with_mock Pleroma.Web.Auth.Authenticator,
140 get_registration: fn _ -> {:ok, registration} end do
144 "/oauth/twitter/callback",
146 "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
147 "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
148 "provider" => "twitter",
149 "state" => Poison.encode!(state_params)
153 assert response = html_response(conn, 200)
154 assert response =~ ~r/name="op" type="submit" value="register"/
155 assert response =~ ~r/name="op" type="submit" value="connect"/
156 assert response =~ Registration.email(registration)
157 assert response =~ Registration.nickname(registration)
161 test "on authentication error, GET /oauth/<provider>/callback redirects to `redirect_uri`", %{
166 "scope" => Enum.join(app.scopes, " "),
167 "client_id" => app.client_id,
168 "redirect_uri" => app.redirect_uris,
174 |> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]})
176 "/oauth/twitter/callback",
178 "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
179 "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
180 "provider" => "twitter",
181 "state" => Poison.encode!(state_params)
185 assert response = html_response(conn, 302)
186 assert redirected_to(conn) == app.redirect_uris
187 assert get_flash(conn, :error) == "Failed to authenticate: (error description)."
190 test "GET /oauth/registration_details renders registration details form", %{
197 "/oauth/registration_details",
199 "authorization" => %{
200 "scopes" => app.scopes,
201 "client_id" => app.client_id,
202 "redirect_uri" => app.redirect_uris,
203 "state" => "a_state",
205 "email" => "john@doe.com"
210 assert response = html_response(conn, 200)
211 assert response =~ ~r/name="op" type="submit" value="register"/
212 assert response =~ ~r/name="op" type="submit" value="connect"/
215 test "with valid params, POST /oauth/register?op=register redirects to `redirect_uri` with `code`",
220 registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
224 |> put_session(:registration_id, registration.id)
229 "authorization" => %{
230 "scopes" => app.scopes,
231 "client_id" => app.client_id,
232 "redirect_uri" => app.redirect_uris,
233 "state" => "a_state",
234 "nickname" => "availablenick",
235 "email" => "available@email.com"
240 assert response = html_response(conn, 302)
241 assert redirected_to(conn) =~ ~r/#{app.redirect_uris}\?code=.+/
244 test "with invalid params, POST /oauth/register?op=register renders registration_details page",
249 another_user = insert(:user)
250 registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
254 "authorization" => %{
255 "scopes" => app.scopes,
256 "client_id" => app.client_id,
257 "redirect_uri" => app.redirect_uris,
258 "state" => "a_state",
259 "nickname" => "availablenickname",
260 "email" => "available@email.com"
264 for {bad_param, bad_param_value} <-
265 [{"nickname", another_user.nickname}, {"email", another_user.email}] do
266 bad_registration_attrs = %{
267 "authorization" => Map.put(params["authorization"], bad_param, bad_param_value)
270 bad_params = Map.merge(params, bad_registration_attrs)
274 |> put_session(:registration_id, registration.id)
275 |> post("/oauth/register", bad_params)
277 assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/
278 assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken."
282 test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`",
287 user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt("testpassword"))
288 registration = insert(:registration, user: nil)
292 |> put_session(:registration_id, registration.id)
297 "authorization" => %{
298 "scopes" => app.scopes,
299 "client_id" => app.client_id,
300 "redirect_uri" => app.redirect_uris,
301 "state" => "a_state",
302 "name" => user.nickname,
303 "password" => "testpassword"
308 assert response = html_response(conn, 302)
309 assert redirected_to(conn) =~ ~r/#{app.redirect_uris}\?code=.+/
312 test "with invalid params, POST /oauth/register?op=connect renders registration_details page",
318 registration = insert(:registration, user: nil)
322 "authorization" => %{
323 "scopes" => app.scopes,
324 "client_id" => app.client_id,
325 "redirect_uri" => app.redirect_uris,
326 "state" => "a_state",
327 "name" => user.nickname,
328 "password" => "wrong password"
334 |> put_session(:registration_id, registration.id)
335 |> post("/oauth/register", params)
337 assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/
338 assert get_flash(conn, :error) == "Invalid Username/Password"
342 describe "GET /oauth/authorize" do
345 app: insert(:oauth_app, redirect_uris: "https://redirect.url"),
348 |> Plug.Session.call(Plug.Session.init(@session_opts))
353 test "renders authentication page", %{app: app, conn: conn} do
359 "response_type" => "code",
360 "client_id" => app.client_id,
361 "redirect_uri" => app.redirect_uris,
366 assert html_response(conn, 200) =~ ~s(type="submit")
369 test "properly handles internal calls with `authorization`-wrapped params", %{
378 "authorization" => %{
379 "response_type" => "code",
380 "client_id" => app.client_id,
381 "redirect_uri" => app.redirect_uris,
387 assert html_response(conn, 200) =~ ~s(type="submit")
390 test "renders authentication page if user is already authenticated but `force_login` is tru-ish",
391 %{app: app, conn: conn} do
392 token = insert(:oauth_token, app_id: app.id)
396 |> put_session(:oauth_token, token.token)
400 "response_type" => "code",
401 "client_id" => app.client_id,
402 "redirect_uri" => app.redirect_uris,
404 "force_login" => "true"
408 assert html_response(conn, 200) =~ ~s(type="submit")
411 test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params",
416 token = insert(:oauth_token, app_id: app.id)
420 |> put_session(:oauth_token, token.token)
424 "response_type" => "code",
425 "client_id" => app.client_id,
426 "redirect_uri" => app.redirect_uris,
427 "state" => "specific_client_state",
432 assert URI.decode(redirected_to(conn)) ==
433 "https://redirect.url?access_token=#{token.token}&state=specific_client_state"
436 test "with existing authentication and OOB `redirect_uri`, redirects to app with `token` and `state` params",
441 token = insert(:oauth_token, app_id: app.id)
445 |> put_session(:oauth_token, token.token)
449 "response_type" => "code",
450 "client_id" => app.client_id,
451 "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
456 assert html_response(conn, 200) =~ "Authorization exists"
460 describe "POST /oauth/authorize" do
461 test "redirects with oauth authorization" do
463 app = insert(:oauth_app, scopes: ["read", "write", "follow"])
467 |> post("/oauth/authorize", %{
468 "authorization" => %{
469 "name" => user.nickname,
470 "password" => "test",
471 "client_id" => app.client_id,
472 "redirect_uri" => app.redirect_uris,
473 "scope" => "read write",
474 "state" => "statepassed"
478 target = redirected_to(conn)
479 assert target =~ app.redirect_uris
481 query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
483 assert %{"state" => "statepassed", "code" => code} = query
484 auth = Repo.get_by(Authorization, token: code)
486 assert auth.scopes == ["read", "write"]
489 test "returns 401 for wrong credentials", %{conn: conn} do
491 app = insert(:oauth_app)
495 |> post("/oauth/authorize", %{
496 "authorization" => %{
497 "name" => user.nickname,
498 "password" => "wrong",
499 "client_id" => app.client_id,
500 "redirect_uri" => app.redirect_uris,
501 "state" => "statepassed",
502 "scope" => Enum.join(app.scopes, " ")
505 |> html_response(:unauthorized)
508 assert result =~ app.client_id
509 assert result =~ app.redirect_uris
512 assert result =~ "Invalid Username/Password"
515 test "returns 401 for missing scopes", %{conn: conn} do
517 app = insert(:oauth_app)
521 |> post("/oauth/authorize", %{
522 "authorization" => %{
523 "name" => user.nickname,
524 "password" => "test",
525 "client_id" => app.client_id,
526 "redirect_uri" => app.redirect_uris,
527 "state" => "statepassed",
531 |> html_response(:unauthorized)
534 assert result =~ app.client_id
535 assert result =~ app.redirect_uris
538 assert result =~ "This action is outside the authorized scopes"
541 test "returns 401 for scopes beyond app scopes", %{conn: conn} do
543 app = insert(:oauth_app, scopes: ["read", "write"])
547 |> post("/oauth/authorize", %{
548 "authorization" => %{
549 "name" => user.nickname,
550 "password" => "test",
551 "client_id" => app.client_id,
552 "redirect_uri" => app.redirect_uris,
553 "state" => "statepassed",
554 "scope" => "read write follow"
557 |> html_response(:unauthorized)
560 assert result =~ app.client_id
561 assert result =~ app.redirect_uris
564 assert result =~ "This action is outside the authorized scopes"
568 describe "POST /oauth/token" do
569 test "issues a token for an all-body request" do
571 app = insert(:oauth_app, scopes: ["read", "write"])
573 {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
577 |> post("/oauth/token", %{
578 "grant_type" => "authorization_code",
579 "code" => auth.token,
580 "redirect_uri" => app.redirect_uris,
581 "client_id" => app.client_id,
582 "client_secret" => app.client_secret
585 assert %{"access_token" => token, "me" => ap_id} = json_response(conn, 200)
587 token = Repo.get_by(Token, token: token)
589 assert token.scopes == auth.scopes
590 assert user.ap_id == ap_id
593 test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do
594 password = "testpassword"
595 user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
597 app = insert(:oauth_app, scopes: ["read", "write"])
599 # Note: "scope" param is intentionally omitted
602 |> post("/oauth/token", %{
603 "grant_type" => "password",
604 "username" => user.nickname,
605 "password" => password,
606 "client_id" => app.client_id,
607 "client_secret" => app.client_secret
610 assert %{"access_token" => token} = json_response(conn, 200)
612 token = Repo.get_by(Token, token: token)
614 assert token.scopes == app.scopes
617 test "issues a token for request with HTTP basic auth client credentials" do
619 app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"])
621 {:ok, auth} = Authorization.create_authorization(app, user, ["scope1", "scope2"])
622 assert auth.scopes == ["scope1", "scope2"]
625 (URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret))
630 |> put_req_header("authorization", "Basic " <> app_encoded)
631 |> post("/oauth/token", %{
632 "grant_type" => "authorization_code",
633 "code" => auth.token,
634 "redirect_uri" => app.redirect_uris
637 assert %{"access_token" => token, "scope" => scope} = json_response(conn, 200)
639 assert scope == "scope1 scope2"
641 token = Repo.get_by(Token, token: token)
643 assert token.scopes == ["scope1", "scope2"]
646 test "issue a token for client_credentials grant type" do
647 app = insert(:oauth_app, scopes: ["read", "write"])
651 |> post("/oauth/token", %{
652 "grant_type" => "client_credentials",
653 "client_id" => app.client_id,
654 "client_secret" => app.client_secret
657 assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
658 json_response(conn, 200)
661 token_from_db = Repo.get_by(Token, token: token)
664 assert scope == "read write"
667 test "rejects token exchange with invalid client credentials" do
669 app = insert(:oauth_app)
671 {:ok, auth} = Authorization.create_authorization(app, user)
675 |> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=")
676 |> post("/oauth/token", %{
677 "grant_type" => "authorization_code",
678 "code" => auth.token,
679 "redirect_uri" => app.redirect_uris
682 assert resp = json_response(conn, 400)
683 assert %{"error" => _} = resp
684 refute Map.has_key?(resp, "access_token")
687 test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do
688 setting = Pleroma.Config.get([:instance, :account_activation_required])
691 Pleroma.Config.put([:instance, :account_activation_required], true)
692 on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end)
695 password = "testpassword"
696 user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
697 info_change = Pleroma.User.Info.confirmation_changeset(user.info, need_confirmation: true)
701 |> Ecto.Changeset.change()
702 |> Ecto.Changeset.put_embed(:info, info_change)
705 refute Pleroma.User.auth_active?(user)
707 app = insert(:oauth_app)
711 |> post("/oauth/token", %{
712 "grant_type" => "password",
713 "username" => user.nickname,
714 "password" => password,
715 "client_id" => app.client_id,
716 "client_secret" => app.client_secret
719 assert resp = json_response(conn, 403)
720 assert %{"error" => _} = resp
721 refute Map.has_key?(resp, "access_token")
724 test "rejects token exchange for valid credentials belonging to deactivated user" do
725 password = "testpassword"
729 password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
730 info: %{deactivated: true}
733 app = insert(:oauth_app)
737 |> post("/oauth/token", %{
738 "grant_type" => "password",
739 "username" => user.nickname,
740 "password" => password,
741 "client_id" => app.client_id,
742 "client_secret" => app.client_secret
745 assert resp = json_response(conn, 403)
746 assert %{"error" => _} = resp
747 refute Map.has_key?(resp, "access_token")
750 test "rejects an invalid authorization code" do
751 app = insert(:oauth_app)
755 |> post("/oauth/token", %{
756 "grant_type" => "authorization_code",
757 "code" => "Imobviouslyinvalid",
758 "redirect_uri" => app.redirect_uris,
759 "client_id" => app.client_id,
760 "client_secret" => app.client_secret
763 assert resp = json_response(conn, 400)
764 assert %{"error" => _} = json_response(conn, 400)
765 refute Map.has_key?(resp, "access_token")
769 describe "POST /oauth/token - refresh token" do
771 oauth_token_config = Pleroma.Config.get(@oauth_config_path)
774 Pleroma.Config.get(@oauth_config_path, oauth_token_config)
778 test "issues a new access token with keep fresh token" do
779 Pleroma.Config.put(@oauth_config_path, true)
781 app = insert(:oauth_app, scopes: ["read", "write"])
783 {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
784 {:ok, token} = Token.exchange_token(app, auth)
788 |> post("/oauth/token", %{
789 "grant_type" => "refresh_token",
790 "refresh_token" => token.refresh_token,
791 "client_id" => app.client_id,
792 "client_secret" => app.client_secret
794 |> json_response(200)
801 "token_type" => "Bearer",
804 "refresh_token" => _,
810 refute Repo.get_by(Token, token: token.token)
811 new_token = Repo.get_by(Token, token: response["access_token"])
812 assert new_token.refresh_token == token.refresh_token
813 assert new_token.scopes == auth.scopes
814 assert new_token.user_id == user.id
815 assert new_token.app_id == app.id
818 test "issues a new access token with new fresh token" do
819 Pleroma.Config.put(@oauth_config_path, false)
821 app = insert(:oauth_app, scopes: ["read", "write"])
823 {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
824 {:ok, token} = Token.exchange_token(app, auth)
828 |> post("/oauth/token", %{
829 "grant_type" => "refresh_token",
830 "refresh_token" => token.refresh_token,
831 "client_id" => app.client_id,
832 "client_secret" => app.client_secret
834 |> json_response(200)
841 "token_type" => "Bearer",
844 "refresh_token" => _,
850 refute Repo.get_by(Token, token: token.token)
851 new_token = Repo.get_by(Token, token: response["access_token"])
852 refute new_token.refresh_token == token.refresh_token
853 assert new_token.scopes == auth.scopes
854 assert new_token.user_id == user.id
855 assert new_token.app_id == app.id
858 test "returns 400 if we try use access token" do
860 app = insert(:oauth_app, scopes: ["read", "write"])
862 {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
863 {:ok, token} = Token.exchange_token(app, auth)
867 |> post("/oauth/token", %{
868 "grant_type" => "refresh_token",
869 "refresh_token" => token.token,
870 "client_id" => app.client_id,
871 "client_secret" => app.client_secret
873 |> json_response(400)
875 assert %{"error" => "Invalid credentials"} == response
878 test "returns 400 if refresh_token invalid" do
879 app = insert(:oauth_app, scopes: ["read", "write"])
883 |> post("/oauth/token", %{
884 "grant_type" => "refresh_token",
885 "refresh_token" => "token.refresh_token",
886 "client_id" => app.client_id,
887 "client_secret" => app.client_secret
889 |> json_response(400)
891 assert %{"error" => "Invalid credentials"} == response
894 test "issues a new token if token expired" do
896 app = insert(:oauth_app, scopes: ["read", "write"])
898 {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
899 {:ok, token} = Token.exchange_token(app, auth)
902 Ecto.Changeset.change(
904 %{valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -86_400 * 30)}
907 {:ok, access_token} = Repo.update(change)
911 |> post("/oauth/token", %{
912 "grant_type" => "refresh_token",
913 "refresh_token" => access_token.refresh_token,
914 "client_id" => app.client_id,
915 "client_secret" => app.client_secret
917 |> json_response(200)
924 "token_type" => "Bearer",
927 "refresh_token" => _,
933 refute Repo.get_by(Token, token: token.token)
934 token = Repo.get_by(Token, token: response["access_token"])
936 assert token.scopes == auth.scopes
937 assert token.user_id == user.id
938 assert token.app_id == app.id
942 describe "POST /oauth/token - bad request" do
943 test "returns 500" do
946 |> post("/oauth/token", %{})
947 |> json_response(500)
949 assert %{"error" => "Bad request"} == response
953 describe "POST /oauth/revoke - bad request" do
954 test "returns 500" do
957 |> post("/oauth/revoke", %{})
958 |> json_response(500)
960 assert %{"error" => "Bad request"} == response