logo

pleroma

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

oauth_controller_test.exs (37689B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Web.OAuth.OAuthControllerTest do
  5. use Pleroma.Web.ConnCase
  6. import Pleroma.Factory
  7. alias Pleroma.MFA
  8. alias Pleroma.MFA.TOTP
  9. alias Pleroma.Repo
  10. alias Pleroma.User
  11. alias Pleroma.Web.OAuth.Authorization
  12. alias Pleroma.Web.OAuth.OAuthController
  13. alias Pleroma.Web.OAuth.Token
  14. @session_opts [
  15. store: :cookie,
  16. key: "_test",
  17. signing_salt: "cooldude"
  18. ]
  19. setup do
  20. clear_config([:instance, :account_activation_required])
  21. clear_config([:instance, :account_approval_required])
  22. end
  23. describe "in OAuth consumer mode, " do
  24. setup do
  25. [
  26. app: insert(:oauth_app),
  27. conn:
  28. build_conn()
  29. |> Plug.Session.call(Plug.Session.init(@session_opts))
  30. |> fetch_session()
  31. ]
  32. end
  33. setup do: clear_config([:auth, :oauth_consumer_strategies], ~w(twitter facebook))
  34. test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{
  35. app: app,
  36. conn: conn
  37. } do
  38. conn =
  39. get(
  40. conn,
  41. "/oauth/authorize",
  42. %{
  43. "response_type" => "code",
  44. "client_id" => app.client_id,
  45. "redirect_uri" => OAuthController.default_redirect_uri(app),
  46. "scope" => "read"
  47. }
  48. )
  49. assert response = html_response(conn, 200)
  50. assert response =~ "Sign in with Twitter"
  51. assert response =~ o_auth_path(conn, :prepare_request)
  52. end
  53. test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{
  54. app: app,
  55. conn: conn
  56. } do
  57. conn =
  58. get(
  59. conn,
  60. "/oauth/prepare_request",
  61. %{
  62. "provider" => "twitter",
  63. "authorization" => %{
  64. "scope" => "read follow",
  65. "client_id" => app.client_id,
  66. "redirect_uri" => OAuthController.default_redirect_uri(app),
  67. "state" => "a_state"
  68. }
  69. }
  70. )
  71. assert response = html_response(conn, 302)
  72. redirect_query = URI.parse(redirected_to(conn)).query
  73. assert %{"state" => state_param} = URI.decode_query(redirect_query)
  74. assert {:ok, state_components} = Poison.decode(state_param)
  75. expected_client_id = app.client_id
  76. expected_redirect_uri = app.redirect_uris
  77. assert %{
  78. "scope" => "read follow",
  79. "client_id" => ^expected_client_id,
  80. "redirect_uri" => ^expected_redirect_uri,
  81. "state" => "a_state"
  82. } = state_components
  83. end
  84. test "with user-bound registration, GET /oauth/<provider>/callback redirects to `redirect_uri` with `code`",
  85. %{app: app, conn: conn} do
  86. registration = insert(:registration)
  87. redirect_uri = OAuthController.default_redirect_uri(app)
  88. state_params = %{
  89. "scope" => Enum.join(app.scopes, " "),
  90. "client_id" => app.client_id,
  91. "redirect_uri" => redirect_uri,
  92. "state" => ""
  93. }
  94. conn =
  95. conn
  96. |> assign(:ueberauth_auth, %{provider: registration.provider, uid: registration.uid})
  97. |> get(
  98. "/oauth/twitter/callback",
  99. %{
  100. "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
  101. "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
  102. "provider" => "twitter",
  103. "state" => Poison.encode!(state_params)
  104. }
  105. )
  106. assert response = html_response(conn, 302)
  107. assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
  108. end
  109. test "with user-unbound registration, GET /oauth/<provider>/callback renders registration_details page",
  110. %{app: app, conn: conn} do
  111. user = insert(:user)
  112. state_params = %{
  113. "scope" => "read write",
  114. "client_id" => app.client_id,
  115. "redirect_uri" => OAuthController.default_redirect_uri(app),
  116. "state" => "a_state"
  117. }
  118. conn =
  119. conn
  120. |> assign(:ueberauth_auth, %{
  121. provider: "twitter",
  122. uid: "171799000",
  123. info: %{nickname: user.nickname, email: user.email, name: user.name, description: nil}
  124. })
  125. |> get(
  126. "/oauth/twitter/callback",
  127. %{
  128. "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
  129. "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
  130. "provider" => "twitter",
  131. "state" => Poison.encode!(state_params)
  132. }
  133. )
  134. assert response = html_response(conn, 200)
  135. assert response =~ ~r/name="op" type="submit" value="register"/
  136. assert response =~ ~r/name="op" type="submit" value="connect"/
  137. assert response =~ user.email
  138. assert response =~ user.nickname
  139. end
  140. test "on authentication error, GET /oauth/<provider>/callback redirects to `redirect_uri`", %{
  141. app: app,
  142. conn: conn
  143. } do
  144. state_params = %{
  145. "scope" => Enum.join(app.scopes, " "),
  146. "client_id" => app.client_id,
  147. "redirect_uri" => OAuthController.default_redirect_uri(app),
  148. "state" => ""
  149. }
  150. conn =
  151. conn
  152. |> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]})
  153. |> get(
  154. "/oauth/twitter/callback",
  155. %{
  156. "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
  157. "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
  158. "provider" => "twitter",
  159. "state" => Poison.encode!(state_params)
  160. }
  161. )
  162. assert response = html_response(conn, 302)
  163. assert redirected_to(conn) == app.redirect_uris
  164. assert get_flash(conn, :error) == "Failed to authenticate: (error description)."
  165. end
  166. test "GET /oauth/registration_details renders registration details form", %{
  167. app: app,
  168. conn: conn
  169. } do
  170. conn =
  171. get(
  172. conn,
  173. "/oauth/registration_details",
  174. %{
  175. "authorization" => %{
  176. "scopes" => app.scopes,
  177. "client_id" => app.client_id,
  178. "redirect_uri" => OAuthController.default_redirect_uri(app),
  179. "state" => "a_state",
  180. "nickname" => nil,
  181. "email" => "john@doe.com"
  182. }
  183. }
  184. )
  185. assert response = html_response(conn, 200)
  186. assert response =~ ~r/name="op" type="submit" value="register"/
  187. assert response =~ ~r/name="op" type="submit" value="connect"/
  188. end
  189. test "with valid params, POST /oauth/register?op=register redirects to `redirect_uri` with `code`",
  190. %{
  191. app: app,
  192. conn: conn
  193. } do
  194. registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
  195. redirect_uri = OAuthController.default_redirect_uri(app)
  196. conn =
  197. conn
  198. |> put_session(:registration_id, registration.id)
  199. |> post(
  200. "/oauth/register",
  201. %{
  202. "op" => "register",
  203. "authorization" => %{
  204. "scopes" => app.scopes,
  205. "client_id" => app.client_id,
  206. "redirect_uri" => redirect_uri,
  207. "state" => "a_state",
  208. "nickname" => "availablenick",
  209. "email" => "available@email.com"
  210. }
  211. }
  212. )
  213. assert response = html_response(conn, 302)
  214. assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
  215. end
  216. test "with unlisted `redirect_uri`, POST /oauth/register?op=register results in HTTP 401",
  217. %{
  218. app: app,
  219. conn: conn
  220. } do
  221. registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
  222. unlisted_redirect_uri = "http://cross-site-request.com"
  223. conn =
  224. conn
  225. |> put_session(:registration_id, registration.id)
  226. |> post(
  227. "/oauth/register",
  228. %{
  229. "op" => "register",
  230. "authorization" => %{
  231. "scopes" => app.scopes,
  232. "client_id" => app.client_id,
  233. "redirect_uri" => unlisted_redirect_uri,
  234. "state" => "a_state",
  235. "nickname" => "availablenick",
  236. "email" => "available@email.com"
  237. }
  238. }
  239. )
  240. assert response = html_response(conn, 401)
  241. end
  242. test "with invalid params, POST /oauth/register?op=register renders registration_details page",
  243. %{
  244. app: app,
  245. conn: conn
  246. } do
  247. another_user = insert(:user)
  248. registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
  249. params = %{
  250. "op" => "register",
  251. "authorization" => %{
  252. "scopes" => app.scopes,
  253. "client_id" => app.client_id,
  254. "redirect_uri" => OAuthController.default_redirect_uri(app),
  255. "state" => "a_state",
  256. "nickname" => "availablenickname",
  257. "email" => "available@email.com"
  258. }
  259. }
  260. for {bad_param, bad_param_value} <-
  261. [{"nickname", another_user.nickname}, {"email", another_user.email}] do
  262. bad_registration_attrs = %{
  263. "authorization" => Map.put(params["authorization"], bad_param, bad_param_value)
  264. }
  265. bad_params = Map.merge(params, bad_registration_attrs)
  266. conn =
  267. conn
  268. |> put_session(:registration_id, registration.id)
  269. |> post("/oauth/register", bad_params)
  270. assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/
  271. assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken."
  272. end
  273. end
  274. test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`",
  275. %{
  276. app: app,
  277. conn: conn
  278. } do
  279. user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("testpassword"))
  280. registration = insert(:registration, user: nil)
  281. redirect_uri = OAuthController.default_redirect_uri(app)
  282. conn =
  283. conn
  284. |> put_session(:registration_id, registration.id)
  285. |> post(
  286. "/oauth/register",
  287. %{
  288. "op" => "connect",
  289. "authorization" => %{
  290. "scopes" => app.scopes,
  291. "client_id" => app.client_id,
  292. "redirect_uri" => redirect_uri,
  293. "state" => "a_state",
  294. "name" => user.nickname,
  295. "password" => "testpassword"
  296. }
  297. }
  298. )
  299. assert response = html_response(conn, 302)
  300. assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
  301. end
  302. test "with unlisted `redirect_uri`, POST /oauth/register?op=connect results in HTTP 401`",
  303. %{
  304. app: app,
  305. conn: conn
  306. } do
  307. user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("testpassword"))
  308. registration = insert(:registration, user: nil)
  309. unlisted_redirect_uri = "http://cross-site-request.com"
  310. conn =
  311. conn
  312. |> put_session(:registration_id, registration.id)
  313. |> post(
  314. "/oauth/register",
  315. %{
  316. "op" => "connect",
  317. "authorization" => %{
  318. "scopes" => app.scopes,
  319. "client_id" => app.client_id,
  320. "redirect_uri" => unlisted_redirect_uri,
  321. "state" => "a_state",
  322. "name" => user.nickname,
  323. "password" => "testpassword"
  324. }
  325. }
  326. )
  327. assert response = html_response(conn, 401)
  328. end
  329. test "with invalid params, POST /oauth/register?op=connect renders registration_details page",
  330. %{
  331. app: app,
  332. conn: conn
  333. } do
  334. user = insert(:user)
  335. registration = insert(:registration, user: nil)
  336. params = %{
  337. "op" => "connect",
  338. "authorization" => %{
  339. "scopes" => app.scopes,
  340. "client_id" => app.client_id,
  341. "redirect_uri" => OAuthController.default_redirect_uri(app),
  342. "state" => "a_state",
  343. "name" => user.nickname,
  344. "password" => "wrong password"
  345. }
  346. }
  347. conn =
  348. conn
  349. |> put_session(:registration_id, registration.id)
  350. |> post("/oauth/register", params)
  351. assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/
  352. assert get_flash(conn, :error) == "Invalid Username/Password"
  353. end
  354. end
  355. describe "GET /oauth/authorize" do
  356. setup do
  357. [
  358. app: insert(:oauth_app, redirect_uris: "https://redirect.url"),
  359. conn:
  360. build_conn()
  361. |> Plug.Session.call(Plug.Session.init(@session_opts))
  362. |> fetch_session()
  363. ]
  364. end
  365. test "renders authentication page", %{app: app, conn: conn} do
  366. conn =
  367. get(
  368. conn,
  369. "/oauth/authorize",
  370. %{
  371. "response_type" => "code",
  372. "client_id" => app.client_id,
  373. "redirect_uri" => OAuthController.default_redirect_uri(app),
  374. "scope" => "read"
  375. }
  376. )
  377. assert html_response(conn, 200) =~ ~s(type="submit")
  378. end
  379. test "properly handles internal calls with `authorization`-wrapped params", %{
  380. app: app,
  381. conn: conn
  382. } do
  383. conn =
  384. get(
  385. conn,
  386. "/oauth/authorize",
  387. %{
  388. "authorization" => %{
  389. "response_type" => "code",
  390. "client_id" => app.client_id,
  391. "redirect_uri" => OAuthController.default_redirect_uri(app),
  392. "scope" => "read"
  393. }
  394. }
  395. )
  396. assert html_response(conn, 200) =~ ~s(type="submit")
  397. end
  398. test "renders authentication page if user is already authenticated but `force_login` is tru-ish",
  399. %{app: app, conn: conn} do
  400. token = insert(:oauth_token, app: app)
  401. conn =
  402. conn
  403. |> put_session(:oauth_token, token.token)
  404. |> get(
  405. "/oauth/authorize",
  406. %{
  407. "response_type" => "code",
  408. "client_id" => app.client_id,
  409. "redirect_uri" => OAuthController.default_redirect_uri(app),
  410. "scope" => "read",
  411. "force_login" => "true"
  412. }
  413. )
  414. assert html_response(conn, 200) =~ ~s(type="submit")
  415. end
  416. test "renders authentication page if user is already authenticated but user request with another client",
  417. %{
  418. app: app,
  419. conn: conn
  420. } do
  421. token = insert(:oauth_token, app: app)
  422. conn =
  423. conn
  424. |> put_session(:oauth_token, token.token)
  425. |> get(
  426. "/oauth/authorize",
  427. %{
  428. "response_type" => "code",
  429. "client_id" => "another_client_id",
  430. "redirect_uri" => OAuthController.default_redirect_uri(app),
  431. "scope" => "read"
  432. }
  433. )
  434. assert html_response(conn, 200) =~ ~s(type="submit")
  435. end
  436. test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params",
  437. %{
  438. app: app,
  439. conn: conn
  440. } do
  441. token = insert(:oauth_token, app: app)
  442. conn =
  443. conn
  444. |> put_session(:oauth_token, token.token)
  445. |> get(
  446. "/oauth/authorize",
  447. %{
  448. "response_type" => "code",
  449. "client_id" => app.client_id,
  450. "redirect_uri" => OAuthController.default_redirect_uri(app),
  451. "state" => "specific_client_state",
  452. "scope" => "read"
  453. }
  454. )
  455. assert URI.decode(redirected_to(conn)) ==
  456. "https://redirect.url?access_token=#{token.token}&state=specific_client_state"
  457. end
  458. test "with existing authentication and unlisted non-OOB `redirect_uri`, redirects without credentials",
  459. %{
  460. app: app,
  461. conn: conn
  462. } do
  463. unlisted_redirect_uri = "http://cross-site-request.com"
  464. token = insert(:oauth_token, app: app)
  465. conn =
  466. conn
  467. |> put_session(:oauth_token, token.token)
  468. |> get(
  469. "/oauth/authorize",
  470. %{
  471. "response_type" => "code",
  472. "client_id" => app.client_id,
  473. "redirect_uri" => unlisted_redirect_uri,
  474. "state" => "specific_client_state",
  475. "scope" => "read"
  476. }
  477. )
  478. assert redirected_to(conn) == unlisted_redirect_uri
  479. end
  480. test "with existing authentication and OOB `redirect_uri`, redirects to app with `token` and `state` params",
  481. %{
  482. app: app,
  483. conn: conn
  484. } do
  485. token = insert(:oauth_token, app: app)
  486. conn =
  487. conn
  488. |> put_session(:oauth_token, token.token)
  489. |> get(
  490. "/oauth/authorize",
  491. %{
  492. "response_type" => "code",
  493. "client_id" => app.client_id,
  494. "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
  495. "scope" => "read"
  496. }
  497. )
  498. assert html_response(conn, 200) =~ "Authorization exists"
  499. end
  500. end
  501. describe "POST /oauth/authorize" do
  502. test "redirects with oauth authorization, " <>
  503. "granting requested app-supported scopes to both admin- and non-admin users" do
  504. app_scopes = ["read", "write", "admin", "secret_scope"]
  505. app = insert(:oauth_app, scopes: app_scopes)
  506. redirect_uri = OAuthController.default_redirect_uri(app)
  507. non_admin = insert(:user, is_admin: false)
  508. admin = insert(:user, is_admin: true)
  509. scopes_subset = ["read:subscope", "write", "admin"]
  510. # In case scope param is missing, expecting _all_ app-supported scopes to be granted
  511. for user <- [non_admin, admin],
  512. {requested_scopes, expected_scopes} <-
  513. %{scopes_subset => scopes_subset, nil: app_scopes} do
  514. conn =
  515. post(
  516. build_conn(),
  517. "/oauth/authorize",
  518. %{
  519. "authorization" => %{
  520. "name" => user.nickname,
  521. "password" => "test",
  522. "client_id" => app.client_id,
  523. "redirect_uri" => redirect_uri,
  524. "scope" => requested_scopes,
  525. "state" => "statepassed"
  526. }
  527. }
  528. )
  529. target = redirected_to(conn)
  530. assert target =~ redirect_uri
  531. query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
  532. assert %{"state" => "statepassed", "code" => code} = query
  533. auth = Repo.get_by(Authorization, token: code)
  534. assert auth
  535. assert auth.scopes == expected_scopes
  536. end
  537. end
  538. test "redirect to on two-factor auth page" do
  539. otp_secret = TOTP.generate_secret()
  540. user =
  541. insert(:user,
  542. multi_factor_authentication_settings: %MFA.Settings{
  543. enabled: true,
  544. totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
  545. }
  546. )
  547. app = insert(:oauth_app, scopes: ["read", "write", "follow"])
  548. conn =
  549. build_conn()
  550. |> post("/oauth/authorize", %{
  551. "authorization" => %{
  552. "name" => user.nickname,
  553. "password" => "test",
  554. "client_id" => app.client_id,
  555. "redirect_uri" => app.redirect_uris,
  556. "scope" => "read write",
  557. "state" => "statepassed"
  558. }
  559. })
  560. result = html_response(conn, 200)
  561. mfa_token = Repo.get_by(MFA.Token, user_id: user.id)
  562. assert result =~ app.redirect_uris
  563. assert result =~ "statepassed"
  564. assert result =~ mfa_token.token
  565. assert result =~ "Two-factor authentication"
  566. end
  567. test "returns 401 for wrong credentials", %{conn: conn} do
  568. user = insert(:user)
  569. app = insert(:oauth_app)
  570. redirect_uri = OAuthController.default_redirect_uri(app)
  571. result =
  572. conn
  573. |> post("/oauth/authorize", %{
  574. "authorization" => %{
  575. "name" => user.nickname,
  576. "password" => "wrong",
  577. "client_id" => app.client_id,
  578. "redirect_uri" => redirect_uri,
  579. "state" => "statepassed",
  580. "scope" => Enum.join(app.scopes, " ")
  581. }
  582. })
  583. |> html_response(:unauthorized)
  584. # Keep the details
  585. assert result =~ app.client_id
  586. assert result =~ redirect_uri
  587. # Error message
  588. assert result =~ "Invalid Username/Password"
  589. end
  590. test "returns 401 for missing scopes" do
  591. user = insert(:user, is_admin: false)
  592. app = insert(:oauth_app, scopes: ["read", "write", "admin"])
  593. redirect_uri = OAuthController.default_redirect_uri(app)
  594. result =
  595. build_conn()
  596. |> post("/oauth/authorize", %{
  597. "authorization" => %{
  598. "name" => user.nickname,
  599. "password" => "test",
  600. "client_id" => app.client_id,
  601. "redirect_uri" => redirect_uri,
  602. "state" => "statepassed",
  603. "scope" => ""
  604. }
  605. })
  606. |> html_response(:unauthorized)
  607. # Keep the details
  608. assert result =~ app.client_id
  609. assert result =~ redirect_uri
  610. # Error message
  611. assert result =~ "This action is outside the authorized scopes"
  612. end
  613. test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do
  614. user = insert(:user)
  615. app = insert(:oauth_app, scopes: ["read", "write"])
  616. redirect_uri = OAuthController.default_redirect_uri(app)
  617. result =
  618. conn
  619. |> post("/oauth/authorize", %{
  620. "authorization" => %{
  621. "name" => user.nickname,
  622. "password" => "test",
  623. "client_id" => app.client_id,
  624. "redirect_uri" => redirect_uri,
  625. "state" => "statepassed",
  626. "scope" => "read write follow"
  627. }
  628. })
  629. |> html_response(:unauthorized)
  630. # Keep the details
  631. assert result =~ app.client_id
  632. assert result =~ redirect_uri
  633. # Error message
  634. assert result =~ "This action is outside the authorized scopes"
  635. end
  636. end
  637. describe "POST /oauth/token" do
  638. test "issues a token for an all-body request" do
  639. user = insert(:user)
  640. app = insert(:oauth_app, scopes: ["read", "write"])
  641. {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
  642. conn =
  643. build_conn()
  644. |> post("/oauth/token", %{
  645. "grant_type" => "authorization_code",
  646. "code" => auth.token,
  647. "redirect_uri" => OAuthController.default_redirect_uri(app),
  648. "client_id" => app.client_id,
  649. "client_secret" => app.client_secret
  650. })
  651. assert %{"access_token" => token, "me" => ap_id} = json_response(conn, 200)
  652. token = Repo.get_by(Token, token: token)
  653. assert token
  654. assert token.scopes == auth.scopes
  655. assert user.ap_id == ap_id
  656. end
  657. test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do
  658. password = "testpassword"
  659. user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password))
  660. app = insert(:oauth_app, scopes: ["read", "write"])
  661. # Note: "scope" param is intentionally omitted
  662. conn =
  663. build_conn()
  664. |> post("/oauth/token", %{
  665. "grant_type" => "password",
  666. "username" => user.nickname,
  667. "password" => password,
  668. "client_id" => app.client_id,
  669. "client_secret" => app.client_secret
  670. })
  671. assert %{"access_token" => token} = json_response(conn, 200)
  672. token = Repo.get_by(Token, token: token)
  673. assert token
  674. assert token.scopes == app.scopes
  675. end
  676. test "issues a mfa token for `password` grant_type, when MFA enabled" do
  677. password = "testpassword"
  678. otp_secret = TOTP.generate_secret()
  679. user =
  680. insert(:user,
  681. password_hash: Pbkdf2.hash_pwd_salt(password),
  682. multi_factor_authentication_settings: %MFA.Settings{
  683. enabled: true,
  684. totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
  685. }
  686. )
  687. app = insert(:oauth_app, scopes: ["read", "write"])
  688. response =
  689. build_conn()
  690. |> post("/oauth/token", %{
  691. "grant_type" => "password",
  692. "username" => user.nickname,
  693. "password" => password,
  694. "client_id" => app.client_id,
  695. "client_secret" => app.client_secret
  696. })
  697. |> json_response(403)
  698. assert match?(
  699. %{
  700. "supported_challenge_types" => "totp",
  701. "mfa_token" => _,
  702. "error" => "mfa_required"
  703. },
  704. response
  705. )
  706. token = Repo.get_by(MFA.Token, token: response["mfa_token"])
  707. assert token.user_id == user.id
  708. assert token.authorization_id
  709. end
  710. test "issues a token for request with HTTP basic auth client credentials" do
  711. user = insert(:user)
  712. app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"])
  713. {:ok, auth} = Authorization.create_authorization(app, user, ["scope1", "scope2"])
  714. assert auth.scopes == ["scope1", "scope2"]
  715. app_encoded =
  716. (URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret))
  717. |> Base.encode64()
  718. conn =
  719. build_conn()
  720. |> put_req_header("authorization", "Basic " <> app_encoded)
  721. |> post("/oauth/token", %{
  722. "grant_type" => "authorization_code",
  723. "code" => auth.token,
  724. "redirect_uri" => OAuthController.default_redirect_uri(app)
  725. })
  726. assert %{"access_token" => token, "scope" => scope} = json_response(conn, 200)
  727. assert scope == "scope1 scope2"
  728. token = Repo.get_by(Token, token: token)
  729. assert token
  730. assert token.scopes == ["scope1", "scope2"]
  731. end
  732. test "issue a token for client_credentials grant type" do
  733. app = insert(:oauth_app, scopes: ["read", "write"])
  734. conn =
  735. build_conn()
  736. |> post("/oauth/token", %{
  737. "grant_type" => "client_credentials",
  738. "client_id" => app.client_id,
  739. "client_secret" => app.client_secret
  740. })
  741. assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
  742. json_response(conn, 200)
  743. assert token
  744. token_from_db = Repo.get_by(Token, token: token)
  745. assert token_from_db
  746. assert refresh
  747. assert scope == "read write"
  748. end
  749. test "rejects token exchange with invalid client credentials" do
  750. user = insert(:user)
  751. app = insert(:oauth_app)
  752. {:ok, auth} = Authorization.create_authorization(app, user)
  753. conn =
  754. build_conn()
  755. |> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=")
  756. |> post("/oauth/token", %{
  757. "grant_type" => "authorization_code",
  758. "code" => auth.token,
  759. "redirect_uri" => OAuthController.default_redirect_uri(app)
  760. })
  761. assert resp = json_response(conn, 400)
  762. assert %{"error" => _} = resp
  763. refute Map.has_key?(resp, "access_token")
  764. end
  765. test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do
  766. Pleroma.Config.put([:instance, :account_activation_required], true)
  767. password = "testpassword"
  768. {:ok, user} =
  769. insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password))
  770. |> User.confirmation_changeset(need_confirmation: true)
  771. |> User.update_and_set_cache()
  772. refute Pleroma.User.account_status(user) == :active
  773. app = insert(:oauth_app)
  774. conn =
  775. build_conn()
  776. |> post("/oauth/token", %{
  777. "grant_type" => "password",
  778. "username" => user.nickname,
  779. "password" => password,
  780. "client_id" => app.client_id,
  781. "client_secret" => app.client_secret
  782. })
  783. assert resp = json_response(conn, 403)
  784. assert %{"error" => _} = resp
  785. refute Map.has_key?(resp, "access_token")
  786. end
  787. test "rejects token exchange for valid credentials belonging to deactivated user" do
  788. password = "testpassword"
  789. user =
  790. insert(:user,
  791. password_hash: Pbkdf2.hash_pwd_salt(password),
  792. deactivated: true
  793. )
  794. app = insert(:oauth_app)
  795. resp =
  796. build_conn()
  797. |> post("/oauth/token", %{
  798. "grant_type" => "password",
  799. "username" => user.nickname,
  800. "password" => password,
  801. "client_id" => app.client_id,
  802. "client_secret" => app.client_secret
  803. })
  804. |> json_response(403)
  805. assert resp == %{
  806. "error" => "Your account is currently disabled",
  807. "identifier" => "account_is_disabled"
  808. }
  809. end
  810. test "rejects token exchange for user with password_reset_pending set to true" do
  811. password = "testpassword"
  812. user =
  813. insert(:user,
  814. password_hash: Pbkdf2.hash_pwd_salt(password),
  815. password_reset_pending: true
  816. )
  817. app = insert(:oauth_app, scopes: ["read", "write"])
  818. resp =
  819. build_conn()
  820. |> post("/oauth/token", %{
  821. "grant_type" => "password",
  822. "username" => user.nickname,
  823. "password" => password,
  824. "client_id" => app.client_id,
  825. "client_secret" => app.client_secret
  826. })
  827. |> json_response(403)
  828. assert resp == %{
  829. "error" => "Password reset is required",
  830. "identifier" => "password_reset_required"
  831. }
  832. end
  833. test "rejects token exchange for user with confirmation_pending set to true" do
  834. Pleroma.Config.put([:instance, :account_activation_required], true)
  835. password = "testpassword"
  836. user =
  837. insert(:user,
  838. password_hash: Pbkdf2.hash_pwd_salt(password),
  839. confirmation_pending: true
  840. )
  841. app = insert(:oauth_app, scopes: ["read", "write"])
  842. resp =
  843. build_conn()
  844. |> post("/oauth/token", %{
  845. "grant_type" => "password",
  846. "username" => user.nickname,
  847. "password" => password,
  848. "client_id" => app.client_id,
  849. "client_secret" => app.client_secret
  850. })
  851. |> json_response(403)
  852. assert resp == %{
  853. "error" => "Your login is missing a confirmed e-mail address",
  854. "identifier" => "missing_confirmed_email"
  855. }
  856. end
  857. test "rejects token exchange for valid credentials belonging to an unapproved user" do
  858. password = "testpassword"
  859. user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password), approval_pending: true)
  860. refute Pleroma.User.account_status(user) == :active
  861. app = insert(:oauth_app)
  862. conn =
  863. build_conn()
  864. |> post("/oauth/token", %{
  865. "grant_type" => "password",
  866. "username" => user.nickname,
  867. "password" => password,
  868. "client_id" => app.client_id,
  869. "client_secret" => app.client_secret
  870. })
  871. assert resp = json_response(conn, 403)
  872. assert %{"error" => _} = resp
  873. refute Map.has_key?(resp, "access_token")
  874. end
  875. test "rejects an invalid authorization code" do
  876. app = insert(:oauth_app)
  877. conn =
  878. build_conn()
  879. |> post("/oauth/token", %{
  880. "grant_type" => "authorization_code",
  881. "code" => "Imobviouslyinvalid",
  882. "redirect_uri" => OAuthController.default_redirect_uri(app),
  883. "client_id" => app.client_id,
  884. "client_secret" => app.client_secret
  885. })
  886. assert resp = json_response(conn, 400)
  887. assert %{"error" => _} = json_response(conn, 400)
  888. refute Map.has_key?(resp, "access_token")
  889. end
  890. end
  891. describe "POST /oauth/token - refresh token" do
  892. setup do: clear_config([:oauth2, :issue_new_refresh_token])
  893. test "issues a new access token with keep fresh token" do
  894. Pleroma.Config.put([:oauth2, :issue_new_refresh_token], true)
  895. user = insert(:user)
  896. app = insert(:oauth_app, scopes: ["read", "write"])
  897. {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
  898. {:ok, token} = Token.exchange_token(app, auth)
  899. response =
  900. build_conn()
  901. |> post("/oauth/token", %{
  902. "grant_type" => "refresh_token",
  903. "refresh_token" => token.refresh_token,
  904. "client_id" => app.client_id,
  905. "client_secret" => app.client_secret
  906. })
  907. |> json_response(200)
  908. ap_id = user.ap_id
  909. assert match?(
  910. %{
  911. "scope" => "write",
  912. "token_type" => "Bearer",
  913. "expires_in" => 600,
  914. "access_token" => _,
  915. "refresh_token" => _,
  916. "me" => ^ap_id
  917. },
  918. response
  919. )
  920. refute Repo.get_by(Token, token: token.token)
  921. new_token = Repo.get_by(Token, token: response["access_token"])
  922. assert new_token.refresh_token == token.refresh_token
  923. assert new_token.scopes == auth.scopes
  924. assert new_token.user_id == user.id
  925. assert new_token.app_id == app.id
  926. end
  927. test "issues a new access token with new fresh token" do
  928. Pleroma.Config.put([:oauth2, :issue_new_refresh_token], false)
  929. user = insert(:user)
  930. app = insert(:oauth_app, scopes: ["read", "write"])
  931. {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
  932. {:ok, token} = Token.exchange_token(app, auth)
  933. response =
  934. build_conn()
  935. |> post("/oauth/token", %{
  936. "grant_type" => "refresh_token",
  937. "refresh_token" => token.refresh_token,
  938. "client_id" => app.client_id,
  939. "client_secret" => app.client_secret
  940. })
  941. |> json_response(200)
  942. ap_id = user.ap_id
  943. assert match?(
  944. %{
  945. "scope" => "write",
  946. "token_type" => "Bearer",
  947. "expires_in" => 600,
  948. "access_token" => _,
  949. "refresh_token" => _,
  950. "me" => ^ap_id
  951. },
  952. response
  953. )
  954. refute Repo.get_by(Token, token: token.token)
  955. new_token = Repo.get_by(Token, token: response["access_token"])
  956. refute new_token.refresh_token == token.refresh_token
  957. assert new_token.scopes == auth.scopes
  958. assert new_token.user_id == user.id
  959. assert new_token.app_id == app.id
  960. end
  961. test "returns 400 if we try use access token" do
  962. user = insert(:user)
  963. app = insert(:oauth_app, scopes: ["read", "write"])
  964. {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
  965. {:ok, token} = Token.exchange_token(app, auth)
  966. response =
  967. build_conn()
  968. |> post("/oauth/token", %{
  969. "grant_type" => "refresh_token",
  970. "refresh_token" => token.token,
  971. "client_id" => app.client_id,
  972. "client_secret" => app.client_secret
  973. })
  974. |> json_response(400)
  975. assert %{"error" => "Invalid credentials"} == response
  976. end
  977. test "returns 400 if refresh_token invalid" do
  978. app = insert(:oauth_app, scopes: ["read", "write"])
  979. response =
  980. build_conn()
  981. |> post("/oauth/token", %{
  982. "grant_type" => "refresh_token",
  983. "refresh_token" => "token.refresh_token",
  984. "client_id" => app.client_id,
  985. "client_secret" => app.client_secret
  986. })
  987. |> json_response(400)
  988. assert %{"error" => "Invalid credentials"} == response
  989. end
  990. test "issues a new token if token expired" do
  991. user = insert(:user)
  992. app = insert(:oauth_app, scopes: ["read", "write"])
  993. {:ok, auth} = Authorization.create_authorization(app, user, ["write"])
  994. {:ok, token} = Token.exchange_token(app, auth)
  995. change =
  996. Ecto.Changeset.change(
  997. token,
  998. %{valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -86_400 * 30)}
  999. )
  1000. {:ok, access_token} = Repo.update(change)
  1001. response =
  1002. build_conn()
  1003. |> post("/oauth/token", %{
  1004. "grant_type" => "refresh_token",
  1005. "refresh_token" => access_token.refresh_token,
  1006. "client_id" => app.client_id,
  1007. "client_secret" => app.client_secret
  1008. })
  1009. |> json_response(200)
  1010. ap_id = user.ap_id
  1011. assert match?(
  1012. %{
  1013. "scope" => "write",
  1014. "token_type" => "Bearer",
  1015. "expires_in" => 600,
  1016. "access_token" => _,
  1017. "refresh_token" => _,
  1018. "me" => ^ap_id
  1019. },
  1020. response
  1021. )
  1022. refute Repo.get_by(Token, token: token.token)
  1023. token = Repo.get_by(Token, token: response["access_token"])
  1024. assert token
  1025. assert token.scopes == auth.scopes
  1026. assert token.user_id == user.id
  1027. assert token.app_id == app.id
  1028. end
  1029. end
  1030. describe "POST /oauth/token - bad request" do
  1031. test "returns 500" do
  1032. response =
  1033. build_conn()
  1034. |> post("/oauth/token", %{})
  1035. |> json_response(500)
  1036. assert %{"error" => "Bad request"} == response
  1037. end
  1038. end
  1039. describe "POST /oauth/revoke - bad request" do
  1040. test "returns 500" do
  1041. response =
  1042. build_conn()
  1043. |> post("/oauth/revoke", %{})
  1044. |> json_response(500)
  1045. assert %{"error" => "Bad request"} == response
  1046. end
  1047. end
  1048. end