Refactor User.post_register_action/1 emails
[akkoma] / test / pleroma / web / o_auth / mfa_controller_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.OAuth.MFAControllerTest do
6 use Pleroma.Web.ConnCase
7 import Pleroma.Factory
8
9 alias Pleroma.MFA
10 alias Pleroma.MFA.BackupCodes
11 alias Pleroma.MFA.TOTP
12 alias Pleroma.Repo
13 alias Pleroma.Web.OAuth.Authorization
14 alias Pleroma.Web.OAuth.OAuthController
15
16 setup %{conn: conn} do
17 otp_secret = TOTP.generate_secret()
18
19 user =
20 insert(:user,
21 multi_factor_authentication_settings: %MFA.Settings{
22 enabled: true,
23 backup_codes: [Pbkdf2.hash_pwd_salt("test-code")],
24 totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
25 }
26 )
27
28 app = insert(:oauth_app)
29 {:ok, conn: conn, user: user, app: app}
30 end
31
32 describe "show" do
33 setup %{conn: conn, user: user, app: app} do
34 mfa_token =
35 insert(:mfa_token,
36 user: user,
37 authorization: build(:oauth_authorization, app: app, scopes: ["write"])
38 )
39
40 {:ok, conn: conn, mfa_token: mfa_token}
41 end
42
43 test "GET /oauth/mfa renders mfa forms", %{conn: conn, mfa_token: mfa_token} do
44 conn =
45 get(
46 conn,
47 "/oauth/mfa",
48 %{
49 "mfa_token" => mfa_token.token,
50 "state" => "a_state",
51 "redirect_uri" => "http://localhost:8080/callback"
52 }
53 )
54
55 assert response = html_response(conn, 200)
56 assert response =~ "Two-factor authentication"
57 assert response =~ mfa_token.token
58 assert response =~ "http://localhost:8080/callback"
59 end
60
61 test "GET /oauth/mfa renders mfa recovery forms", %{conn: conn, mfa_token: mfa_token} do
62 conn =
63 get(
64 conn,
65 "/oauth/mfa",
66 %{
67 "mfa_token" => mfa_token.token,
68 "state" => "a_state",
69 "redirect_uri" => "http://localhost:8080/callback",
70 "challenge_type" => "recovery"
71 }
72 )
73
74 assert response = html_response(conn, 200)
75 assert response =~ "Two-factor recovery"
76 assert response =~ mfa_token.token
77 assert response =~ "http://localhost:8080/callback"
78 end
79 end
80
81 describe "verify" do
82 setup %{conn: conn, user: user, app: app} do
83 mfa_token =
84 insert(:mfa_token,
85 user: user,
86 authorization: build(:oauth_authorization, app: app, scopes: ["write"])
87 )
88
89 {:ok, conn: conn, user: user, mfa_token: mfa_token, app: app}
90 end
91
92 test "POST /oauth/mfa/verify, verify totp code", %{
93 conn: conn,
94 user: user,
95 mfa_token: mfa_token,
96 app: app
97 } do
98 otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
99
100 conn =
101 conn
102 |> post("/oauth/mfa/verify", %{
103 "mfa" => %{
104 "mfa_token" => mfa_token.token,
105 "challenge_type" => "totp",
106 "code" => otp_token,
107 "state" => "a_state",
108 "redirect_uri" => OAuthController.default_redirect_uri(app)
109 }
110 })
111
112 target = redirected_to(conn)
113 target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string()
114 query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
115 assert %{"state" => "a_state", "code" => code} = query
116 assert target_url == OAuthController.default_redirect_uri(app)
117 auth = Repo.get_by(Authorization, token: code)
118 assert auth.scopes == ["write"]
119 end
120
121 test "POST /oauth/mfa/verify, verify recovery code", %{
122 conn: conn,
123 mfa_token: mfa_token,
124 app: app
125 } do
126 conn =
127 conn
128 |> post("/oauth/mfa/verify", %{
129 "mfa" => %{
130 "mfa_token" => mfa_token.token,
131 "challenge_type" => "recovery",
132 "code" => "test-code",
133 "state" => "a_state",
134 "redirect_uri" => OAuthController.default_redirect_uri(app)
135 }
136 })
137
138 target = redirected_to(conn)
139 target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string()
140 query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
141 assert %{"state" => "a_state", "code" => code} = query
142 assert target_url == OAuthController.default_redirect_uri(app)
143 auth = Repo.get_by(Authorization, token: code)
144 assert auth.scopes == ["write"]
145 end
146 end
147
148 describe "challenge/totp" do
149 test "returns access token with valid code", %{conn: conn, user: user, app: app} do
150 otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
151
152 mfa_token =
153 insert(:mfa_token,
154 user: user,
155 authorization: build(:oauth_authorization, app: app, scopes: ["write"])
156 )
157
158 response =
159 conn
160 |> post("/oauth/mfa/challenge", %{
161 "mfa_token" => mfa_token.token,
162 "challenge_type" => "totp",
163 "code" => otp_token,
164 "client_id" => app.client_id,
165 "client_secret" => app.client_secret
166 })
167 |> json_response(:ok)
168
169 ap_id = user.ap_id
170
171 assert match?(
172 %{
173 "access_token" => _,
174 "expires_in" => 600,
175 "me" => ^ap_id,
176 "refresh_token" => _,
177 "scope" => "write",
178 "token_type" => "Bearer"
179 },
180 response
181 )
182 end
183
184 test "returns errors when mfa token invalid", %{conn: conn, user: user, app: app} do
185 otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
186
187 response =
188 conn
189 |> post("/oauth/mfa/challenge", %{
190 "mfa_token" => "XXX",
191 "challenge_type" => "totp",
192 "code" => otp_token,
193 "client_id" => app.client_id,
194 "client_secret" => app.client_secret
195 })
196 |> json_response(400)
197
198 assert response == %{"error" => "Invalid code"}
199 end
200
201 test "returns error when otp code is invalid", %{conn: conn, user: user, app: app} do
202 mfa_token = insert(:mfa_token, user: user)
203
204 response =
205 conn
206 |> post("/oauth/mfa/challenge", %{
207 "mfa_token" => mfa_token.token,
208 "challenge_type" => "totp",
209 "code" => "XXX",
210 "client_id" => app.client_id,
211 "client_secret" => app.client_secret
212 })
213 |> json_response(400)
214
215 assert response == %{"error" => "Invalid code"}
216 end
217
218 test "returns error when client credentails is wrong ", %{conn: conn, user: user} do
219 otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
220 mfa_token = insert(:mfa_token, user: user)
221
222 response =
223 conn
224 |> post("/oauth/mfa/challenge", %{
225 "mfa_token" => mfa_token.token,
226 "challenge_type" => "totp",
227 "code" => otp_token,
228 "client_id" => "xxx",
229 "client_secret" => "xxx"
230 })
231 |> json_response(400)
232
233 assert response == %{"error" => "Invalid code"}
234 end
235 end
236
237 describe "challenge/recovery" do
238 setup %{conn: conn} do
239 app = insert(:oauth_app)
240 {:ok, conn: conn, app: app}
241 end
242
243 test "returns access token with valid code", %{conn: conn, app: app} do
244 otp_secret = TOTP.generate_secret()
245
246 [code | _] = backup_codes = BackupCodes.generate()
247
248 hashed_codes =
249 backup_codes
250 |> Enum.map(&Pbkdf2.hash_pwd_salt(&1))
251
252 user =
253 insert(:user,
254 multi_factor_authentication_settings: %MFA.Settings{
255 enabled: true,
256 backup_codes: hashed_codes,
257 totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
258 }
259 )
260
261 mfa_token =
262 insert(:mfa_token,
263 user: user,
264 authorization: build(:oauth_authorization, app: app, scopes: ["write"])
265 )
266
267 response =
268 conn
269 |> post("/oauth/mfa/challenge", %{
270 "mfa_token" => mfa_token.token,
271 "challenge_type" => "recovery",
272 "code" => code,
273 "client_id" => app.client_id,
274 "client_secret" => app.client_secret
275 })
276 |> json_response(:ok)
277
278 ap_id = user.ap_id
279
280 assert match?(
281 %{
282 "access_token" => _,
283 "expires_in" => 600,
284 "me" => ^ap_id,
285 "refresh_token" => _,
286 "scope" => "write",
287 "token_type" => "Bearer"
288 },
289 response
290 )
291
292 error_response =
293 conn
294 |> post("/oauth/mfa/challenge", %{
295 "mfa_token" => mfa_token.token,
296 "challenge_type" => "recovery",
297 "code" => code,
298 "client_id" => app.client_id,
299 "client_secret" => app.client_secret
300 })
301 |> json_response(400)
302
303 assert error_response == %{"error" => "Invalid code"}
304 end
305 end
306 end