summaryrefslogtreecommitdiff
path: root/lib/pleroma/web/oauth/mfa_controller.ex
blob: f102c93e7e5bbfcd3be86f634cee3265c3f761e1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.OAuth.MFAController do
  @moduledoc """
  The model represents api to use Multi Factor authentications.
  """

  use Pleroma.Web, :controller

  alias Pleroma.MFA
  alias Pleroma.Web.Auth.TOTPAuthenticator
  alias Pleroma.Web.OAuth.MFAView, as: View
  alias Pleroma.Web.OAuth.OAuthController
  alias Pleroma.Web.OAuth.OAuthView
  alias Pleroma.Web.OAuth.Token

  plug(:fetch_session when action in [:show, :verify])
  plug(:fetch_flash when action in [:show, :verify])

  @doc """
  Display form to input mfa code or recovery code.
  """
  def show(conn, %{"mfa_token" => mfa_token} = params) do
    template = Map.get(params, "challenge_type", "totp")

    conn
    |> put_view(View)
    |> render("#{template}.html", %{
      mfa_token: mfa_token,
      redirect_uri: params["redirect_uri"],
      state: params["state"]
    })
  end

  @doc """
  Verification code and continue authorization.
  """
  def verify(conn, %{"mfa" => %{"mfa_token" => mfa_token} = mfa_params} = _) do
    with {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
         {:ok, _} <- validates_challenge(user, mfa_params) do
      conn
      |> OAuthController.after_create_authorization(auth, %{
        "authorization" => %{
          "redirect_uri" => mfa_params["redirect_uri"],
          "state" => mfa_params["state"]
        }
      })
    else
      _ ->
        conn
        |> put_flash(:error, "Two-factor authentication failed.")
        |> put_status(:unauthorized)
        |> show(mfa_params)
    end
  end

  @doc """
  Verification second step of MFA (or recovery) and returns access token.

  ## Endpoint
  POST /oauth/mfa/challenge

  params:
  `client_id`
  `client_secret`
  `mfa_token` - access token to check second step of mfa
  `challenge_type` - 'totp' or 'recovery'
  `code`

  """
  def challenge(conn, %{"mfa_token" => mfa_token} = params) do
    with {:ok, app} <- Token.Utils.fetch_app(conn),
         {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
         {:ok, _} <- validates_challenge(user, params),
         {:ok, token} <- Token.exchange_token(app, auth) do
      json(conn, OAuthView.render("token.json", %{user: user, token: token}))
    else
      _error ->
        conn
        |> put_status(400)
        |> json(%{error: "Invalid code"})
    end
  end

  # Verify TOTP Code
  defp validates_challenge(user, %{"challenge_type" => "totp", "code" => code} = _) do
    TOTPAuthenticator.verify(code, user)
  end

  # Verify Recovery Code
  defp validates_challenge(user, %{"challenge_type" => "recovery", "code" => code} = _) do
    TOTPAuthenticator.verify_recovery_code(user, code)
  end

  defp validates_challenge(_, _), do: {:error, :unsupported_challenge_type}
end