Merge develop
[akkoma] / lib / pleroma / web / oauth / oauth_controller.ex
index 0121cd661001211b45f509f0bce2344f6f9f0800..26e68be428461fb56af9f1aca878dc74e5ef1d98 100644 (file)
@@ -6,6 +6,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
   use Pleroma.Web, :controller
 
   alias Pleroma.Helpers.UriHelper
+  alias Pleroma.Maps
+  alias Pleroma.MFA
   alias Pleroma.Plugs.RateLimiter
   alias Pleroma.Registration
   alias Pleroma.Repo
@@ -14,6 +16,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do
   alias Pleroma.Web.ControllerHelper
   alias Pleroma.Web.OAuth.App
   alias Pleroma.Web.OAuth.Authorization
+  alias Pleroma.Web.OAuth.MFAController
+  alias Pleroma.Web.OAuth.MFAView
+  alias Pleroma.Web.OAuth.OAuthView
   alias Pleroma.Web.OAuth.Scopes
   alias Pleroma.Web.OAuth.Token
   alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
@@ -25,9 +30,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)
 
@@ -70,6 +76,13 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     available_scopes = (app && app.scopes) || []
     scopes = Scopes.fetch_scopes(params, available_scopes)
 
+    scopes =
+      if scopes == [] do
+        available_scopes
+      else
+        scopes
+      end
+
     # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
     render(conn, Authenticator.auth_template(), %{
       response_type: params["response_type"],
@@ -105,7 +118,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     if redirect_uri in String.split(app.redirect_uris) do
       redirect_uri = redirect_uri(conn, redirect_uri)
       url_params = %{access_token: token.token}
-      url_params = UriHelper.append_param_if_present(url_params, :state, params["state"])
+      url_params = Maps.put_if_present(url_params, :state, params["state"])
       url = UriHelper.append_uri_params(redirect_uri, url_params)
       redirect(conn, external: url)
     else
@@ -120,7 +133,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
         %{"authorization" => _} = params,
         opts \\ []
       ) do
-    with {:ok, auth} <- do_create_authorization(conn, params, opts[:user]) do
+    with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
+         {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
       after_create_authorization(conn, auth, params)
     else
       error ->
@@ -131,7 +145,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do
   def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
         "authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
       }) do
-    render(conn, "oob_authorization_created.html", %{auth: auth})
+    # Enforcing the view to reuse the template when calling from other controllers
+    conn
+    |> put_view(OAuthView)
+    |> render("oob_authorization_created.html", %{auth: auth})
   end
 
   def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
@@ -143,7 +160,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     if redirect_uri in String.split(app.redirect_uris) do
       redirect_uri = redirect_uri(conn, redirect_uri)
       url_params = %{code: auth.token}
-      url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"])
+      url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
       url = UriHelper.append_uri_params(redirect_uri, url_params)
       redirect(conn, external: url)
     else
@@ -178,6 +195,22 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     |> authorize(params)
   end
 
