diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md
index 921995510..289f85930 100644
--- a/docs/API/differences_in_mastoapi_responses.md
+++ b/docs/API/differences_in_mastoapi_responses.md
@@ -120,6 +120,18 @@ Accepts additional parameters:
 - `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`.
 - `include_types`: will include the notifications for activities with the given types. The parameter accepts an array of types (`mention`, `follow`, `reblog`, `favourite`, `move`, `pleroma:emoji_reaction`). Usage example: `GET /api/v1/notifications?include_types[]=mention&include_types[]=reblog`.
 
+## DELETE `/api/v1/notifications/destroy_multiple`
+
+An endpoint to delete multiple statuses by IDs.
+
+Required parameters:
+
+- `ids`: array of activity ids
+
+Usage example: `DELETE /api/v1/notifications/destroy_multiple/?ids[]=1&ids[]=2`.
+
+Returns on success: 200 OK `{}`
+
 ## POST `/api/v1/statuses`
 
 Additional parameters can be added to the JSON body/Form data:
diff --git a/docs/dev.md b/docs/dev.md
new file mode 100644
index 000000000..f1b4cbf8b
--- /dev/null
+++ b/docs/dev.md
@@ -0,0 +1,23 @@
+This document contains notes and guidelines for Pleroma developers.
+
+# Authentication & Authorization
+
+## OAuth token-based authentication & authorization
+
+* Pleroma supports hierarchical OAuth scopes, just like Mastodon but with added granularity of admin scopes. For a reference, see [Mastodon OAuth scopes](https://docs.joinmastodon.org/api/oauth-scopes/).
+
+* It is important to either define OAuth scope restrictions or explicitly mark OAuth scope check as skipped, for every controller action. To define scopes, call `plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: [...]})`. To explicitly set OAuth scopes check skipped, call `plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug <when ...>)`.
+
+* In controllers, `use Pleroma.Web, :controller` will result in `action/2` (see `Pleroma.Web.controller/0` for definition) be called prior to actual controller action, and it'll perform security / privacy checks before passing control to actual controller action.
+
+  For routes with `:authenticated_api` pipeline, authentication & authorization are expected, thus `OAuthScopesPlug` will be run unless explicitly skipped (also `EnsureAuthenticatedPlug` will be executed immediately before action even if there was an early run to give an early error, since `OAuthScopesPlug` supports `:proceed_unauthenticated` option, and other plugs may support similar options as well).
+
+  For `:api` pipeline routes, it'll be verified whether `OAuthScopesPlug` was called or explicitly skipped, and if it was not then auth information will be dropped for request. Then `EnsurePublicOrAuthenticatedPlug` will be called to ensure that either the instance is not private or user is authenticated (unless explicitly skipped). Such automated checks help to prevent human errors and result in higher security / privacy for users.
+
+## [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization)
+
+* With HTTP Basic Auth, OAuth scopes check is _not_ performed for any action (since password is provided during the auth, requester is able to obtain a token with full permissions anyways). `Pleroma.Plugs.AuthenticationPlug` and `Pleroma.Plugs.LegacyAuthenticationPlug` both call `Pleroma.Plugs.OAuthScopesPlug.skip_plug(conn)` when password is provided.
+
+## Auth-related configuration, OAuth consumer mode etc.
+
+See `Authentication` section of [`docs/configuration/cheatsheet.md`](docs/configuration/cheatsheet.md#authentication).
diff --git a/lib/pleroma/plugs/auth_expected_plug.ex b/lib/pleroma/plugs/auth_expected_plug.ex
deleted file mode 100644
index f79597dc3..000000000
--- a/lib/pleroma/plugs/auth_expected_plug.ex
+++ /dev/null
@@ -1,17 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Plugs.AuthExpectedPlug do
-  import Plug.Conn
-
-  def init(options), do: options
-
-  def call(conn, _) do
-    put_private(conn, :auth_expected, true)
-  end
-
-  def auth_expected?(conn) do
-    conn.private[:auth_expected]
-  end
-end
diff --git a/lib/pleroma/plugs/ensure_authenticated_plug.ex b/lib/pleroma/plugs/ensure_authenticated_plug.ex
index 054d2297f..9c8f5597f 100644
--- a/lib/pleroma/plugs/ensure_authenticated_plug.ex
+++ b/lib/pleroma/plugs/ensure_authenticated_plug.ex
@@ -5,17 +5,21 @@
 defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do
   import Plug.Conn
   import Pleroma.Web.TranslationHelpers
+
   alias Pleroma.User
 
+  use Pleroma.Web, :plug
+
   def init(options) do
     options
   end
 
-  def call(%{assigns: %{user: %User{}}} = conn, _) do
+  @impl true
+  def perform(%{assigns: %{user: %User{}}} = conn, _) do
     conn
   end
 
-  def call(conn, options) do
+  def perform(conn, options) do
     perform =
       cond do
         options[:if_func] -> options[:if_func].()
diff --git a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex
index d980ff13d..7265bb87a 100644
--- a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex
+++ b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex
@@ -5,14 +5,18 @@
 defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do
   import Pleroma.Web.TranslationHelpers
   import Plug.Conn
+
   alias Pleroma.Config
   alias Pleroma.User
 
+  use Pleroma.Web, :plug
+
   def init(options) do
     options
   end
 
-  def call(conn, _) do
+  @impl true
+  def perform(conn, _) do
     public? = Config.get!([:instance, :public])
 
     case {public?, conn} do
diff --git a/lib/pleroma/plugs/expect_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_authenticated_check_plug.ex
new file mode 100644
index 000000000..66b8d5de5
--- /dev/null
+++ b/lib/pleroma/plugs/expect_authenticated_check_plug.ex
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.ExpectAuthenticatedCheckPlug do
+  @moduledoc """
+  Marks `Pleroma.Plugs.EnsureAuthenticatedPlug` as expected to be executed later in plug chain.
+
+  No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`).
+  """
+
+  use Pleroma.Web, :plug
+
+  def init(options), do: options
+
+  @impl true
+  def perform(conn, _) do
+    conn
+  end
+end
diff --git a/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex b/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex
new file mode 100644
index 000000000..ba0ef76bd
--- /dev/null
+++ b/lib/pleroma/plugs/expect_public_or_authenticated_check_plug.ex
@@ -0,0 +1,21 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug do
+  @moduledoc """
+  Marks `Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug` as expected to be executed later in plug
+  chain.
+
+  No-op plug which affects `Pleroma.Web` operation (is checked with `PlugHelper.plug_called?/2`).
+  """
+
+  use Pleroma.Web, :plug
+
+  def init(options), do: options
+
+  @impl true
+  def perform(conn, _) do
+    conn
+  end
+end
diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex
index 66f48c28c..efc25b79f 100644
--- a/lib/pleroma/plugs/oauth_scopes_plug.ex
+++ b/lib/pleroma/plugs/oauth_scopes_plug.ex
@@ -7,15 +7,12 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
   import Pleroma.Web.Gettext
 
   alias Pleroma.Config
-  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
-  alias Pleroma.Plugs.PlugHelper
 
   use Pleroma.Web, :plug
 
-  @behaviour Plug
-
   def init(%{scopes: _} = options), do: options
 
+  @impl true
   def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
     op = options[:op] || :|
     token = assigns[:token]
@@ -31,10 +28,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
         conn
 
       options[:fallback] == :proceed_unauthenticated ->
-        conn
-        |> assign(:user, nil)
-        |> assign(:token, nil)
-        |> maybe_perform_instance_privacy_check(options)
+        drop_auth_info(conn)
 
       true ->
         missing_scopes = scopes -- matched_scopes
@@ -50,6 +44,15 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
     end
   end
 
+  @doc "Drops authentication info from connection"
+  def drop_auth_info(conn) do
+    # To simplify debugging, setting a private variable on `conn` if auth info is dropped
+    conn
+    |> put_private(:authentication_ignored, true)
+    |> assign(:user, nil)
+    |> assign(:token, nil)
+  end
+
   @doc "Filters descendants of supported scopes"
   def filter_descendants(scopes, supported_scopes) do
     Enum.filter(
@@ -71,12 +74,4 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
       scopes
     end
   end
-
-  defp maybe_perform_instance_privacy_check(%Plug.Conn{} = conn, options) do
-    if options[:skip_instance_privacy_check] do
-      conn
-    else
-      EnsurePublicOrAuthenticatedPlug.call(conn, [])
-    end
-  end
 end
diff --git a/lib/pleroma/tests/auth_test_controller.ex b/lib/pleroma/tests/auth_test_controller.ex
new file mode 100644
index 000000000..fb04411d9
--- /dev/null
+++ b/lib/pleroma/tests/auth_test_controller.ex
@@ -0,0 +1,93 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+# A test controller reachable only in :test env.
+defmodule Pleroma.Tests.AuthTestController do
+  @moduledoc false
+
+  use Pleroma.Web, :controller
+
+  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
+  alias Pleroma.Plugs.OAuthScopesPlug
+  alias Pleroma.User
+
+  # Serves only with proper OAuth token (:api and :authenticated_api)
+  # Skipping EnsurePublicOrAuthenticatedPlug has no effect in this case
+  #
+  # Suggested use case: all :authenticated_api endpoints (makes no sense for :api endpoints)
+  plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :do_oauth_check)
+
+  # Via :api, keeps :user if token has requested scopes (if :user is dropped, serves if public)
+  # Via :authenticated_api, serves if token is present and has requested scopes
+  #
+  # Suggested use case: vast majority of :api endpoints (no sense for :authenticated_api ones)
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read"], fallback: :proceed_unauthenticated}
+    when action == :fallback_oauth_check
+  )
+
+  # Keeps :user if present, executes regardless of token / token scopes
+  # Fails with no :user for :authenticated_api / no user for :api on private instance
+  # Note: EnsurePublicOrAuthenticatedPlug is not skipped (private instance fails on no :user)
+  # Note: Basic Auth processing results in :skip_plug call for OAuthScopesPlug
+  #
+  # Suggested use: suppressing OAuth checks for other auth mechanisms (like Basic Auth)
+  # For controller-level use, see :skip_oauth_skip_publicity_check instead
+  plug(
+    :skip_plug,
+    OAuthScopesPlug when action == :skip_oauth_check
+  )
+
+  # (Shouldn't be executed since the plug is skipped)
+  plug(OAuthScopesPlug, %{scopes: ["admin"]} when action == :skip_oauth_check)
+
+  # Via :api, keeps :user if token has requested scopes, and continues with nil :user otherwise
+  # Via :authenticated_api, serves if token is present and has requested scopes
+  #
+  # Suggested use: as :fallback_oauth_check but open with nil :user for :api on private instances
+  plug(
+    :skip_plug,
+    EnsurePublicOrAuthenticatedPlug when action == :fallback_oauth_skip_publicity_check
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read"], fallback: :proceed_unauthenticated}
+    when action == :fallback_oauth_skip_publicity_check
+  )
+
+  # Via :api, keeps :user if present, serves regardless of token presence / scopes / :user presence
+  # Via :authenticated_api, serves if :user is set (regardless of token presence and its scopes)
+  #
+  # Suggested use: making an :api endpoint always accessible (e.g. email confirmation endpoint)
+  plug(
+    :skip_plug,
+    [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug]
+    when action == :skip_oauth_skip_publicity_check
+  )
+
+  # Via :authenticated_api, always fails with 403 (endpoint is insecure)
+  # Via :api, drops :user if present and serves if public (private instance rejects on no user)
+  #
+  # Suggested use: none; please define OAuth rules for all :api / :authenticated_api endpoints
+  plug(:skip_plug, [] when action == :missing_oauth_check_definition)
+
+  def do_oauth_check(conn, _params), do: conn_state(conn)
+
+  def fallback_oauth_check(conn, _params), do: conn_state(conn)
+
+  def skip_oauth_check(conn, _params), do: conn_state(conn)
+
+  def fallback_oauth_skip_publicity_check(conn, _params), do: conn_state(conn)
+
+  def skip_oauth_skip_publicity_check(conn, _params), do: conn_state(conn)
+
+  def missing_oauth_check_definition(conn, _params), do: conn_state(conn)
+
+  defp conn_state(%{assigns: %{user: %User{} = user}} = conn),
+    do: json(conn, %{user_id: user.id})
+
+  defp conn_state(conn), do: json(conn, %{user_id: nil})
+end
diff --git a/lib/pleroma/tests/oauth_test_controller.ex b/lib/pleroma/tests/oauth_test_controller.ex
deleted file mode 100644
index 58d517f78..000000000
--- a/lib/pleroma/tests/oauth_test_controller.ex
+++ /dev/null
@@ -1,31 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-# A test controller reachable only in :test env.
-# Serves to test OAuth scopes check skipping / enforcement.
-defmodule Pleroma.Tests.OAuthTestController do
-  @moduledoc false
-
-  use Pleroma.Web, :controller
-
-  alias Pleroma.Plugs.OAuthScopesPlug
-
-  plug(:skip_plug, OAuthScopesPlug when action == :skipped_oauth)
-
-  plug(OAuthScopesPlug, %{scopes: ["read"]} when action != :missed_oauth)
-
-  def skipped_oauth(conn, _params) do
-    noop(conn)
-  end
-
-  def performed_oauth(conn, _params) do
-    noop(conn)
-  end
-
-  def missed_oauth(conn, _params) do
-    noop(conn)
-  end
-
-  defp noop(conn), do: json(conn, %{})
-end
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 9c79310c0..816c11e01 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -48,6 +48,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     %{scopes: ["write:accounts"], admin: true}
     when action in [
            :get_password_reset,
+           :force_password_reset,
            :user_delete,
            :users_create,
            :user_toggle_activation,
@@ -56,7 +57,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
            :tag_users,
            :untag_users,
            :right_add,
+           :right_add_multiple,
            :right_delete,
+           :right_delete_multiple,
            :update_user_credentials
          ]
   )
