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