Merge remote-tracking branch 'origin/develop' into manifest
[akkoma] / lib / pleroma / mfa.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.MFA do
6 @moduledoc """
7 The MFA context.
8 """
9
10 alias Pleroma.User
11
12 alias Pleroma.MFA.BackupCodes
13 alias Pleroma.MFA.Changeset
14 alias Pleroma.MFA.Settings
15 alias Pleroma.MFA.TOTP
16
17 @doc """
18 Returns MFA methods the user has enabled.
19
20 ## Examples
21
22 iex> Pleroma.MFA.supported_method(User)
23 "totp, u2f"
24 """
25 @spec supported_methods(User.t()) :: String.t()
26 def supported_methods(user) do
27 settings = fetch_settings(user)
28
29 Settings.mfa_methods()
30 |> Enum.reduce([], fn m, acc ->
31 if method_enabled?(m, settings) do
32 acc ++ [m]
33 else
34 acc
35 end
36 end)
37 |> Enum.join(",")
38 end
39
40 @doc "Checks that user enabled MFA"
41 def require?(user) do
42 fetch_settings(user).enabled
43 end
44
45 @doc """
46 Display MFA settings of user
47 """
48 def mfa_settings(user) do
49 settings = fetch_settings(user)
50
51 Settings.mfa_methods()
52 |> Enum.map(fn m -> [m, method_enabled?(m, settings)] end)
53 |> Enum.into(%{enabled: settings.enabled}, fn [a, b] -> {a, b} end)
54 end
55
56 @doc false
57 def fetch_settings(%User{} = user) do
58 user.multi_factor_authentication_settings || %Settings{}
59 end
60
61 @doc "clears backup codes"
62 def invalidate_backup_code(%User{} = user, hash_code) do
63 %{backup_codes: codes} = fetch_settings(user)
64
65 user
66 |> Changeset.cast_backup_codes(codes -- [hash_code])
67 |> User.update_and_set_cache()
68 end
69
70 @doc "generates backup codes"
71 @spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
72 def generate_backup_codes(%User{} = user) do
73 with codes <- BackupCodes.generate(),
74 hashed_codes <- Enum.map(codes, &Pleroma.Password.Pbkdf2.hash_pwd_salt/1),
75 changeset <- Changeset.cast_backup_codes(user, hashed_codes),
76 {:ok, _} <- User.update_and_set_cache(changeset) do
77 {:ok, codes}
78 else
79 {:error, msg} ->
80 %{error: msg}
81 end
82 end
83
84 @doc """
85 Generates secret key and set delivery_type to 'app' for TOTP method.
86 """
87 @spec setup_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
88 def setup_totp(user) do
89 user
90 |> Changeset.setup_totp(%{secret: TOTP.generate_secret(), delivery_type: "app"})
91 |> User.update_and_set_cache()
92 end
93
94 @doc """
95 Confirms the TOTP method for user.
96
97 `attrs`:
98 `password` - current user password
99 `code` - TOTP token
100 """
101 @spec confirm_totp(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t() | atom()}
102 def confirm_totp(%User{} = user, attrs) do
103 with settings <- user.multi_factor_authentication_settings.totp,
104 {:ok, :pass} <- TOTP.validate_token(settings.secret, attrs["code"]) do
105 user
106 |> Changeset.confirm_totp()
107 |> User.update_and_set_cache()
108 end
109 end
110
111 @doc """
112 Disables the TOTP method for user.
113
114 `attrs`:
115 `password` - current user password
116 """
117 @spec disable_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
118 def disable_totp(%User{} = user) do
119 user
120 |> Changeset.disable_totp()
121 |> Changeset.disable()
122 |> User.update_and_set_cache()
123 end
124
125 @doc """
126 Force disables all MFA methods for user.
127 """
128 @spec disable(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
129 def disable(%User{} = user) do
130 user
131 |> Changeset.disable_totp()
132 |> Changeset.disable(true)
133 |> User.update_and_set_cache()
134 end
135
136 @doc """
137 Checks if the user has MFA method enabled.
138 """
139 def method_enabled?(method, settings) do
140 with {:ok, %{confirmed: true} = _} <- Map.fetch(settings, method) do
141 true
142 else
143 _ -> false
144 end
145 end
146
147 @doc """
148 Checks if the user has enabled at least one MFA method.
149 """
150 def enabled?(settings) do
151 Settings.mfa_methods()
152 |> Enum.map(fn m -> method_enabled?(m, settings) end)
153 |> Enum.any?()
154 end
155 end