logo

pleroma

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

o_auth_controller_test.exs (40169B)


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