move to 2.5.5
[anni] / test / pleroma / web / o_auth / mfa_controller_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 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, async: true
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: [Pleroma.Password.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                  "me" => ^ap_id,
175                  "refresh_token" => _,
176                  "scope" => "write",
177                  "token_type" => "Bearer"
178                },
179                response
180              )
181     end
182
183     test "returns errors when mfa token invalid", %{conn: conn, user: user, app: app} do
184       otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
185
186       response =
187         conn
188         |> post("/oauth/mfa/challenge", %{
189           "mfa_token" => "XXX",
190           "challenge_type" => "totp",
191           "code" => otp_token,
192           "client_id" => app.client_id,
193           "client_secret" => app.client_secret
194         })
195         |> json_response(400)
196
197       assert response == %{"error" => "Invalid code"}
198     end
199
200     test "returns error when otp code is invalid", %{conn: conn, user: user, app: app} do
201       mfa_token = insert(:mfa_token, user: user)
202
203       response =
204         conn
205         |> post("/oauth/mfa/challenge", %{
206           "mfa_token" => mfa_token.token,
207           "challenge_type" => "totp",
208           "code" => "XXX",
209           "client_id" => app.client_id,
210           "client_secret" => app.client_secret
211         })
212         |> json_response(400)
213
214       assert response == %{"error" => "Invalid code"}
215     end
216
217     test "returns error when client credentails is wrong ", %{conn: conn, user: user} do
218       otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
219       mfa_token = insert(:mfa_token, user: user)
220
221       response =
222         conn
223         |> post("/oauth/mfa/challenge", %{
224           "mfa_token" => mfa_token.token,
225           "challenge_type" => "totp",
226           "code" => otp_token,
227           "client_id" => "xxx",
228           "client_secret" => "xxx"
229         })
230         |> json_response(400)
231
232       assert response == %{"error" => "Invalid code"}
233     end
234   end
235
236   describe "challenge/recovery" do
237     setup %{conn: conn} do
238       app = insert(:oauth_app)
239       {:ok, conn: conn, app: app}
240     end
241
242     test "returns access token with valid code", %{conn: conn, app: app} do
243       otp_secret = TOTP.generate_secret()
244
245       [code | _] = backup_codes = BackupCodes.generate()
246
247       hashed_codes =
248         backup_codes
249         |> Enum.map(&Pleroma.Password.Pbkdf2.hash_pwd_salt(&1))
250
251       user =
252         insert(:user,
253           multi_factor_authentication_settings: %MFA.Settings{
254             enabled: true,
255             backup_codes: hashed_codes,
256             totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
257           }
258         )
259
260       mfa_token =
261         insert(:mfa_token,
262           user: user,
263           authorization: build(:oauth_authorization, app: app, scopes: ["write"])
264         )
265
266       response =
267         conn
268         |> post("/oauth/mfa/challenge", %{
269           "mfa_token" => mfa_token.token,
270           "challenge_type" => "recovery",
271           "code" => code,
272           "client_id" => app.client_id,
273           "client_secret" => app.client_secret
274         })
275         |> json_response(:ok)
276
277       ap_id = user.ap_id
278
279       assert match?(
280                %{
281                  "access_token" => _,
282                  "me" => ^ap_id,
283                  "refresh_token" => _,
284                  "scope" => "write",
285                  "token_type" => "Bearer"
286                },
287                response
288              )
289
290       error_response =
291         conn
292         |> post("/oauth/mfa/challenge", %{
293           "mfa_token" => mfa_token.token,
294           "challenge_type" => "recovery",
295           "code" => code,
296           "client_id" => app.client_id,
297           "client_secret" => app.client_secret
298         })
299         |> json_response(400)
300
301       assert error_response == %{"error" => "Invalid code"}
302     end
303   end
304 end