logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

mfa_controller_test.exs (9009B)


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