logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma

mfa_controller_test.exs (9038B)


  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. defmodule Pleroma.Web.OAuth.MFAControllerTest do
  5. use Pleroma.Web.ConnCase
  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: [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. "expires_in" => 600,
  152. "me" => ^ap_id,
  153. "refresh_token" => _,
  154. "scope" => "write",
  155. "token_type" => "Bearer"
  156. },
  157. response
  158. )
  159. end
  160. test "returns errors when mfa token invalid", %{conn: conn, user: user, app: app} do
  161. otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
  162. response =
  163. conn
  164. |> post("/oauth/mfa/challenge", %{
  165. "mfa_token" => "XXX",
  166. "challenge_type" => "totp",
  167. "code" => otp_token,
  168. "client_id" => app.client_id,
  169. "client_secret" => app.client_secret
  170. })
  171. |> json_response(400)
  172. assert response == %{"error" => "Invalid code"}
  173. end
  174. test "returns error when otp code is invalid", %{conn: conn, user: user, app: app} do
  175. mfa_token = insert(:mfa_token, user: user)
  176. response =
  177. conn
  178. |> post("/oauth/mfa/challenge", %{
  179. "mfa_token" => mfa_token.token,
  180. "challenge_type" => "totp",
  181. "code" => "XXX",
  182. "client_id" => app.client_id,
  183. "client_secret" => app.client_secret
  184. })
  185. |> json_response(400)
  186. assert response == %{"error" => "Invalid code"}
  187. end
  188. test "returns error when client credentails is wrong ", %{conn: conn, user: user} do
  189. otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
  190. mfa_token = insert(:mfa_token, user: user)
  191. response =
  192. conn
  193. |> post("/oauth/mfa/challenge", %{
  194. "mfa_token" => mfa_token.token,
  195. "challenge_type" => "totp",
  196. "code" => otp_token,
  197. "client_id" => "xxx",
  198. "client_secret" => "xxx"
  199. })
  200. |> json_response(400)
  201. assert response == %{"error" => "Invalid code"}
  202. end
  203. end
  204. describe "challenge/recovery" do
  205. setup %{conn: conn} do
  206. app = insert(:oauth_app)
  207. {:ok, conn: conn, app: app}
  208. end
  209. test "returns access token with valid code", %{conn: conn, app: app} do
  210. otp_secret = TOTP.generate_secret()
  211. [code | _] = backup_codes = BackupCodes.generate()
  212. hashed_codes =
  213. backup_codes
  214. |> Enum.map(&Pbkdf2.hash_pwd_salt(&1))
  215. user =
  216. insert(:user,
  217. multi_factor_authentication_settings: %MFA.Settings{
  218. enabled: true,
  219. backup_codes: hashed_codes,
  220. totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
  221. }
  222. )
  223. mfa_token =
  224. insert(:mfa_token,
  225. user: user,
  226. authorization: build(:oauth_authorization, app: app, scopes: ["write"])
  227. )
  228. response =
  229. conn
  230. |> post("/oauth/mfa/challenge", %{
  231. "mfa_token" => mfa_token.token,
  232. "challenge_type" => "recovery",
  233. "code" => code,
  234. "client_id" => app.client_id,
  235. "client_secret" => app.client_secret
  236. })
  237. |> json_response(:ok)
  238. ap_id = user.ap_id
  239. assert match?(
  240. %{
  241. "access_token" => _,
  242. "expires_in" => 600,
  243. "me" => ^ap_id,
  244. "refresh_token" => _,
  245. "scope" => "write",
  246. "token_type" => "Bearer"
  247. },
  248. response
  249. )
  250. error_response =
  251. conn
  252. |> post("/oauth/mfa/challenge", %{
  253. "mfa_token" => mfa_token.token,
  254. "challenge_type" => "recovery",
  255. "code" => code,
  256. "client_id" => app.client_id,
  257. "client_secret" => app.client_secret
  258. })
  259. |> json_response(400)
  260. assert error_response == %{"error" => "Invalid code"}
  261. end
  262. end
  263. end