Merge remote-tracking branch 'remotes/origin/develop' into auth-improvements
[akkoma] / lib / pleroma / web / o_auth / mfa_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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