@@ -84,13 +87,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
   plug(
     OAuthScopesPlug,
     %{scopes: ["write:reports"], admin: true}
-    when action in [:reports_update]
+    when action in [:reports_update, :report_notes_create, :report_notes_delete]
   )
 
   plug(
     OAuthScopesPlug,
     %{scopes: ["read:statuses"], admin: true}
-    when action == :list_user_statuses
+    when action in [:list_statuses, :list_user_statuses, :list_instance_statuses]
   )
 
   plug(
@@ -102,13 +105,30 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
   plug(
     OAuthScopesPlug,
     %{scopes: ["read"], admin: true}
-    when action in [:config_show, :list_log, :stats]
+    when action in [
+           :config_show,
+           :list_log,
+           :stats,
+           :relay_list,
+           :config_descriptions,
+           :need_reboot
+         ]
   )
 
   plug(
     OAuthScopesPlug,
     %{scopes: ["write"], admin: true}
-    when action == :config_update
+    when action in [
+           :restart,
+           :config_update,
+           :resend_confirmation_email,
+           :confirm_email,
+           :oauth_app_create,
+           :oauth_app_list,
+           :oauth_app_update,
+           :oauth_app_delete,
+           :reload_emoji
+         ]
   )
 
   action_fallback(:errors)
@@ -1103,25 +1123,25 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     |> json(%{"status_visibility" => count})
   end
 
-  def errors(conn, {:error, :not_found}) do
+  defp errors(conn, {:error, :not_found}) do
     conn
     |> put_status(:not_found)
     |> json(dgettext("errors", "Not found"))
   end
 
-  def errors(conn, {:error, reason}) do
+  defp errors(conn, {:error, reason}) do
     conn
     |> put_status(:bad_request)
     |> json(reason)
   end
 
-  def errors(conn, {:param_cast, _}) do
+  defp errors(conn, {:param_cast, _}) do
     conn
     |> put_status(:bad_request)
     |> json(dgettext("errors", "Invalid parameters"))
   end
 
-  def errors(conn, _) do
+  defp errors(conn, _) do
     conn
     |> put_status(:internal_server_error)
     |> json(dgettext("errors", "Something went wrong"))
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index 6fb6e627b..fe9548b1b 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -131,6 +131,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
             "Include statuses from muted acccounts."
           ),
           Operation.parameter(:exclude_reblogs, :query, BooleanLike, "Exclude reblogs"),
+          Operation.parameter(:exclude_replies, :query, BooleanLike, "Exclude replies"),
           Operation.parameter(
             :exclude_visibilities,
             :query,
@@ -294,13 +295,13 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
     }
   end
 
-  def follows_operation do
+  def follow_by_uri_operation do
     %Operation{
       tags: ["accounts"],
-      summary: "Follows",
+      summary: "Follow by URI",
       operationId: "AccountController.follows",
       security: [%{"oAuth" => ["follow", "write:follows"]}],
-      requestBody: request_body("Parameters", follows_request(), required: true),
+      requestBody: request_body("Parameters", follow_by_uri_request(), required: true),
       responses: %{
         200 => Operation.response("Account", "application/json", AccountRelationship),
         400 => Operation.response("Error", "application/json", ApiError),
@@ -615,7 +616,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
     }
   end
 
-  defp follows_request do
+  defp follow_by_uri_request do
     %Schema{
       title: "AccountFollowsRequest",
       description: "POST body for muting an account",
diff --git a/lib/pleroma/web/fallback_redirect_controller.ex b/lib/pleroma/web/fallback_redirect_controller.ex
index c13518030..0d9d578fc 100644
--- a/lib/pleroma/web/fallback_redirect_controller.ex
+++ b/lib/pleroma/web/fallback_redirect_controller.ex
@@ -4,7 +4,9 @@
 
 defmodule Fallback.RedirectController do
   use Pleroma.Web, :controller
+
   require Logger
+
   alias Pleroma.User
   alias Pleroma.Web.Metadata
 
diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex
index 557cde328..d0d8bc8eb 100644
--- a/lib/pleroma/web/masto_fe_controller.ex
+++ b/lib/pleroma/web/masto_fe_controller.ex
@@ -5,19 +5,25 @@
 defmodule Pleroma.Web.MastoFEController do
   use Pleroma.Web, :controller
 
+  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
   alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.User
 
   plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings)
 
   # Note: :index action handles attempt of unauthenticated access to private instance with redirect
+  plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action == :index)
+
   plug(
     OAuthScopesPlug,
-    %{scopes: ["read"], fallback: :proceed_unauthenticated, skip_instance_privacy_check: true}
+    %{scopes: ["read"], fallback: :proceed_unauthenticated}
     when action == :index
   )
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :manifest])
+  plug(
+    :skip_plug,
+    [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :manifest
+  )
 
   @doc "GET /web/*path"
   def index(%{assigns: %{user: user, token: token}} = conn, _params)
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 37adeec5f..1eedf02d6 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
       skip_relationships?: 1
     ]
 
