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 "redirects to app if user is already authenticated", %{app: app, conn: conn} do
412 token = insert(:oauth_token, app_id: app.id)
416 |> put_session(:oauth_token, token.token)
420 "response_type" => "code",
421 "client_id" => app.client_id,
422 "redirect_uri" => app.redirect_uris,
427 assert redirected_to(conn) == "https://redirect.url"
431 describe "POST /oauth/authorize" do
432 test "redirects with oauth authorization" do
434 app = insert(:oauth_app, scopes: ["read", "write", "follow"])
438 |> post("/oauth/authorize", %{
439 "authorization" => %{
440 "name" => user.nickname,
441 "password" => "test",
442 "client_id" => app.client_id,
443 "redirect_uri" => app.redirect_uris,
444 "scope" => "read write",
445 "state" => "statepassed"
449 target = redirected_to(conn)
450 assert target =~ app.redirect_uris
452 query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
454 assert %{"state" => "statepassed", "code" => code} = query
455 auth = Repo.get_by(Authorization, token: code)
457 assert auth.scopes == ["read", "write"]
460 test "returns 401 for wrong credentials", %{conn: conn} do
462 app = insert(:oauth_app)
466 |> post("/oauth/authorize", %{
467 "authorization" => %{
468 "name" => user.nickname,
469 "password" => "wrong",
470 "client_id" => app.client_id,
471 "redirect_uri" => app.redirect_uris,
472 "state" => "statepassed",
473 "scope" => Enum.join(app.scopes, " ")
476 |> html_response(:unauthorized)
479 assert result =~ app.client_id
480 assert result =~ app.redirect_uris
483 assert result =~ "Invalid Username/Password"
486 test "returns 401 for missing scopes", %{conn: conn} do
488 app = insert(:oauth_app)
492 |> post("/oauth/authorize", %{
493 "authorization" => %{
494 "name" => user.nickname,
495 "password" => "test",
496 "client_id" => app.client_id,
497 "redirect_uri" => app.redirect_uris,
498 "state" => "statepassed",
502 |> html_response(:unauthorized)
505 assert result =~ app.client_id
506 assert result =~ app.redirect_uris
509 assert result =~ "This action is outside the authorized scopes"
512 test "returns 401 for scopes beyond app scopes", %{conn: conn} do
514 app = insert(:oauth_app, scopes: ["read", "write"])
518 |> post("/oauth/authorize", %{
519 "authorization" => %{
520 "name" => user.nickname,
521 "password" => "test",
522 "client_id" => app.client_id,
523 "redirect_uri" => app.redirect_uris,
524 "state" => "statepassed",
525 "scope" => "read write follow"
528 |> html_response(:unauthorized)
531 assert result =~ app.client_id
532 assert result =~ app.redirect_uris
535 assert result =~ "This action is outside the authorized scopes"
539 describe "POST /oauth/token" do
540 test "issues a token for an all-body request" do
542 app = insert(:oauth_app, scopes: ["read", "write"])
544 {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
548 |> post("/oauth/token", %{
549 "grant_type" => "authorization_code",
550 "code" => auth.token,
551 "redirect_uri" => app.redirect_uris,
552 "client_id" => app.client_id,
553 "client_secret" => app.client_secret
556 assert %{"access_token" => token, "me" => ap_id} = json_response(conn, 200)
558 token = Repo.get_by(Token, token: token)
560 assert token.scopes == auth.scopes
561 assert user.ap_id == ap_id
564 test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do
565 password = "testpassword"
566 user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
568 app = insert(:oauth_app, scopes: ["read", "write"])
570 # Note: "scope" param is intentionally omitted
573 |> post("/oauth/token", %{
574 "grant_type" => "password",
575 "username" => user.nickname,
576 "password" => password,
577 "client_id" => app.client_id,
578 "client_secret" => app.client_secret
581 assert %{"access_token" => token} = json_response(conn, 200)
583 token = Repo.get_by(Token, token: token)
585 assert token.scopes == app.scopes
588 test "issues a token for request with HTTP basic auth client credentials" do
590 app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"])
592 {:ok, auth} = Authorization.create_authorization(app, user, ["scope1", "scope2"])
593 assert auth.scopes == ["scope1", "scope2"]
596 (URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret))
601 |> put_req_header("authorization", "Basic " <> app_encoded)
602 |> post("/oauth/token", %{
603 "grant_type" => "authorization_code",
604 "code" => auth.token,
605 "redirect_uri" => app.redirect_uris
608 assert %{"access_token" => token, "scope" => scope} = json_response(conn, 200)
610 assert scope == "scope1 scope2"
612 token = Repo.get_by(Token, token: token)
614 assert token.scopes == ["scope1", "scope2"]
617 test "issue a token for client_credentials grant type" do
618 app = insert(:oauth_app, scopes: ["read", "write"])
622 |> post("/oauth/token", %{
623 "grant_type" => "client_credentials",
624 "client_id" => app.client_id,
625 "client_secret" => app.client_secret
628 assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
629 json_response(conn, 200)
632 token_from_db = Repo.get_by(Token, token: token)
635 assert scope == "read write"
638 test "rejects token exchange with invalid client credentials" do
640 app = insert(:oauth_app)
642 {:ok, auth} = Authorization.create_authorization(app, user)
646 |> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=")
647 |> post("/oauth/token", %{
648 "grant_type" => "authorization_code",
649 "code" => auth.token,
650 "redirect_uri" => app.redirect_uris
653 assert resp = json_response(conn, 400)
654 assert %{"error" => _} = resp
655 refute Map.has_key?(resp, "access_token")
658 test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do
659 setting = Pleroma.Config.get([:instance, :account_activation_required])
662 Pleroma.Config.put([:instance, :account_activation_required], true)
663 on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end)
666 password = "testpassword"
667 user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
668 info_change = Pleroma.User.Info.confirmation_changeset(user.info, need_confirmation: true)
672 |> Ecto.Changeset.change()
673 |> Ecto.Changeset.put_embed(:info, info_change)
676 refute Pleroma.User.auth_active?(user)
678 app = insert(:oauth_app)
682 |> post("/oauth/token", %{
683 "grant_type" => "password",
684 "username" => user.nickname,
685 "password" => password,
686 "client_id" => app.client_id,
687 "client_secret" => app.client_secret
690 assert resp = json_response(conn, 403)
691 assert %{"error" => _} = resp
692 refute Map.has_key?(resp, "access_token")
695 test "rejects token exchange for valid credentials belonging to deactivated user" do
696 password = "testpassword"
700 password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
701 info: %{deactivated: true}
704 app = insert(:oauth_app)
708 |> post("/oauth/token", %{
709 "grant_type" => "password",
710 "username" => user.nickname,
711 "password" => password,
712 "client_id" => app.client_id,
713 "client_secret" => app.client_secret
716 assert resp = json_response(conn, 403)
717 assert %{"error" => _} = resp
718 refute Map.has_key?(resp, "access_token")
721 test "rejects an invalid authorization code" do
722 app = insert(:oauth_app)
726 |> post("/oauth/token", %{
727 "grant_type" => "authorization_code",
728 "code" => "Imobviouslyinvalid",
729 "redirect_uri" => app.redirect_uris,
730 "client_id" => app.client_id,
731 "client_secret" => app.client_secret
734 assert resp = json_response(conn, 400)
735 assert %{"error" => _} = json_response(conn, 400)
736 refute Map.has_key?(resp, "access_token")
740 describe "POST /oauth/token - refresh token" do
742 oauth_token_config = Pleroma.Config.get(@oauth_config_path)
745 Pleroma.Config.get(@oauth_config_path, oauth_token_config)
749 test "issues a new access token with keep fresh token" do
750 Pleroma.Config.put(@oauth_config_path, true)
752 app = insert(:oauth_app, scopes: ["read", "write"])
754 {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
755 {:ok, token} = Token.exchange_token(app, auth)
759 |> post("/oauth/token", %{
760 "grant_type" => "refresh_token",
761 "refresh_token" => token.refresh_token,
762 "client_id" => app.client_id,
763 "client_secret" => app.client_secret
765 |> json_response(200)
772 "token_type" => "Bearer",
775 "refresh_token" => _,
781 refute Repo.get_by(Token, token: token.token)
782 new_token = Repo.get_by(Token, token: response["access_token"])
783 assert new_token.refresh_token == token.refresh_token
784 assert new_token.scopes == auth.scopes
785 assert new_token.user_id == user.id
786 assert new_token.app_id == app.id
789 test "issues a new access token with new fresh token" do
790 Pleroma.Config.put(@oauth_config_path, false)
792 app = insert(:oauth_app, scopes: ["read", "write"])
794 {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
795 {:ok, token} = Token.exchange_token(app, auth)
799 |> post("/oauth/token", %{
800 "grant_type" => "refresh_token",
801 "refresh_token" => token.refresh_token,
802 "client_id" => app.client_id,
803 "client_secret" => app.client_secret
805 |> json_response(200)
812 "token_type" => "Bearer",
815 "refresh_token" => _,
821 refute Repo.get_by(Token, token: token.token)
822 new_token = Repo.get_by(Token, token: response["access_token"])
823 refute new_token.refresh_token == token.refresh_token
824 assert new_token.scopes == auth.scopes
825 assert new_token.user_id == user.id
826 assert new_token.app_id == app.id
829 test "returns 400 if we try use access token" do
831 app = insert(:oauth_app, scopes: ["read", "write"])
833 {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
834 {:ok, token} = Token.exchange_token(app, auth)
838 |> post("/oauth/token", %{
839 "grant_type" => "refresh_token",
840 "refresh_token" => token.token,
841 "client_id" => app.client_id,
842 "client_secret" => app.client_secret
844 |> json_response(400)
846 assert %{"error" => "Invalid credentials"} == response
849 test "returns 400 if refresh_token invalid" do
850 app = insert(:oauth_app, scopes: ["read", "write"])
854 |> post("/oauth/token", %{
855 "grant_type" => "refresh_token",
856 "refresh_token" => "token.refresh_token",
857 "client_id" => app.client_id,
858 "client_secret" => app.client_secret
860 |> json_response(400)
862 assert %{"error" => "Invalid credentials"} == response
865 test "issues a new token if token expired" do
867 app = insert(:oauth_app, scopes: ["read", "write"])
869 {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
870 {:ok, token} = Token.exchange_token(app, auth)
873 Ecto.Changeset.change(
875 %{valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -86_400 * 30)}
878 {:ok, access_token} = Repo.update(change)
882 |> post("/oauth/token", %{
883 "grant_type" => "refresh_token",
884 "refresh_token" => access_token.refresh_token,
885 "client_id" => app.client_id,
886 "client_secret" => app.client_secret
888 |> json_response(200)
895 "token_type" => "Bearer",
898 "refresh_token" => _,
904 refute Repo.get_by(Token, token: token.token)
905 token = Repo.get_by(Token, token: response["access_token"])
907 assert token.scopes == auth.scopes
908 assert token.user_id == user.id
909 assert token.app_id == app.id
913 describe "POST /oauth/token - bad request" do
914 test "returns 500" do
917 |> post("/oauth/token", %{})
918 |> json_response(500)
920 assert %{"error" => "Bad request"} == response
924 describe "POST /oauth/revoke - bad request" do
925 test "returns 500" do
928 |> post("/oauth/revoke", %{})
929 |> json_response(500)
931 assert %{"error" => "Bad request"} == response