Merge branch 'stable' into stable-sync/2.1.2
[akkoma] / lib / pleroma / web / twitter_api / controllers / remote_follow_controller.ex
index 460a42566147c02301c06f53e8290e3de9bff40a..072d889e2e54e7a9b5bdd105ce95e6290490ae0a 100644 (file)
@@ -1,5 +1,5 @@
 # Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
@@ -8,15 +8,24 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
   require Logger
 
   alias Pleroma.Activity
+  alias Pleroma.MFA
   alias Pleroma.Object.Fetcher
   alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.User
   alias Pleroma.Web.Auth.Authenticator
+  alias Pleroma.Web.Auth.TOTPAuthenticator
   alias Pleroma.Web.CommonAPI
 
   @status_types ["Article", "Event", "Note", "Video", "Page", "Question"]
 
-  plug(OAuthScopesPlug, %{scopes: ["follow", "write:follows"]} when action in [:do_follow])
+  plug(Pleroma.Web.FederatingPlug)
+
+  # 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 in [:do_follow]
+  )
 
   # GET /ostatus_subscribe
   #
@@ -30,7 +39,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
   defp follow_status(conn, _user, acct) do
     with {:ok, object} <- Fetcher.fetch_object_from_id(acct),
          %Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(object.data["id"]) do
-      redirect(conn, to: "/notice/#{activity_id}")
+      redirect(conn, to: o_status_path(conn, :notice, activity_id))
     else
       error ->
         handle_follow_error(conn, error)
@@ -61,27 +70,75 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
 
   # POST  /ostatus_subscribe
   #
-  def do_follow(conn, %{"authorization" => %{"name" => _, "password" => _, "id" => id}}) do
+  # adds a remote account in followers if user already is signed in.
+  #
+  def do_follow(%{assigns: %{user: %User{} = user}} = conn, %{"user" => %{"id" => id}}) do
     with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
+         {:ok, _, _, _} <- CommonAPI.follow(user, followee) do
+      redirect(conn, to: "/users/#{followee.id}")
+    else
+      error ->
+        handle_follow_error(conn, error)
+    end
+  end
+
+  # POST  /ostatus_subscribe
+  #
+  # step 1.
+  # checks login\password and displays step 2 form of MFA if need.
+  #
+  def do_follow(conn, %{"authorization" => %{"name" => _, "password" => _, "id" => id}}) do
+    with {_, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
          {_, {:ok, user}, _} <- {:auth, Authenticator.get_user(conn), followee},
+         {_, _, _, false} <- {:mfa_required, followee, user, MFA.require?(user)},
          {:ok, _, _, _} <- CommonAPI.follow(user, followee) do
-      render(conn, "followed.html", %{error: false})
+      redirect(conn, to: "/users/#{followee.id}")
     else
       error ->
         handle_follow_error(conn, error)
     end
   end
 
-  def do_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do
-    with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
+  # POST  /ostatus_subscribe
+  #
+  # step 2
+  # checks TOTP code. otherwise displays form with errors
+  #
+  def do_follow(conn, %{"mfa" => %{"code" => code, "token" => token, "id" => id}}) do
+    with {_, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
+         {_, _, {:ok, %{user: user}}} <- {:mfa_token, followee, MFA.Token.validate(token)},
+         {_, _, _, {:ok, _}} <-
+           {:verify_mfa_code, followee, token, TOTPAuthenticator.verify(code, user)},
          {:ok, _, _, _} <- CommonAPI.follow(user, followee) do
-      render(conn, "followed.html", %{error: false})
+      redirect(conn, to: "/users/#{followee.id}")
     else
       error ->
         handle_follow_error(conn, error)
     end
   end
 
+  def do_follow(%{assigns: %{user: nil}} = conn, _) do
+    Logger.debug("Insufficient permissions: follow | write:follows.")
+    render(conn, "followed.html", %{error: "Insufficient permissions: follow | write:follows."})
+  end
+
+  defp handle_follow_error(conn, {:mfa_token, followee, _} = _) do
+    render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee})
+  end
+
+  defp handle_follow_error(conn, {:verify_mfa_code, followee, token, _} = _) do
+    render(conn, "follow_mfa.html", %{
+      error: "Wrong authentication code",
+      followee: followee,
+      mfa_token: token
+    })
+  end
+
+  defp handle_follow_error(conn, {:mfa_required, followee, user, _} = _) do
+    {:ok, %{token: token}} = MFA.Token.create(user)
+    render(conn, "follow_mfa.html", %{followee: followee, mfa_token: token, error: false})
+  end
+
   defp handle_follow_error(conn, {:auth, _, followee} = _) do
     render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee})
   end