+  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
   alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.Plugs.RateLimiter
   alias Pleroma.User
@@ -28,18 +29,26 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
 
   plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
 
-  plug(:skip_plug, OAuthScopesPlug when action == :identity_proofs)
+  plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
+
+  plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
 
   plug(
     OAuthScopesPlug,
     %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
-    when action == :show
+    when action in [:show, :followers, :following]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
+    when action == :statuses
   )
 
   plug(
     OAuthScopesPlug,
     %{scopes: ["read:accounts"]}
-    when action in [:endorsements, :verify_credentials, :followers, :following]
+    when action in [:verify_credentials, :endorsements, :identity_proofs]
   )
 
   plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
@@ -58,21 +67,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
 
   plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
 
-  # Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows
   plug(
     OAuthScopesPlug,
-    %{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow]
+    %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
   )
 
   plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
 
   plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
 
-  plug(
-    Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
-    when action not in [:create, :show, :statuses]
-  )
-
   @relationship_actions [:follow, :unfollow]
   @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
 
@@ -378,7 +381,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
   end
 
   @doc "POST /api/v1/follows"
-  def follows(%{body_params: %{uri: uri}} = conn, _) do
+  def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
     case User.get_cached_by_nickname(uri) do
       %User{} = user ->
         conn
diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
index 005c60444..408e11474 100644
--- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
@@ -5,6 +5,7 @@
 defmodule Pleroma.Web.MastodonAPI.AppController do
   use Pleroma.Web, :controller
 
+  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
   alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.Repo
   alias Pleroma.Web.OAuth.App
@@ -13,7 +14,14 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
 
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
 
+  plug(
+    :skip_plug,
+    [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug]
+    when action == :create
+  )
+
   plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
+
   plug(OpenApiSpex.Plug.CastAndValidate)
 
   @local_mastodon_name "Mastodon-Local"
diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
index 37b389382..753b3db3e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -13,10 +13,10 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
 
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
 
-  @local_mastodon_name "Mastodon-Local"
-
   plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)
 
+  @local_mastodon_name "Mastodon-Local"
+
   @doc "GET /web/login"
   def login(%{assigns: %{user: %User{}}} = conn, _params) do
     redirect(conn, to: local_mastodon_root_path(conn))
diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
index 7c9b11bf1..c44641526 100644
--- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
@@ -14,9 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
 
   plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index)
-  plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action == :read)
-
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+  plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action != :index)
 
   @doc "GET /api/v1/conversations"
   def index(%{assigns: %{user: user}} = conn, params) do
@@ -28,7 +26,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
   end
 
   @doc "POST /api/v1/conversations/:id/read"
-  def read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
+  def mark_as_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
     with %Participation{} = participation <-
            Repo.get_by(Participation, id: participation_id, user_id: user.id),
          {:ok, participation} <- Participation.mark_as_read(participation) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
index 3bfebef8b..000ad743f 100644
--- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
@@ -7,6 +7,12 @@ defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
 
   plug(OpenApiSpex.Plug.CastAndValidate)
 
+  plug(
+    :skip_plug,
+    [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+    when action == :index
+  )
+
   defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.CustomEmojiOperation
 
   def index(conn, _params) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
index 84de79413..c4fa383f2 100644
--- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -21,8 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
     %{scopes: ["follow", "write:blocks"]} when action != :index
   )
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
   @doc "GET /api/v1/domain_blocks"
   def index(%{assigns: %{user: user}} = conn, _) do
     json(conn, Map.get(user, :domain_blocks, []))
diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
index 7b0b937a2..7fd0562c9 100644
--- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
@@ -17,8 +17,6 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
     %{scopes: ["write:filters"]} when action not in @oauth_read_actions
   )
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
   @doc "GET /api/v1/filters"
   def index(%{assigns: %{user: user}} = conn, _) do
     filters = Filter.get_filters(user)
diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
index 1ca86f63f..25f2269b9 100644
--- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
@@ -21,8 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
     %{scopes: ["follow", "write:follows"]} when action != :index
   )
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
   @doc "GET /api/v1/follow_requests"
   def index(%{assigns: %{user: followed}} = conn, _params) do
     follow_requests = User.get_follow_requests(followed)
diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
index 27b5b1a52..237f85677 100644
--- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
@@ -5,6 +5,12 @@
 defmodule Pleroma.Web.MastodonAPI.InstanceController do
   use Pleroma.Web, :controller
 
+  plug(
+    :skip_plug,
+    [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+    when action in [:show, :peers]
+  )
+
   @doc "GET /api/v1/instance"
   def show(conn, _params) do
     render(conn, "show.json")
diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
index dac4daa7b..bfe856025 100644
--- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
@@ -11,16 +11,16 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
 
   plug(:list_by_id_and_user when action not in [:index, :create])
 
-  plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in [:index, :show, :list_accounts])
+  @oauth_read_actions [:index, :show, :list_accounts]
+
+  plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in @oauth_read_actions)
 
   plug(
     OAuthScopesPlug,
     %{scopes: ["write:lists"]}
-    when action in [:create, :update, :delete, :add_to_list, :remove_from_list]
+    when action not in @oauth_read_actions
   )
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
 
   # GET /api/v1/lists
diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
index 58e8a30c2..9f9d4574e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
   )
 
   plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert)
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
 
   # GET /api/v1/markers
diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
index ac8c18f24..e7767de4e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -15,9 +15,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
 
   require Logger
 
-  plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug when action in [:empty_array, :empty_object])
-
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+  plug(
+    :skip_plug,
+    [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+    when action in [:empty_array, :empty_object]
+  )
 
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
 
diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
index 2b6f00952..e36751220 100644
--- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
@@ -15,8 +15,6 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
 
   plug(OAuthScopesPlug, %{scopes: ["write:media"]})
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
   @doc "POST /api/v1/media"
   def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
     with {:ok, object} <-
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index dcb421756..a14c86893 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -22,8 +22,6 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
 
   plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions)
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
   defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.NotificationOperation
 
   # GET /api/v1/notifications
diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
index d9f894118..af9b66eff 100644
--- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
@@ -22,8 +22,6 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
 
   plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :vote)
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
   @doc "GET /api/v1/polls/:id"
   def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
     with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
diff --git a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
index f5782be13..9fbaa7bd1 100644
--- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
@@ -11,8 +11,6 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
 
   plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
   @doc "POST /api/v1/reports"
   def create(%{assigns: %{user: user}} = conn, params) do
     with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
index e1e6bd89b..899b78873 100644
--- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
@@ -18,8 +18,6 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
   plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
   plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
 
   @doc "GET /api/v1/scheduled_statuses"
diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
index c258742dd..cd49da6ad 100644
--- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -21,7 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
   # Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
   plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+  # Note: on private instances auth is required (EnsurePublicOrAuthenticatedPlug is not skipped)
 
   plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
 
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index f6e4f7d66..9eea2e9eb 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -24,6 +24,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
   alias Pleroma.Web.MastodonAPI.AccountView
   alias Pleroma.Web.MastodonAPI.ScheduledActivityView
 
+  plug(:skip_plug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show])
+
   @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
 
   plug(
@@ -77,8 +79,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
     %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
   )
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :show])
-
   @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
 
   plug(
@@ -358,7 +358,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
   end
 
   @doc "GET /api/v1/favourites"
-  def favourites(%{assigns: %{user: user}} = conn, params) do
+  def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
     activities =
       ActivityPub.fetch_favourites(
         user,
diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
index 4647c1f96..d184ea1d0 100644
--- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
   action_fallback(:errors)
 
   plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+
   plug(:restrict_push_enabled)
 
   # Creates PushSubscription
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
index 403d500e0..2d67e19da 100644
--- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -9,11 +9,14 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
     only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1, skip_relationships?: 1]
 
   alias Pleroma.Pagination
+  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
   alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.Plugs.RateLimiter
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
 
+  plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:public, :hashtag])
+
   # TODO: Replace with a macro when there is a Phoenix release with the following commit in it:
   # https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e
 
@@ -26,7 +29,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
   plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
   plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :public)
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
+    when action in [:public, :hashtag]
+  )
 
   plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
 
@@ -94,7 +101,9 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
 
     restrict? = Pleroma.Config.get([:restrict_unauthenticated, :timelines, cfg_key])
 
-    if not (restrict? and is_nil(user)) do
+    if restrict? and is_nil(user) do
+      render_error(conn, :unauthorized, "authorization required for timeline view")
+    else
       activities =
         params
         |> Map.put("type", ["Create", "Announce"])
@@ -112,12 +121,10 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
         as: :activity,
         skip_relationships: skip_relationships?(params)
       )