+  defp handle_create_authorization_error(
+         %Plug.Conn{} = conn,
+         {:mfa_required, user, auth, _},
+         params
+       ) do
+    {:ok, token} = MFA.Token.create(user, auth)
+
+    data = %{
+      "mfa_token" => token.token,
+      "redirect_uri" => params["authorization"]["redirect_uri"],
+      "state" => params["authorization"]["state"]
+    }
+
+    MFAController.show(conn, data)
+  end
+
   defp handle_create_authorization_error(
          %Plug.Conn{} = conn,
          {:account_status, :password_reset_pending},
@@ -212,9 +245,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     with {:ok, app} <- Token.Utils.fetch_app(conn),
          {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
          {:ok, token} <- RefreshToken.grant(token) do
-      response_attrs = %{created_at: Token.Utils.format_created_at(token)}
-
-      json(conn, Token.Response.build(user, token, response_attrs))
+      json(conn, OAuthView.render("token.json", %{user: user, token: token}))
     else
       _error -> render_invalid_credentials_error(conn)
     end
@@ -226,11 +257,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do
          {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
          %User{} = user <- User.get_cached_by_id(auth.user_id),
          {:ok, token} <- Token.exchange_token(app, auth) do
-      response_attrs = %{created_at: Token.Utils.format_created_at(token)}
-
-      json(conn, Token.Response.build(user, token, response_attrs))
+      json(conn, OAuthView.render("token.json", %{user: user, token: token}))
     else
-      _error -> render_invalid_credentials_error(conn)
+      error ->
+        handle_token_exchange_error(conn, error)
     end
   end
 
@@ -240,11 +270,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do
       ) do
     with {:ok, %User{} = user} <- Authenticator.get_user(conn),
          {:ok, app} <- Token.Utils.fetch_app(conn),
-         {:account_status, :active} <- {:account_status, User.account_status(user)},
-         {:ok, scopes} <- validate_scopes(app, params),
-         {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
-         {:ok, token} <- Token.exchange_token(app, auth) do
-      json(conn, Token.Response.build(user, token))
+         requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
+         {:ok, token} <- login(user, app, requested_scopes) do
+      json(conn, OAuthView.render("token.json", %{user: user, token: token}))
     else
       error ->
         handle_token_exchange_error(conn, error)
@@ -267,15 +295,22 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     with {:ok, app} <- Token.Utils.fetch_app(conn),
          {:ok, auth} <- Authorization.create_authorization(app, %User{}),
          {:ok, token} <- Token.exchange_token(app, auth) do
-      json(conn, Token.Response.build_for_client_credentials(token))
+      json(conn, OAuthView.render("token.json", %{token: token}))
     else
-      _error -> render_invalid_credentials_error(conn)
+      _error ->
+        handle_token_exchange_error(conn, :invalid_credentails)
     end
   end
 
   # Bad request
   def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
 
+  defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
+    conn
+    |> put_status(:forbidden)
+    |> json(build_and_response_mfa_token(user, auth))
+  end
+
   defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
     render_error(
       conn,
@@ -309,6 +344,16 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     )
   end
 
+  defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
+    render_error(
+      conn,
+      :forbidden,
+      "Your account is awaiting approval.",
+      %{},
+      "awaiting_approval"
+    )
+  end
+
   defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
     render_invalid_credentials_error(conn)
   end
@@ -433,7 +478,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
   def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
     with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
          %Registration{} = registration <- Repo.get(Registration, registration_id),
-         {_, {:ok, auth}} <- {:create_authorization, do_create_authorization(conn, params)},
+         {_, {:ok, auth, _user}} <-
+           {:create_authorization, do_create_authorization(conn, params)},
          %User{} = user <- Repo.preload(auth, :user).user,
          {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
       conn
@@ -483,6 +529,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     end
   end
 
+  defp do_create_authorization(conn, auth_attrs, user \\ nil)
+
   defp do_create_authorization(
          %Plug.Conn{} = conn,
          %{
@@ -492,15 +540,34 @@ defmodule Pleroma.Web.OAuth.OAuthController do
                "redirect_uri" => redirect_uri
              } = auth_attrs
          },
-         user \\ nil
+         user
        ) do
     with {_, {:ok, %User{} = user}} <-
            {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
          %App{} = app <- Repo.get_by(App, client_id: client_id),
          true <- redirect_uri in String.split(app.redirect_uris),
-         {:ok, scopes} <- validate_scopes(app, auth_attrs),
-         {:account_status, :active} <- {:account_status, User.account_status(user)} do
-      Authorization.create_authorization(app, user, scopes)
+         requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes),
+         {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do
+      {:ok, auth, user}
+    end
+  end
+
+  defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
+       when is_list(requested_scopes) do
+    with {:account_status, :active} <- {:account_status, User.account_status(user)},
+         {:ok, scopes} <- validate_scopes(app, requested_scopes),
+         {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
+      {:ok, auth}
+    end
+  end
+
+  # Note: intended to be a private function but opened for AccountController that logs in on signup
+  @doc "If checks pass, creates authorization and token for given user, app and requested scopes."
+  def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do
+    with {:ok, auth} <- do_create_authorization(user, app, requested_scopes),
+         {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
+         {:ok, token} <- Token.exchange_token(app, auth) do
+      {:ok, token}
     end
   end
 
@@ -514,12 +581,21 @@ defmodule Pleroma.Web.OAuth.OAuthController do
   defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
     do: put_session(conn, :registration_id, registration_id)
 
-  @spec validate_scopes(App.t(), map()) ::
+  defp build_and_response_mfa_token(user, auth) do
+    with {:ok, token} <- MFA.Token.create(user, auth) do
+      MFAView.render("mfa_response.json", %{token: token, user: user})
+    end
+  end
+
+  @spec validate_scopes(App.t(), map() | list()) ::
           {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
-  defp validate_scopes(%App{} = app, params) do
-    params
-    |> Scopes.fetch_scopes(app.scopes)
-    |> Scopes.validate(app.scopes)
+  defp validate_scopes(%App{} = app, params) when is_map(params) do
+    requested_scopes = Scopes.fetch_scopes(params, app.scopes)
+    validate_scopes(app, requested_scopes)
+  end
+
+  defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
+    Scopes.validate(requested_scopes, app.scopes)
   end
 
   def default_redirect_uri(%App{} = app) do