1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.OAuth.MFAControllerTest do
6 use Pleroma.Web.ConnCase, async: true
10 alias Pleroma.MFA.BackupCodes
11 alias Pleroma.MFA.TOTP
13 alias Pleroma.Web.OAuth.Authorization
14 alias Pleroma.Web.OAuth.OAuthController
16 setup %{conn: conn} do
17 otp_secret = TOTP.generate_secret()
21 multi_factor_authentication_settings: %MFA.Settings{
23 backup_codes: [Pleroma.Password.Pbkdf2.hash_pwd_salt("test-code")],
24 totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
28 app = insert(:oauth_app)
29 {:ok, conn: conn, user: user, app: app}
33 setup %{conn: conn, user: user, app: app} do
37 authorization: build(:oauth_authorization, app: app, scopes: ["write"])
40 {:ok, conn: conn, mfa_token: mfa_token}
43 test "GET /oauth/mfa renders mfa forms", %{conn: conn, mfa_token: mfa_token} do
49 "mfa_token" => mfa_token.token,
51 "redirect_uri" => "http://localhost:8080/callback"
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"
61 test "GET /oauth/mfa renders mfa recovery forms", %{conn: conn, mfa_token: mfa_token} do
67 "mfa_token" => mfa_token.token,
69 "redirect_uri" => "http://localhost:8080/callback",
70 "challenge_type" => "recovery"
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"
82 setup %{conn: conn, user: user, app: app} do
86 authorization: build(:oauth_authorization, app: app, scopes: ["write"])
89 {:ok, conn: conn, user: user, mfa_token: mfa_token, app: app}
92 test "POST /oauth/mfa/verify, verify totp code", %{
98 otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
102 |> post("/oauth/mfa/verify", %{
104 "mfa_token" => mfa_token.token,
105 "challenge_type" => "totp",
107 "state" => "a_state",
108 "redirect_uri" => OAuthController.default_redirect_uri(app)
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"]
121 test "POST /oauth/mfa/verify, verify recovery code", %{
123 mfa_token: mfa_token,
128 |> post("/oauth/mfa/verify", %{
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)
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"]
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)
155 authorization: build(:oauth_authorization, app: app, scopes: ["write"])
160 |> post("/oauth/mfa/challenge", %{
161 "mfa_token" => mfa_token.token,
162 "challenge_type" => "totp",
164 "client_id" => app.client_id,
165 "client_secret" => app.client_secret
167 |> json_response(:ok)
175 "refresh_token" => _,
177 "token_type" => "Bearer"
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)
188 |> post("/oauth/mfa/challenge", %{
189 "mfa_token" => "XXX",
190 "challenge_type" => "totp",
192 "client_id" => app.client_id,
193 "client_secret" => app.client_secret
195 |> json_response(400)
197 assert response == %{"error" => "Invalid code"}
200 test "returns error when otp code is invalid", %{conn: conn, user: user, app: app} do
201 mfa_token = insert(:mfa_token, user: user)
205 |> post("/oauth/mfa/challenge", %{
206 "mfa_token" => mfa_token.token,
207 "challenge_type" => "totp",
209 "client_id" => app.client_id,
210 "client_secret" => app.client_secret
212 |> json_response(400)
214 assert response == %{"error" => "Invalid code"}
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)
223 |> post("/oauth/mfa/challenge", %{
224 "mfa_token" => mfa_token.token,
225 "challenge_type" => "totp",
227 "client_id" => "xxx",
228 "client_secret" => "xxx"
230 |> json_response(400)
232 assert response == %{"error" => "Invalid code"}
236 describe "challenge/recovery" do
237 setup %{conn: conn} do
238 app = insert(:oauth_app)
239 {:ok, conn: conn, app: app}
242 test "returns access token with valid code", %{conn: conn, app: app} do
243 otp_secret = TOTP.generate_secret()
245 [code | _] = backup_codes = BackupCodes.generate()
249 |> Enum.map(&Pleroma.Password.Pbkdf2.hash_pwd_salt(&1))
253 multi_factor_authentication_settings: %MFA.Settings{
255 backup_codes: hashed_codes,
256 totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
263 authorization: build(:oauth_authorization, app: app, scopes: ["write"])
268 |> post("/oauth/mfa/challenge", %{
269 "mfa_token" => mfa_token.token,
270 "challenge_type" => "recovery",
272 "client_id" => app.client_id,
273 "client_secret" => app.client_secret
275 |> json_response(:ok)
283 "refresh_token" => _,
285 "token_type" => "Bearer"
292 |> post("/oauth/mfa/challenge", %{
293 "mfa_token" => mfa_token.token,
294 "challenge_type" => "recovery",
296 "client_id" => app.client_id,
297 "client_secret" => app.client_secret
299 |> json_response(400)
301 assert error_response == %{"error" => "Invalid code"}