-    else
-      render_error(conn, :unauthorized, "authorization required for timeline view")
     end
   end
 
-  def hashtag_fetching(params, user, local_only) do
+  defp hashtag_fetching(params, user, local_only) do
     tags =
       [params["tag"], params["any"]]
       |> List.flatten()
diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
index 1a09ac62a..4657a4383 100644
--- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
@@ -4,6 +4,7 @@
 
 defmodule Pleroma.Web.MediaProxy.MediaProxyController do
   use Pleroma.Web, :controller
+
   alias Pleroma.ReverseProxy
   alias Pleroma.Web.MediaProxy
 
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 0121cd661..685269877 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -25,9 +25,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do
 
   plug(:fetch_session)
   plug(:fetch_flash)
-  plug(RateLimiter, [name: :authentication] when action == :create_authorization)
 
-  plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug)
+  plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug])
+
+  plug(RateLimiter, [name: :authentication] when action == :create_authorization)
 
   action_fallback(Pleroma.Web.OAuth.FallbackController)
 
diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
index 60405fbff..be7477867 100644
--- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
     only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2, skip_relationships?: 1]
 
   alias Ecto.Changeset
+  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
   alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.Plugs.RateLimiter
   alias Pleroma.User
@@ -17,6 +18,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
 
   require Pleroma.Constants
 
+  plug(
+    :skip_plug,
+    [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirmation_resend
+  )
+
   plug(
     OAuthScopesPlug,
     %{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
@@ -33,15 +39,13 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
          ]
   )
 
-  plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
-
-  # An extra safety measure for possible actions not guarded by OAuth permissions specification
   plug(
-    Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
-    when action != :confirmation_resend
+    OAuthScopesPlug,
+    %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites
   )
 
   plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend)
+
   plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])
   plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
 
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
index 03e95e020..e01825b48 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
@@ -1,6 +1,7 @@
 defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
   use Pleroma.Web, :controller
 
+  alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug
   alias Pleroma.Plugs.OAuthScopesPlug
 
   require Logger
@@ -11,17 +12,20 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
     when action in [
            :create,
            :delete,
-           :download_from,
-           :list_from,
+           :save_from,
            :import_from_fs,
            :update_file,
            :update_metadata
          ]
   )
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+  plug(
+    :skip_plug,
+    [OAuthScopesPlug, ExpectPublicOrAuthenticatedCheckPlug]
+    when action in [:download_shared, :list_packs, :list_from]
+  )
 
-  def emoji_dir_path do
+  defp emoji_dir_path do
     Path.join(
       Pleroma.Config.get!([:instance, :static_dir]),
       "emoji"
@@ -212,13 +216,13 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
   end
 
   @doc """
-  An admin endpoint to request downloading a pack named `pack_name` from the instance
+  An admin endpoint to request downloading and storing a pack named `pack_name` from the instance
   `instance_address`.
 
   If the requested instance's admin chose to share the pack, it will be downloaded
   from that instance, otherwise it will be downloaded from the fallback source, if there is one.
   """
-  def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
+  def save_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
     address = String.trim(address)
 
     if shareable_packs_available(address) do
diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
index d9c1c8636..d4e0d8b7c 100644
--- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
@@ -12,8 +12,6 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
   plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)
   plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show)
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
   @doc "GET /api/v1/pleroma/mascot"
   def show(%{assigns: %{user: user}} = conn, _params) do
     json(conn, User.get_mascot(user))
diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
index fe1b97a20..2c1874051 100644
--- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
@@ -26,6 +26,12 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
     when action in [:conversation, :conversation_statuses]
   )
 
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
+    when action == :emoji_reactions_by
+  )
+
   plug(
     OAuthScopesPlug,
     %{scopes: ["write:statuses"]}
@@ -34,12 +40,14 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
 
   plug(
     OAuthScopesPlug,
-    %{scopes: ["write:conversations"]} when action in [:update_conversation, :read_conversations]
+    %{scopes: ["write:conversations"]}
+    when action in [:update_conversation, :mark_conversations_as_read]
   )
 
-  plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification)
-
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read
+  )
 
   def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id} = params) do
     with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
@@ -167,7 +175,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
     end
   end
 
-  def read_conversations(%{assigns: %{user: user}} = conn, _params) do
+  def mark_conversations_as_read(%{assigns: %{user: user}} = conn, _params) do
     with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
       conn
       |> add_link_headers(participations)
@@ -176,7 +184,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
     end
   end
 
-  def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
+  def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
     with {:ok, notification} <- Notification.read_one(user, notification_id) do
       conn
       |> put_view(NotificationView)
@@ -189,7 +197,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
     end
   end
 
-  def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id} = params) do
+  def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"max_id" => max_id} = params) do
     with notifications <- Notification.set_read_up_to(user, max_id) do
       notifications = Enum.take(notifications, 80)
 
diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
index 4463ec477..22da6c0ad 100644
--- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
@@ -13,10 +13,12 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.MastodonAPI.StatusView
 
-  plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :user_scrobbles)
-  plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles)
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :user_scrobbles
+  )
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+  plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles)
 
   def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do
     params =
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index fe984b06c..ab6ae9415 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -16,6 +16,14 @@ defmodule Pleroma.Web.Router do
     plug(Pleroma.Plugs.UserEnabledPlug)
   end
 
+  pipeline :expect_authentication do
+    plug(Pleroma.Plugs.ExpectAuthenticatedCheckPlug)
+  end
+
+  pipeline :expect_public_instance_or_authentication do
+    plug(Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug)
+  end
+
   pipeline :authenticate do
     plug(Pleroma.Plugs.OAuthPlug)
     plug(Pleroma.Plugs.BasicAuthDecoderPlug)
@@ -39,20 +47,22 @@ defmodule Pleroma.Web.Router do
   end
 
   pipeline :api do
+    plug(:expect_public_instance_or_authentication)
     plug(:base_api)
     plug(:after_auth)
     plug(Pleroma.Plugs.IdempotencyPlug)
   end
 
   pipeline :authenticated_api do
+    plug(:expect_authentication)
     plug(:base_api)
-    plug(Pleroma.Plugs.AuthExpectedPlug)
     plug(:after_auth)
     plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
     plug(Pleroma.Plugs.IdempotencyPlug)
   end
 
   pipeline :admin_api do
+    plug(:expect_authentication)
     plug(:base_api)
     plug(Pleroma.Plugs.AdminSecretAuthenticationPlug)
     plug(:after_auth)
@@ -200,24 +210,28 @@ defmodule Pleroma.Web.Router do
   end
 
   scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
+    # Modifying packs
     scope "/packs" do
-      # Modifying packs
       pipe_through(:admin_api)
 
       post("/import_from_fs", EmojiAPIController, :import_from_fs)
-
       post("/:pack_name/update_file", EmojiAPIController, :update_file)
       post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata)
       put("/:name", EmojiAPIController, :create)
       delete("/:name", EmojiAPIController, :delete)
-      post("/download_from", EmojiAPIController, :download_from)
-      post("/list_from", EmojiAPIController, :list_from)
+
+      # Note: /download_from downloads and saves to instance, not to requester
+      post("/download_from", EmojiAPIController, :save_from)
     end
 
+    # Pack info / downloading
     scope "/packs" do
-      # Pack info / downloading
       get("/", EmojiAPIController, :list_packs)
       get("/:name/download_shared/", EmojiAPIController, :download_shared)
+      get("/list_from", EmojiAPIController, :list_from)
+
+      # Deprecated: POST /api/pleroma/emoji/packs/list_from (use GET instead)
+      post("/list_from", EmojiAPIController, :list_from)
     end
   end
 
@@ -277,7 +291,7 @@ defmodule Pleroma.Web.Router do
 
       get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
       get("/conversations/:id", PleromaAPIController, :conversation)
-      post("/conversations/read", PleromaAPIController, :read_conversations)
+      post("/conversations/read", PleromaAPIController, :mark_conversations_as_read)
     end
 
     scope [] do
@@ -286,7 +300,7 @@ defmodule Pleroma.Web.Router do
       patch("/conversations/:id", PleromaAPIController, :update_conversation)
       put("/statuses/:id/reactions/:emoji", PleromaAPIController, :react_with_emoji)
       delete("/statuses/:id/reactions/:emoji", PleromaAPIController, :unreact_with_emoji)
-      post("/notifications/read", PleromaAPIController, :read_notification)
+      post("/notifications/read", PleromaAPIController, :mark_notifications_as_read)
 
       patch("/accounts/update_avatar", AccountController, :update_avatar)
       patch("/accounts/update_banner", AccountController, :update_banner)
@@ -322,53 +336,84 @@ defmodule Pleroma.Web.Router do
     pipe_through(:authenticated_api)
 
     get("/accounts/verify_credentials", AccountController, :verify_credentials)
+    patch("/accounts/update_credentials", AccountController, :update_credentials)
 
     get("/accounts/relationships", AccountController, :relationships)
-
     get("/accounts/:id/lists", AccountController, :lists)
     get("/accounts/:id/identity_proofs", AccountController, :identity_proofs)
-
-    get("/follow_requests", FollowRequestController, :index)
+    get("/endorsements", AccountController, :endorsements)
     get("/blocks", AccountController, :blocks)
     get("/mutes", AccountController, :mutes)
 
