First
[anni] / lib / pleroma / web / o_auth / mfa_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.OAuth.MFAController do
6   @moduledoc """
7   The model represents api to use Multi Factor authentications.
8   """
9
10   use Pleroma.Web, :controller
11
12   alias Pleroma.MFA
13   alias Pleroma.Web.Auth.TOTPAuthenticator
14   alias Pleroma.Web.OAuth.MFAView, as: View
15   alias Pleroma.Web.OAuth.OAuthController
16   alias Pleroma.Web.OAuth.Token
17
18   plug(:fetch_session when action in [:show, :verify])
19   plug(:fetch_flash when action in [:show, :verify])
20
21   @doc """
22   Display form to input mfa code or recovery code.
23   """
24   def show(conn, %{"mfa_token" => mfa_token} = params) do
25     template = Map.get(params, "challenge_type", "totp")
26
27     conn
28     |> put_view(View)
29     |> render("#{template}.html", %{
30       mfa_token: mfa_token,
31       redirect_uri: params["redirect_uri"],
32       state: params["state"]
33     })
34   end
35
36   @doc """
37   Verification code and continue authorization.
38   """
39   def verify(conn, %{"mfa" => %{"mfa_token" => mfa_token} = mfa_params} = _) do
40     with {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
41          {:ok, _} <- validates_challenge(user, mfa_params) do
42       conn
43       |> OAuthController.after_create_authorization(auth, %{
44         "authorization" => %{
45           "redirect_uri" => mfa_params["redirect_uri"],
46           "state" => mfa_params["state"]
47         }
48       })
49     else
50       _ ->
51         conn
52         |> put_flash(:error, "Two-factor authentication failed.")
53         |> put_status(:unauthorized)
54         |> show(mfa_params)
55     end
56   end
57
58   @doc """
59   Verification second step of MFA (or recovery) and returns access token.
60
61   ## Endpoint
62   POST /oauth/mfa/challenge
63
64   params:
65   `client_id`
66   `client_secret`
67   `mfa_token` - access token to check second step of mfa
68   `challenge_type` - 'totp' or 'recovery'
69   `code`
70
71   """
72   def challenge(conn, %{"mfa_token" => mfa_token} = params) do
73     with {:ok, app} <- Token.Utils.fetch_app(conn),
74          {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
75          {:ok, _} <- validates_challenge(user, params),
76          {:ok, token} <- Token.exchange_token(app, auth) do
77       OAuthController.after_token_exchange(conn, %{user: user, token: token})
78     else
79       _error ->
80         conn
81         |> put_status(400)
82         |> json(%{error: "Invalid code"})
83     end
84   end
85
86   # Verify TOTP Code
87   defp validates_challenge(user, %{"challenge_type" => "totp", "code" => code} = _) do
88     TOTPAuthenticator.verify(code, user)
89   end
90
91   # Verify Recovery Code
92   defp validates_challenge(user, %{"challenge_type" => "recovery", "code" => code} = _) do
93     TOTPAuthenticator.verify_recovery_code(user, code)
94   end
95
96   defp validates_challenge(_, _), do: {:error, :unsupported_challenge_type}
97 end