logo

pleroma

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

o_auth_controller_test.exs (40490B)


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