-    get("/timelines/home", TimelineController, :home)
-    get("/timelines/direct", TimelineController, :direct)
+    post("/follows", AccountController, :follow_by_uri)
+    post("/accounts/:id/follow", AccountController, :follow)
+    post("/accounts/:id/unfollow", AccountController, :unfollow)
+    post("/accounts/:id/block", AccountController, :block)
+    post("/accounts/:id/unblock", AccountController, :unblock)
+    post("/accounts/:id/mute", AccountController, :mute)
+    post("/accounts/:id/unmute", AccountController, :unmute)
 
-    get("/favourites", StatusController, :favourites)
-    get("/bookmarks", StatusController, :bookmarks)
+    get("/apps/verify_credentials", AppController, :verify_credentials)
+
+    get("/conversations", ConversationController, :index)
+    post("/conversations/:id/read", ConversationController, :mark_as_read)
+
+    get("/domain_blocks", DomainBlockController, :index)
+    post("/domain_blocks", DomainBlockController, :create)
+    delete("/domain_blocks", DomainBlockController, :delete)
+
+    get("/filters", FilterController, :index)
+
+    post("/filters", FilterController, :create)
+    get("/filters/:id", FilterController, :show)
+    put("/filters/:id", FilterController, :update)
+    delete("/filters/:id", FilterController, :delete)
+
+    get("/follow_requests", FollowRequestController, :index)
+    post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
+    post("/follow_requests/:id/reject", FollowRequestController, :reject)
+
+    get("/lists", ListController, :index)
+    get("/lists/:id", ListController, :show)
+    get("/lists/:id/accounts", ListController, :list_accounts)
+
+    delete("/lists/:id", ListController, :delete)
+    post("/lists", ListController, :create)
+    put("/lists/:id", ListController, :update)
+    post("/lists/:id/accounts", ListController, :add_to_list)
+    delete("/lists/:id/accounts", ListController, :remove_from_list)
+
+    get("/markers", MarkerController, :index)
+    post("/markers", MarkerController, :upsert)
+
+    post("/media", MediaController, :create)
+    put("/media/:id", MediaController, :update)
 
     get("/notifications", NotificationController, :index)
     get("/notifications/:id", NotificationController, :show)
+
     post("/notifications/:id/dismiss", NotificationController, :dismiss)
     post("/notifications/clear", NotificationController, :clear)
     delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
     # Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
     post("/notifications/dismiss", NotificationController, :dismiss_via_body)
 
+    post("/polls/:id/votes", PollController, :vote)
+
+    post("/reports", ReportController, :create)
+
     get("/scheduled_statuses", ScheduledActivityController, :index)
     get("/scheduled_statuses/:id", ScheduledActivityController, :show)
 
-    get("/lists", ListController, :index)
-    get("/lists/:id", ListController, :show)
-    get("/lists/:id/accounts", ListController, :list_accounts)
+    put("/scheduled_statuses/:id", ScheduledActivityController, :update)
+    delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
 
-    get("/domain_blocks", DomainBlockController, :index)
-
-    get("/filters", FilterController, :index)
-
-    get("/suggestions", SuggestionController, :index)
-
-    get("/conversations", ConversationController, :index)
-    post("/conversations/:id/read", ConversationController, :read)
-
-    get("/endorsements", AccountController, :endorsements)
-
-    patch("/accounts/update_credentials", AccountController, :update_credentials)
+    # Unlike `GET /api/v1/accounts/:id/favourites`, demands authentication
+    get("/favourites", StatusController, :favourites)
+    get("/bookmarks", StatusController, :bookmarks)
 
     post("/statuses", StatusController, :create)
     delete("/statuses/:id", StatusController, :delete)
-
     post("/statuses/:id/reblog", StatusController, :reblog)
     post("/statuses/:id/unreblog", StatusController, :unreblog)
     post("/statuses/:id/favourite", StatusController, :favourite)
@@ -380,49 +425,16 @@ defmodule Pleroma.Web.Router do
     post("/statuses/:id/mute", StatusController, :mute_conversation)
     post("/statuses/:id/unmute", StatusController, :unmute_conversation)
 
-    put("/scheduled_statuses/:id", ScheduledActivityController, :update)
-    delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
-
-    post("/polls/:id/votes", PollController, :vote)
-
-    post("/media", MediaController, :create)
-    put("/media/:id", MediaController, :update)
-
-    delete("/lists/:id", ListController, :delete)
-    post("/lists", ListController, :create)
-    put("/lists/:id", ListController, :update)
-
-    post("/lists/:id/accounts", ListController, :add_to_list)
-    delete("/lists/:id/accounts", ListController, :remove_from_list)
-
-    post("/filters", FilterController, :create)
-    get("/filters/:id", FilterController, :show)
-    put("/filters/:id", FilterController, :update)
-    delete("/filters/:id", FilterController, :delete)
-
-    post("/reports", ReportController, :create)
-
-    post("/follows", AccountController, :follows)
-    post("/accounts/:id/follow", AccountController, :follow)
-    post("/accounts/:id/unfollow", AccountController, :unfollow)
-    post("/accounts/:id/block", AccountController, :block)
-    post("/accounts/:id/unblock", AccountController, :unblock)
-    post("/accounts/:id/mute", AccountController, :mute)
-    post("/accounts/:id/unmute", AccountController, :unmute)
-
-    post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
-    post("/follow_requests/:id/reject", FollowRequestController, :reject)
-
-    post("/domain_blocks", DomainBlockController, :create)
-    delete("/domain_blocks", DomainBlockController, :delete)
-
     post("/push/subscription", SubscriptionController, :create)
     get("/push/subscription", SubscriptionController, :get)
     put("/push/subscription", SubscriptionController, :update)
     delete("/push/subscription", SubscriptionController, :delete)
 
-    get("/markers", MarkerController, :index)
-    post("/markers", MarkerController, :upsert)
+    get("/suggestions", SuggestionController, :index)
+
+    get("/timelines/home", TimelineController, :home)
+    get("/timelines/direct", TimelineController, :direct)
+    get("/timelines/list/:list_id", TimelineController, :list)
   end
 
   scope "/api/web", Pleroma.Web do
@@ -434,15 +446,24 @@ defmodule Pleroma.Web.Router do
   scope "/api/v1", Pleroma.Web.MastodonAPI do
     pipe_through(:api)
 
-    post("/accounts", AccountController, :create)
     get("/accounts/search", SearchController, :account_search)
+    get("/search", SearchController, :search)
+
+    get("/accounts/:id/statuses", AccountController, :statuses)
+    get("/accounts/:id/followers", AccountController, :followers)
+    get("/accounts/:id/following", AccountController, :following)
+    get("/accounts/:id", AccountController, :show)
+
+    post("/accounts", AccountController, :create)
 
     get("/instance", InstanceController, :show)
     get("/instance/peers", InstanceController, :peers)
 
     post("/apps", AppController, :create)
-    get("/apps/verify_credentials", AppController, :verify_credentials)
 
+    get("/statuses", StatusController, :index)
+    get("/statuses/:id", StatusController, :show)
+    get("/statuses/:id/context", StatusController, :context)
     get("/statuses/:id/card", StatusController, :card)
     get("/statuses/:id/favourited_by", StatusController, :favourited_by)
     get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
@@ -453,20 +474,8 @@ defmodule Pleroma.Web.Router do
 
     get("/timelines/public", TimelineController, :public)
     get("/timelines/tag/:tag", TimelineController, :hashtag)
-    get("/timelines/list/:list_id", TimelineController, :list)
-
-    get("/statuses", StatusController, :index)
-    get("/statuses/:id", StatusController, :show)
-    get("/statuses/:id/context", StatusController, :context)
 
     get("/polls/:id", PollController, :show)
-
-    get("/accounts/:id/statuses", AccountController, :statuses)
-    get("/accounts/:id/followers", AccountController, :followers)
-    get("/accounts/:id/following", AccountController, :following)
-    get("/accounts/:id", AccountController, :show)
-
-    get("/search", SearchController, :search)
   end
 
   scope "/api/v2", Pleroma.Web.MastodonAPI do
@@ -507,7 +516,11 @@ defmodule Pleroma.Web.Router do
     get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
     delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
 
-    post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
+    post(
+      "/qvitter/statuses/notifications/read",
+      TwitterAPI.Controller,
+      :mark_notifications_as_read
+    )
   end
 
   pipeline :ostatus do
@@ -647,11 +660,28 @@ defmodule Pleroma.Web.Router do
 
   # Test-only routes needed to test action dispatching and plug chain execution
   if Pleroma.Config.get(:env) == :test do
+    @test_actions [
+      :do_oauth_check,
+      :fallback_oauth_check,
+      :skip_oauth_check,
+      :fallback_oauth_skip_publicity_check,
+      :skip_oauth_skip_publicity_check,
+      :missing_oauth_check_definition
+    ]
+
+    scope "/test/api", Pleroma.Tests do
+      pipe_through(:api)
+
+      for action <- @test_actions do
+        get("/#{action}", AuthTestController, action)
+      end
+    end
+
     scope "/test/authenticated_api", Pleroma.Tests do
       pipe_through(:authenticated_api)
 
-      for action <- [:skipped_oauth, :performed_oauth, :missed_oauth] do
-        get("/#{action}", OAuthTestController, action)
+      for action <- @test_actions do
+        get("/#{action}", AuthTestController, action)
       end
     end
   end
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index d5d5ce08f..fd2aee175 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -25,13 +25,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
     when action == :follow_import
   )
 
-  # Note: follower can submit the form (with password auth) not being signed in (having no token)
-  plug(
-    OAuthScopesPlug,
-    %{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]}
-    when action == :do_remote_follow
-  )
-
   plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks_import)
 
   plug(
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 31adc2817..c2de26b0b 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
   use Pleroma.Web, :controller
 
   alias Pleroma.Notification
+  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
   alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.User
   alias Pleroma.Web.OAuth.Token
@@ -13,12 +14,18 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
 
   require Logger
 
-  plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read)
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read
+  )
+
+  plug(
+    :skip_plug,
+    [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirm_email
+  )
 
   plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token])
 
-  plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
   action_fallback(:errors)
 
   def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
@@ -46,13 +53,13 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
     json_reply(conn, 201, "")
   end
 
-  def errors(conn, {:param_cast, _}) do
+  defp errors(conn, {:param_cast, _}) do
     conn
     |> put_status(400)
     |> json("Invalid parameters")
   end
 
-  def errors(conn, _) do
+  defp errors(conn, _) do
     conn
     |> put_status(500)
     |> json("Something went wrong")
@@ -64,7 +71,10 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
     |> send_resp(status, json)
   end
 
-  def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
+  def mark_notifications_as_read(
+        %{assigns: %{user: user}} = conn,
+        %{"latest_id" => latest_id} = params
+      ) do
     Notification.set_read_up_to(user, latest_id)
 
     notifications = Notification.for_user(user, params)
@@ -75,7 +85,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
     |> render("index.json", %{notifications: notifications, for: user})
   end
 
-  def notifications_read(%{assigns: %{user: _user}} = conn, _) do
+  def mark_notifications_as_read(%{assigns: %{user: _user}} = conn, _) do
     bad_request_reply(conn, "You need to specify latest_id")
   end
 
diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex
index bf48ce26c..08e42a7e5 100644
--- a/lib/pleroma/web/web.ex
+++ b/lib/pleroma/web/web.ex
@@ -2,6 +2,11 @@
 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
+defmodule Pleroma.Web.Plug do
+  # Substitute for `call/2` which is defined with `use Pleroma.Web, :plug`
+  @callback perform(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
+end
+
 defmodule Pleroma.Web do
   @moduledoc """
   A module that keeps using definitions for controllers,
@@ -20,44 +25,91 @@ defmodule Pleroma.Web do
   below.
   """
 
+  alias Pleroma.Plugs.EnsureAuthenticatedPlug
+  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
+  alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug
+  alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug
+  alias Pleroma.Plugs.OAuthScopesPlug
+  alias Pleroma.Plugs.PlugHelper
+
   def controller do
     quote do
       use Phoenix.Controller, namespace: Pleroma.Web
 
       import Plug.Conn
+
       import Pleroma.Web.Gettext
       import Pleroma.Web.Router.Helpers
       import Pleroma.Web.TranslationHelpers
 
-      alias Pleroma.Plugs.PlugHelper
-
       plug(:set_put_layout)
 
       defp set_put_layout(conn, _) do
         put_layout(conn, Pleroma.Config.get(:app_layout, "app.html"))
       end
 
-      # Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain
-      defp skip_plug(conn, plug_module) do
-        try do
-          plug_module.skip_plug(conn)
-        rescue
-          UndefinedFunctionError ->
-            raise "#{plug_module} is not skippable. Append `use Pleroma.Web, :plug` to its code."
-        end
+      # Marks plugs intentionally skipped and blocks their execution if present in plugs chain
+      defp skip_plug(conn, plug_modules) do
+        plug_modules
+        |> List.wrap()
+        |> Enum.reduce(
+          conn,
+          fn plug_module, conn ->
+            try do
+              plug_module.skip_plug(conn)
+            rescue
+              UndefinedFunctionError ->
+                raise "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code."
+            end
+          end
+        )
       end
 
       # Executed just before actual controller action, invokes before-action hooks (callbacks)
       defp action(conn, params) do
-        with %Plug.Conn{halted: false} <- maybe_halt_on_missing_oauth_scopes_check(conn) do
+        with %{halted: false} = conn <- maybe_drop_authentication_if_oauth_check_ignored(conn),
+             %{halted: false} = conn <- maybe_perform_public_or_authenticated_check(conn),
+             %{halted: false} = conn <- maybe_perform_authenticated_check(conn),
+             %{halted: false} = conn <- maybe_halt_on_missing_oauth_scopes_check(conn) do
           super(conn, params)
         end
       end
 
+      # For non-authenticated API actions, drops auth info if OAuth scopes check was ignored
+      #   (neither performed nor explicitly skipped)
+      defp maybe_drop_authentication_if_oauth_check_ignored(conn) do
+        if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and
+             not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
+          OAuthScopesPlug.drop_auth_info(conn)
+        else
+          conn
+        end
+      end
+
+      # Ensures instance is public -or- user is authenticated if such check was scheduled
+      defp maybe_perform_public_or_authenticated_check(conn) do
+        if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) do
+          EnsurePublicOrAuthenticatedPlug.call(conn, %{})
+        else
+          conn
+        end
+      end
+
+      # Ensures user is authenticated if such check was scheduled
+      # Note: runs prior to action even if it was already executed earlier in plug chain
+      #   (since OAuthScopesPlug has option of proceeding unauthenticated)
+      defp maybe_perform_authenticated_check(conn) do
+        if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) do
+          EnsureAuthenticatedPlug.call(conn, %{})
+        else
+          conn
+        end
+      end
+
       # Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check
       defp maybe_halt_on_missing_oauth_scopes_check(conn) do
-        if Pleroma.Plugs.AuthExpectedPlug.auth_expected?(conn) &&
-             not PlugHelper.plug_called_or_skipped?(conn, Pleroma.Plugs.OAuthScopesPlug) do
+        if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and
+             not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
           conn
           |> render_error(
             :forbidden,
@@ -132,7 +184,8 @@ defmodule Pleroma.Web do
 
   def plug do
     quote do
-      alias Pleroma.Plugs.PlugHelper
+      @behaviour Pleroma.Web.Plug
+      @behaviour Plug
 
       @doc """
       Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain.
@@ -146,14 +199,22 @@ defmodule Pleroma.Web do
       end
 
       @impl Plug
-      @doc "If marked as skipped, returns `conn`, and calls `perform/2` otherwise."
+      @doc """
+      If marked as skipped, returns `conn`, otherwise calls `perform/2`.
+      Note: multiple invocations of the same plug (with different or same options) are allowed.
+      """
       def call(%Plug.Conn{} = conn, options) do
         if PlugHelper.plug_skipped?(conn, __MODULE__) do
           conn
         else
-          conn
-          |> PlugHelper.append_to_private_list(PlugHelper.called_plugs_list_id(), __MODULE__)
-          |> perform(options)
+          conn =
+            PlugHelper.append_to_private_list(
+              conn,
+              PlugHelper.called_plugs_list_id(),
+              __MODULE__
+            )
+
+          apply(__MODULE__, :perform, [conn, options])
         end
       end
     end
diff --git a/test/plugs/ensure_authenticated_plug_test.exs b/test/plugs/ensure_authenticated_plug_test.exs
index 7f3559b83..689fe757f 100644
--- a/test/plugs/ensure_authenticated_plug_test.exs
+++ b/test/plugs/ensure_authenticated_plug_test.exs
@@ -20,7 +20,7 @@ defmodule Pleroma.Plugs.EnsureAuthenticatedPlugTest do
       conn = assign(conn, :user, %User{})
       ret_conn = EnsureAuthenticatedPlug.call(conn, %{})
 
-      assert ret_conn == conn
+      refute ret_conn.halted
     end
   end
 
@@ -34,20 +34,22 @@ defmodule Pleroma.Plugs.EnsureAuthenticatedPlugTest do
 
     test "it continues if a user is assigned", %{conn: conn, true_fn: true_fn, false_fn: false_fn} do
       conn = assign(conn, :user, %User{})
-      assert EnsureAuthenticatedPlug.call(conn, if_func: true_fn) == conn
-      assert EnsureAuthenticatedPlug.call(conn, if_func: false_fn) == conn
-      assert EnsureAuthenticatedPlug.call(conn, unless_func: true_fn) == conn
-      assert EnsureAuthenticatedPlug.call(conn, unless_func: false_fn) == conn
+      refute EnsureAuthenticatedPlug.call(conn, if_func: true_fn).halted
+      refute EnsureAuthenticatedPlug.call(conn, if_func: false_fn).halted
+      refute EnsureAuthenticatedPlug.call(conn, unless_func: true_fn).halted
+      refute EnsureAuthenticatedPlug.call(conn, unless_func: false_fn).halted
     end
 
     test "it continues if a user is NOT assigned but :if_func evaluates to `false`",
          %{conn: conn, false_fn: false_fn} do
-      assert EnsureAuthenticatedPlug.call(conn, if_func: false_fn) == conn
+      ret_conn = EnsureAuthenticatedPlug.call(conn, if_func: false_fn)
+      refute ret_conn.halted
     end
 
     test "it continues if a user is NOT assigned but :unless_func evaluates to `true`",
          %{conn: conn, true_fn: true_fn} do
-      assert EnsureAuthenticatedPlug.call(conn, unless_func: true_fn) == conn
+      ret_conn = EnsureAuthenticatedPlug.call(conn, unless_func: true_fn)
+      refute ret_conn.halted
     end
 
     test "it halts if a user is NOT assigned and :if_func evaluates to `true`",
diff --git a/test/plugs/ensure_public_or_authenticated_plug_test.exs b/test/plugs/ensure_public_or_authenticated_plug_test.exs
index 411252274..fc2934369 100644
--- a/test/plugs/ensure_public_or_authenticated_plug_test.exs
+++ b/test/plugs/ensure_public_or_authenticated_plug_test.exs
@@ -29,7 +29,7 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
       conn
       |> EnsurePublicOrAuthenticatedPlug.call(%{})
 
-    assert ret_conn == conn
+    refute ret_conn.halted
   end
 
   test "it continues if a user is assigned, even if not public", %{conn: conn} do
@@ -43,6 +43,6 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
       conn
       |> EnsurePublicOrAuthenticatedPlug.call(%{})
 
-    assert ret_conn == conn
+    refute ret_conn.halted
   end
 end
diff --git a/test/plugs/oauth_scopes_plug_test.exs b/test/plugs/oauth_scopes_plug_test.exs
index edbc94227..884de7b4d 100644
--- a/test/plugs/oauth_scopes_plug_test.exs
+++ b/test/plugs/oauth_scopes_plug_test.exs
@@ -5,17 +5,12 @@
 defmodule Pleroma.Plugs.OAuthScopesPlugTest do
   use Pleroma.Web.ConnCase, async: true
 
-  alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
   alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.Repo
 
   import Mock
   import Pleroma.Factory
 
-  setup_with_mocks([{EnsurePublicOrAuthenticatedPlug, [], [call: fn conn, _ -> conn end]}]) do
-    :ok
-  end
-
   test "is not performed if marked as skipped", %{conn: conn} do
     with_mock OAuthScopesPlug, [:passthrough], perform: &passthrough([&1, &2]) do
       conn =
@@ -60,7 +55,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
 
   describe "with `fallback: :proceed_unauthenticated` option, " do
     test "if `token.scopes` doesn't fulfill specified conditions, " <>
-           "clears :user and :token assigns and calls EnsurePublicOrAuthenticatedPlug",
+           "clears :user and :token assigns",
          %{conn: conn} do
       user = insert(:user)
       token1 = insert(:oauth_token, scopes: ["read", "write"], user: user)
@@ -79,35 +74,6 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
         refute ret_conn.halted
         refute ret_conn.assigns[:user]
         refute ret_conn.assigns[:token]
-
-        assert called(EnsurePublicOrAuthenticatedPlug.call(ret_conn, :_))
-      end
-    end
-
-    test "with :skip_instance_privacy_check option, " <>
-           "if `token.scopes` doesn't fulfill specified conditions, " <>
-           "clears :user and :token assigns and does NOT call EnsurePublicOrAuthenticatedPlug",
-         %{conn: conn} do
-      user = insert(:user)
-      token1 = insert(:oauth_token, scopes: ["read:statuses", "write"], user: user)
-
-      for token <- [token1, nil], op <- [:|, :&] do
-        ret_conn =
-          conn
-          |> assign(:user, user)
-          |> assign(:token, token)
-          |> OAuthScopesPlug.call(%{
-            scopes: ["read"],
-            op: op,
-            fallback: :proceed_unauthenticated,
-            skip_instance_privacy_check: true
-          })
-
-        refute ret_conn.halted
-        refute ret_conn.assigns[:user]
-        refute ret_conn.assigns[:token]
-
-        refute called(EnsurePublicOrAuthenticatedPlug.call(ret_conn, :_))
       end
     end
   end
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs
index fbacb3993..eca526604 100644
--- a/test/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/web/activity_pub/activity_pub_controller_test.exs
@@ -766,7 +766,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
   end
 
   describe "POST /users/:nickname/outbox" do
-    test "it rejects posts from other users / unauuthenticated users", %{conn: conn} do
+    test "it rejects posts from other users / unauthenticated users", %{conn: conn} do
       data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
       user = insert(:user)
       other_user = insert(:user)
diff --git a/test/web/auth/auth_test_controller_test.exs b/test/web/auth/auth_test_controller_test.exs
new file mode 100644
index 000000000..fed52b7f3
--- /dev/null
+++ b/test/web/auth/auth_test_controller_test.exs
@@ -0,0 +1,242 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Tests.AuthTestControllerTest do
+  use Pleroma.Web.ConnCase
+
+  import Pleroma.Factory
+
+  describe "do_oauth_check" do
+    test "serves with proper OAuth token (fulfilling requested scopes)" do
+      %{conn: good_token_conn, user: user} = oauth_access(["read"])
+
+      assert %{"user_id" => user.id} ==
+               good_token_conn
+               |> get("/test/authenticated_api/do_oauth_check")
+               |> json_response(200)
+
+      # Unintended usage (:api) — use with :authenticated_api instead
+      assert %{"user_id" => user.id} ==
+               good_token_conn
+               |> get("/test/api/do_oauth_check")
+               |> json_response(200)
+    end
+
+    test "fails on no token / missing scope(s)" do
+      %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"])
+
+      bad_token_conn
+      |> get("/test/authenticated_api/do_oauth_check")
+      |> json_response(403)
+
+      bad_token_conn
+      |> assign(:token, nil)
+      |> get("/test/api/do_oauth_check")
+      |> json_response(403)
+    end
+  end
+
+  describe "fallback_oauth_check" do
+    test "serves with proper OAuth token (fulfilling requested scopes)" do
+      %{conn: good_token_conn, user: user} = oauth_access(["read"])
+
+      assert %{"user_id" => user.id} ==
+               good_token_conn
+               |> get("/test/api/fallback_oauth_check")
+               |> json_response(200)
+
+      # Unintended usage (:authenticated_api) — use with :api instead
+      assert %{"user_id" => user.id} ==
+               good_token_conn
+               |> get("/test/authenticated_api/fallback_oauth_check")
+               |> json_response(200)
+    end
+
+    test "for :api on public instance, drops :user and renders on no token / missing scope(s)" do
+      clear_config([:instance, :public], true)
+
+      %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"])
+
+      assert %{"user_id" => nil} ==
+               bad_token_conn
+               |> get("/test/api/fallback_oauth_check")
+               |> json_response(200)
+
+      assert %{"user_id" => nil} ==
+               bad_token_conn
+               |> assign(:token, nil)
+               |> get("/test/api/fallback_oauth_check")
+               |> json_response(200)
+    end
+
+    test "for :api on private instance, fails on no token / missing scope(s)" do
+      clear_config([:instance, :public], false)
+
+      %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"])
+
+      bad_token_conn
+      |> get("/test/api/fallback_oauth_check")
+      |> json_response(403)
+
+      bad_token_conn
+      |> assign(:token, nil)
+      |> get("/test/api/fallback_oauth_check")
+      |> json_response(403)
+    end
+  end
+
+  describe "skip_oauth_check" do
+    test "for :authenticated_api, serves if :user is set (regardless of token / token scopes)" do
+      user = insert(:user)
+
+      assert %{"user_id" => user.id} ==
+               build_conn()
+               |> assign(:user, user)
+               |> get("/test/authenticated_api/skip_oauth_check")
+               |> json_response(200)
+
+      %{conn: bad_token_conn, user: user} = oauth_access(["irrelevant_scope"])
+
+      assert %{"user_id" => user.id} ==
+               bad_token_conn
+               |> get("/test/authenticated_api/skip_oauth_check")
+               |> json_response(200)
+    end
+
+    test "serves via :api on public instance if :user is not set" do
+      clear_config([:instance, :public], true)
+
+      assert %{"user_id" => nil} ==
+               build_conn()
+               |> get("/test/api/skip_oauth_check")
+               |> json_response(200)
+
+      build_conn()
+      |> get("/test/authenticated_api/skip_oauth_check")
+      |> json_response(403)
+    end
+
+    test "fails on private instance if :user is not set" do
+      clear_config([:instance, :public], false)
+
+      build_conn()
+      |> get("/test/api/skip_oauth_check")
+      |> json_response(403)
+
+      build_conn()
+      |> get("/test/authenticated_api/skip_oauth_check")
+      |> json_response(403)
+    end
+  end
+
+  describe "fallback_oauth_skip_publicity_check" do
+    test "serves with proper OAuth token (fulfilling requested scopes)" do
+      %{conn: good_token_conn, user: user} = oauth_access(["read"])
+
+      assert %{"user_id" => user.id} ==
+               good_token_conn
+               |> get("/test/api/fallback_oauth_skip_publicity_check")
+               |> json_response(200)
+
+      # Unintended usage (:authenticated_api)
+      assert %{"user_id" => user.id} ==
+               good_token_conn
+               |> get("/test/authenticated_api/fallback_oauth_skip_publicity_check")
+               |> json_response(200)
+    end
+
+    test "for :api on private / public instance, drops :user and renders on token issue" do
+      %{conn: bad_token_conn} = oauth_access(["irrelevant_scope"])
+
+      for is_public <- [true, false] do
+        clear_config([:instance, :public], is_public)
+
+        assert %{"user_id" => nil} ==
+                 bad_token_conn
+                 |> get("/test/api/fallback_oauth_skip_publicity_check")
+                 |> json_response(200)
+
+        assert %{"user_id" => nil} ==
+                 bad_token_conn
+                 |> assign(:token, nil)
+                 |> get("/test/api/fallback_oauth_skip_publicity_check")
+                 |> json_response(200)
+      end
+    end
+  end
+
+  describe "skip_oauth_skip_publicity_check" do
+    test "for :authenticated_api, serves if :user is set (regardless of token / token scopes)" do
+      user = insert(:user)
+
+      assert %{"user_id" => user.id} ==
+               build_conn()
+               |> assign(:user, user)
+               |> get("/test/authenticated_api/skip_oauth_skip_publicity_check")
+               |> json_response(200)
+
+      %{conn: bad_token_conn, user: user} = oauth_access(["irrelevant_scope"])
+
+      assert %{"user_id" => user.id} ==
+               bad_token_conn
+               |> get("/test/authenticated_api/skip_oauth_skip_publicity_check")
+               |> json_response(200)
+    end
+
+    test "for :api, serves on private and public instances regardless of whether :user is set" do
+      user = insert(:user)
+
+      for is_public <- [true, false] do
+        clear_config([:instance, :public], is_public)
+
+        assert %{"user_id" => nil} ==
+                 build_conn()
+                 |> get("/test/api/skip_oauth_skip_publicity_check")
+                 |> json_response(200)
+
+        assert %{"user_id" => user.id} ==
+                 build_conn()
+                 |> assign(:user, user)
+                 |> get("/test/api/skip_oauth_skip_publicity_check")
+                 |> json_response(200)
+      end
+    end
+  end
+
+  describe "missing_oauth_check_definition" do
+    def test_missing_oauth_check_definition_failure(endpoint, expected_error) do
+      %{conn: conn} = oauth_access(["read", "write", "follow", "push", "admin"])
+
+      assert %{"error" => expected_error} ==
+               conn
+               |> get(endpoint)
+               |> json_response(403)
+    end
+
+    test "fails if served via :authenticated_api" do
+      test_missing_oauth_check_definition_failure(
+        "/test/authenticated_api/missing_oauth_check_definition",
+        "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
+      )
+    end
+
+    test "fails if served via :api and the instance is private" do
+      clear_config([:instance, :public], false)
+
+      test_missing_oauth_check_definition_failure(
+        "/test/api/missing_oauth_check_definition",
+        "This resource requires authentication."
+      )
+    end
+
+    test "succeeds with dropped :user if served via :api on public instance" do
+      %{conn: conn} = oauth_access(["read", "write", "follow", "push", "admin"])
+
+      assert %{"user_id" => nil} ==
+               conn
+               |> get("/test/api/missing_oauth_check_definition")
+               |> json_response(200)
+    end
+  end
+end
diff --git a/test/web/auth/oauth_test_controller_test.exs b/test/web/auth/oauth_test_controller_test.exs
deleted file mode 100644
index a2f6009ac..000000000
--- a/test/web/auth/oauth_test_controller_test.exs
+++ /dev/null
@@ -1,49 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Tests.OAuthTestControllerTest do
-  use Pleroma.Web.ConnCase
-
-  import Pleroma.Factory
-
-  setup %{conn: conn} do
-    user = insert(:user)
-    conn = assign(conn, :user, user)
-    %{conn: conn, user: user}
-  end
-
-  test "missed_oauth", %{conn: conn} do
-    res =
-      conn
-      |> get("/test/authenticated_api/missed_oauth")
-      |> json_response(403)
-
-    assert res ==
-             %{
-               "error" =>
-                 "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
-             }
-  end
-
-  test "skipped_oauth", %{conn: conn} do
-    conn
-    |> assign(:token, nil)
-    |> get("/test/authenticated_api/skipped_oauth")
-    |> json_response(200)
-  end
-
-  test "performed_oauth", %{user: user} do
-    %{conn: good_token_conn} = oauth_access(["read"], user: user)
-
-    good_token_conn
-    |> get("/test/authenticated_api/performed_oauth")
-    |> json_response(200)
-
-    %{conn: bad_token_conn} = oauth_access(["follow"], user: user)
-
-    bad_token_conn
-    |> get("/test/authenticated_api/performed_oauth")
-    |> json_response(403)
-  end
-end
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 75f184242..bb4bc4396 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -7,35 +7,28 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
 
   describe "empty_array/2 (stubs)" do
     test "GET /api/v1/accounts/:id/identity_proofs" do
-      %{user: user, conn: conn} = oauth_access(["n/a"])
+      %{user: user, conn: conn} = oauth_access(["read:accounts"])
 
-      res =
-        conn
-        |> assign(:user, user)
-        |> get("/api/v1/accounts/#{user.id}/identity_proofs")
-        |> json_response(200)
-
-      assert res == []
+      assert [] ==
+               conn
+               |> get("/api/v1/accounts/#{user.id}/identity_proofs")
+               |> json_response(200)
     end
 
     test "GET /api/v1/endorsements" do
       %{conn: conn} = oauth_access(["read:accounts"])
 
-      res =
-        conn
-        |> get("/api/v1/endorsements")
-        |> json_response(200)
-
-      assert res == []
+      assert [] ==
+               conn
+               |> get("/api/v1/endorsements")
+               |> json_response(200)
     end
 
     test "GET /api/v1/trends", %{conn: conn} do
-      res =
-        conn
-        |> get("/api/v1/trends")
-        |> json_response(200)
-
-      assert res == []
+      assert [] ==
+               conn
+               |> get("/api/v1/trends")
+               |> json_response(200)
     end
   end
 end
diff --git a/test/web/pleroma_api/controllers/account_controller_test.exs b/test/web/pleroma_api/controllers/account_controller_test.exs
index ae5334015..6b671a667 100644
--- a/test/web/pleroma_api/controllers/account_controller_test.exs
+++ b/test/web/pleroma_api/controllers/account_controller_test.exs
@@ -151,15 +151,18 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
       assert like["id"] == activity.id
     end
 
-    test "does not return favorites for specified user_id when user is not logged in", %{
+    test "returns favorites for specified user_id when requester is not logged in", %{
       user: user
     } do
       activity = insert(:note_activity)
       CommonAPI.favorite(user, activity.id)
 
-      build_conn()
-      |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
-      |> json_response(403)
+      response =
+        build_conn()
+        |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+        |> json_response(200)
+
+      assert length(response) == 1
     end
 
     test "returns favorited DM only when user is logged in and he is one of recipients", %{
@@ -185,9 +188,12 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
         assert length(response) == 1
       end
 
-      build_conn()
-      |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
-      |> json_response(403)
+      response =
+        build_conn()
+        |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
+        |> json_response(200)
+
+      assert length(response) == 0
     end
 
     test "does not return others' favorited DM when user is not one of recipients", %{
diff --git a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
index 435fb6592..4246eb400 100644
--- a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
+++ b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
@@ -38,8 +38,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
   end
 
   test "listing remote packs" do
-    admin = insert(:user, is_admin: true)
-    %{conn: conn} = oauth_access(["admin:write"], user: admin)
+    conn = build_conn()
 
     resp =
       build_conn()
@@ -76,7 +75,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
     assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end)
   end
 
-  test "downloading shared & unshared packs from another instance via download_from, deleting them" do
+  test "downloading shared & unshared packs from another instance, deleting them" do
     on_exit(fn ->
       File.rm_rf!("#{@emoji_dir_path}/test_pack2")
       File.rm_rf!("#{@emoji_dir_path}/test_pack_nonshared2")
@@ -136,7 +135,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
             |> post(
               emoji_api_path(
                 conn,
-                :download_from
+                :save_from
               ),
               %{
                 instance_address: "https://old-instance",
@@ -152,7 +151,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
            |> post(
              emoji_api_path(
                conn,
-               :download_from
+               :save_from
              ),
              %{
                instance_address: "https://example.com",
@@ -179,7 +178,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
            |> post(
              emoji_api_path(
                conn,
-               :download_from
+               :save_from
              ),
              %{
                instance_address: "https://example.com",
diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs
index ab0a2c3df..464d0ea2e 100644
--- a/test/web/twitter_api/twitter_api_controller_test.exs
+++ b/test/web/twitter_api/twitter_api_controller_test.exs
@@ -19,13 +19,9 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
     end
 
     test "with credentials, without any params" do
-      %{user: current_user, conn: conn} =
-        oauth_access(["read:notifications", "write:notifications"])
+      %{conn: conn} = oauth_access(["write:notifications"])
 
-      conn =
-        conn
-        |> assign(:user, current_user)
-        |> post("/api/qvitter/statuses/notifications/read")
+      conn = post(conn, "/api/qvitter/statuses/notifications/read")
 
       assert json_response(conn, 400) == %{
                "error" => "You need to specify latest_id",