logo

pleroma

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

o_auth_controller.ex (20692B)


  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.OAuthController do
  5. use Pleroma.Web, :controller
  6. alias Pleroma.Helpers.AuthHelper
  7. alias Pleroma.Helpers.UriHelper
  8. alias Pleroma.Maps
  9. alias Pleroma.MFA
  10. alias Pleroma.Registration
  11. alias Pleroma.Repo
  12. alias Pleroma.User
  13. alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator
  14. alias Pleroma.Web.OAuth.App
  15. alias Pleroma.Web.OAuth.Authorization
  16. alias Pleroma.Web.OAuth.MFAController
  17. alias Pleroma.Web.OAuth.MFAView
  18. alias Pleroma.Web.OAuth.OAuthView
  19. alias Pleroma.Web.OAuth.Scopes
  20. alias Pleroma.Web.OAuth.Token
  21. alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
  22. alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
  23. alias Pleroma.Web.Plugs.RateLimiter
  24. alias Pleroma.Web.Utils.Params
  25. require Logger
  26. if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
  27. plug(:fetch_session)
  28. plug(:fetch_flash)
  29. plug(:skip_auth)
  30. plug(RateLimiter, [name: :authentication] when action == :create_authorization)
  31. action_fallback(Pleroma.Web.OAuth.FallbackController)
  32. @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"
  33. # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg
  34. def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do
  35. {auth_attrs, params} = Map.pop(params, "authorization")
  36. authorize(conn, Map.merge(params, auth_attrs))
  37. end
  38. def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do
  39. if Params.truthy_param?(params["force_login"]) do
  40. do_authorize(conn, params)
  41. else
  42. handle_existing_authorization(conn, params)
  43. end
  44. end
  45. # Note: the token is set in oauth_plug, but the token and client do not always go together.
  46. # For example, MastodonFE's token is set if user requests with another client,
  47. # after user already authorized to MastodonFE.
  48. # So we have to check client and token.
  49. def authorize(
  50. %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
  51. %{"client_id" => client_id} = params
  52. ) do
  53. with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app),
  54. ^client_id <- t.app.client_id do
  55. handle_existing_authorization(conn, params)
  56. else
  57. _ -> do_authorize(conn, params)
  58. end
  59. end
  60. def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
  61. defp do_authorize(%Plug.Conn{} = conn, params) do
  62. app = Repo.get_by(App, client_id: params["client_id"])
  63. available_scopes = (app && app.scopes) || []
  64. scopes = Scopes.fetch_scopes(params, available_scopes)
  65. user =
  66. with %{assigns: %{user: %User{} = user}} <- conn do
  67. user
  68. else
  69. _ -> nil
  70. end
  71. scopes =
  72. if scopes == [] do
  73. available_scopes
  74. else
  75. scopes
  76. end
  77. # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
  78. render(conn, Authenticator.auth_template(), %{
  79. user: user,
  80. app: app && Map.delete(app, :client_secret),
  81. response_type: params["response_type"],
  82. client_id: params["client_id"],
  83. available_scopes: available_scopes,
  84. scopes: scopes,
  85. redirect_uri: params["redirect_uri"],
  86. state: params["state"],
  87. params: params
  88. })
  89. end
  90. defp handle_existing_authorization(
  91. %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
  92. %{"redirect_uri" => @oob_token_redirect_uri}
  93. ) do
  94. render(conn, "oob_token_exists.html", %{token: token})
  95. end
  96. defp handle_existing_authorization(
  97. %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
  98. %{} = params
  99. ) do
  100. app = Repo.preload(token, :app).app
  101. redirect_uri =
  102. if is_binary(params["redirect_uri"]) do
  103. params["redirect_uri"]
  104. else
  105. default_redirect_uri(app)
  106. end
  107. if redirect_uri in String.split(app.redirect_uris) do
  108. redirect_uri = redirect_uri(conn, redirect_uri)
  109. url_params = %{access_token: token.token}
  110. url_params = Maps.put_if_present(url_params, :state, params["state"])
  111. url = UriHelper.modify_uri_params(redirect_uri, url_params)
  112. redirect(conn, external: url)
  113. else
  114. conn
  115. |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
  116. |> redirect(external: redirect_uri(conn, redirect_uri))
  117. end
  118. end
  119. def create_authorization(_, _, opts \\ [])
  120. def create_authorization(%Plug.Conn{assigns: %{user: %User{} = user}} = conn, params, []) do
  121. create_authorization(conn, params, user: user)
  122. end
  123. def create_authorization(%Plug.Conn{} = conn, %{"authorization" => _} = params, opts) do
  124. with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
  125. {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
  126. after_create_authorization(conn, auth, params)
  127. else
  128. error ->
  129. handle_create_authorization_error(conn, error, params)
  130. end
  131. end
  132. def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
  133. "authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
  134. }) do
  135. # Enforcing the view to reuse the template when calling from other controllers
  136. conn
  137. |> put_view(OAuthView)
  138. |> render("oob_authorization_created.html", %{auth: auth})
  139. end
  140. def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
  141. "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
  142. }) do
  143. app = Repo.preload(auth, :app).app
  144. # An extra safety measure before we redirect (also done in `do_create_authorization/2`)
  145. if redirect_uri in String.split(app.redirect_uris) do
  146. redirect_uri = redirect_uri(conn, redirect_uri)
  147. url_params = %{code: auth.token}
  148. url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
  149. url = UriHelper.modify_uri_params(redirect_uri, url_params)
  150. redirect(conn, external: url)
  151. else
  152. conn
  153. |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
  154. |> redirect(external: redirect_uri(conn, redirect_uri))
  155. end
  156. end
  157. defp handle_create_authorization_error(
  158. %Plug.Conn{} = conn,
  159. {:error, scopes_issue},
  160. %{"authorization" => _} = params
  161. )
  162. when scopes_issue in [:unsupported_scopes, :missing_scopes] do
  163. # Per https://github.com/tootsuite/mastodon/blob/
  164. # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
  165. conn
  166. |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
  167. |> put_status(:unauthorized)
  168. |> authorize(params)
  169. end
  170. defp handle_create_authorization_error(
  171. %Plug.Conn{} = conn,
  172. {:account_status, :confirmation_pending},
  173. %{"authorization" => _} = params
  174. ) do
  175. conn
  176. |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
  177. |> put_status(:forbidden)
  178. |> authorize(params)
  179. end
  180. defp handle_create_authorization_error(
  181. %Plug.Conn{} = conn,
  182. {:mfa_required, user, auth, _},
  183. params
  184. ) do
  185. {:ok, token} = MFA.Token.create(user, auth)
  186. data = %{
  187. "mfa_token" => token.token,
  188. "redirect_uri" => params["authorization"]["redirect_uri"],
  189. "state" => params["authorization"]["state"]
  190. }
  191. MFAController.show(conn, data)
  192. end
  193. defp handle_create_authorization_error(
  194. %Plug.Conn{} = conn,
  195. {:account_status, :password_reset_pending},
  196. %{"authorization" => _} = params
  197. ) do
  198. conn
  199. |> put_flash(:error, dgettext("errors", "Password reset is required"))
  200. |> put_status(:forbidden)
  201. |> authorize(params)
  202. end
  203. defp handle_create_authorization_error(
  204. %Plug.Conn{} = conn,
  205. {:account_status, :deactivated},
  206. %{"authorization" => _} = params
  207. ) do
  208. conn
  209. |> put_flash(:error, dgettext("errors", "Your account is currently disabled"))
  210. |> put_status(:forbidden)
  211. |> authorize(params)
  212. end
  213. defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
  214. Authenticator.handle_error(conn, error)
  215. end
  216. @doc "Renew access_token with refresh_token"
  217. def token_exchange(
  218. %Plug.Conn{} = conn,
  219. %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
  220. ) do
  221. with {:ok, app} <- Token.Utils.fetch_app(conn),
  222. {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
  223. {:ok, token} <- RefreshToken.grant(token) do
  224. after_token_exchange(conn, %{user: user, token: token})
  225. else
  226. _error -> render_invalid_credentials_error(conn)
  227. end
  228. end
  229. def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do
  230. with {:ok, app} <- Token.Utils.fetch_app(conn),
  231. fixed_token = Token.Utils.fix_padding(params["code"]),
  232. {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
  233. %User{} = user <- User.get_cached_by_id(auth.user_id),
  234. {:ok, token} <- Token.exchange_token(app, auth) do
  235. after_token_exchange(conn, %{user: user, token: token})
  236. else
  237. error ->
  238. handle_token_exchange_error(conn, error)
  239. end
  240. end
  241. def token_exchange(
  242. %Plug.Conn{} = conn,
  243. %{"grant_type" => "password"} = params
  244. ) do
  245. with {:ok, %User{} = user} <- Authenticator.get_user(conn),
  246. {:ok, app} <- Token.Utils.fetch_app(conn),
  247. requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
  248. {:ok, token} <- login(user, app, requested_scopes) do
  249. after_token_exchange(conn, %{user: user, token: token})
  250. else
  251. error ->
  252. handle_token_exchange_error(conn, error)
  253. end
  254. end
  255. def token_exchange(
  256. %Plug.Conn{} = conn,
  257. %{"grant_type" => "password", "name" => name, "password" => _password} = params
  258. ) do
  259. params =
  260. params
  261. |> Map.delete("name")
  262. |> Map.put("username", name)
  263. token_exchange(conn, params)
  264. end
  265. def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
  266. with {:ok, app} <- Token.Utils.fetch_app(conn),
  267. {:ok, auth} <- Authorization.create_authorization(app, %User{}),
  268. {:ok, token} <- Token.exchange_token(app, auth) do
  269. after_token_exchange(conn, %{token: token})
  270. else
  271. _error ->
  272. handle_token_exchange_error(conn, :invalid_credentials)
  273. end
  274. end
  275. # Bad request
  276. def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
  277. def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
  278. conn
  279. |> AuthHelper.put_session_token(token.token)
  280. |> json(OAuthView.render("token.json", view_params))
  281. end
  282. defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
  283. conn
  284. |> put_status(:forbidden)
  285. |> json(build_and_response_mfa_token(user, auth))
  286. end
  287. defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
  288. render_error(
  289. conn,
  290. :forbidden,
  291. "Your account is currently disabled",
  292. %{},
  293. "account_is_disabled"
  294. )
  295. end
  296. defp handle_token_exchange_error(
  297. %Plug.Conn{} = conn,
  298. {:account_status, :password_reset_pending}
  299. ) do
  300. render_error(
  301. conn,
  302. :forbidden,
  303. "Password reset is required",
  304. %{},
  305. "password_reset_required"
  306. )
  307. end
  308. defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do
  309. render_error(
  310. conn,
  311. :forbidden,
  312. "Your login is missing a confirmed e-mail address",
  313. %{},
  314. "missing_confirmed_email"
  315. )
  316. end
  317. defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
  318. render_error(
  319. conn,
  320. :forbidden,
  321. "Your account is awaiting approval.",
  322. %{},
  323. "awaiting_approval"
  324. )
  325. end
  326. defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
  327. render_invalid_credentials_error(conn)
  328. end
  329. def token_revoke(%Plug.Conn{} = conn, %{"token" => token}) do
  330. with {:ok, %Token{} = oauth_token} <- Token.get_by_token(token),
  331. {:ok, oauth_token} <- RevokeToken.revoke(oauth_token) do
  332. conn =
  333. with session_token = AuthHelper.get_session_token(conn),
  334. %Token{token: ^session_token} <- oauth_token do
  335. AuthHelper.delete_session_token(conn)
  336. else
  337. _ -> conn
  338. end
  339. json(conn, %{})
  340. else
  341. _error ->
  342. # RFC 7009: invalid tokens [in the request] do not cause an error response
  343. json(conn, %{})
  344. end
  345. end
  346. def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
  347. # Response for bad request
  348. defp bad_request(%Plug.Conn{} = conn, _) do
  349. render_error(conn, :internal_server_error, "Bad request")
  350. end
  351. @doc "Prepares OAuth request to provider for Ueberauth"
  352. def prepare_request(%Plug.Conn{} = conn, %{
  353. "provider" => provider,
  354. "authorization" => auth_attrs
  355. }) do
  356. scope =
  357. auth_attrs
  358. |> Scopes.fetch_scopes([])
  359. |> Scopes.to_string()
  360. state =
  361. auth_attrs
  362. |> Map.delete("scopes")
  363. |> Map.put("scope", scope)
  364. |> Jason.encode!()
  365. params =
  366. auth_attrs
  367. |> Map.drop(~w(scope scopes client_id redirect_uri))
  368. |> Map.put("state", state)
  369. # Handing the request to Ueberauth
  370. redirect(conn, to: Routes.o_auth_path(conn, :request, provider, params))
  371. end
  372. def request(%Plug.Conn{} = conn, params) do
  373. message =
  374. if params["provider"] do
  375. dgettext("errors", "Unsupported OAuth provider: %{provider}.",
  376. provider: params["provider"]
  377. )
  378. else
  379. dgettext("errors", "Bad OAuth request.")
  380. end
  381. conn
  382. |> put_flash(:error, message)
  383. |> redirect(to: "/")
  384. end
  385. def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
  386. params = callback_params(params)
  387. messages = for e <- Map.get(failure, :errors, []), do: e.message
  388. message = Enum.join(messages, "; ")
  389. conn
  390. |> put_flash(
  391. :error,
  392. dgettext("errors", "Failed to authenticate: %{message}.", message: message)
  393. )
  394. |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
  395. end
  396. def callback(%Plug.Conn{} = conn, params) do
  397. params = callback_params(params)
  398. with {:ok, registration} <- Authenticator.get_registration(conn) do
  399. auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
  400. case Repo.get_assoc(registration, :user) do
  401. {:ok, user} ->
  402. create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
  403. _ ->
  404. registration_params =
  405. Map.merge(auth_attrs, %{
  406. "nickname" => Registration.nickname(registration),
  407. "email" => Registration.email(registration)
  408. })
  409. conn
  410. |> put_session_registration_id(registration.id)
  411. |> registration_details(%{"authorization" => registration_params})
  412. end
  413. else
  414. error ->
  415. Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
  416. conn
  417. |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
  418. |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
  419. end
  420. end
  421. defp callback_params(%{"state" => state} = params) do
  422. Map.merge(params, Jason.decode!(state))
  423. end
  424. def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
  425. render(conn, "register.html", %{
  426. client_id: auth_attrs["client_id"],
  427. redirect_uri: auth_attrs["redirect_uri"],
  428. state: auth_attrs["state"],
  429. scopes: Scopes.fetch_scopes(auth_attrs, []),
  430. nickname: auth_attrs["nickname"],
  431. email: auth_attrs["email"]
  432. })
  433. end
  434. def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
  435. with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
  436. %Registration{} = registration <- Repo.get(Registration, registration_id),
  437. {_, {:ok, auth, _user}} <-
  438. {:create_authorization, do_create_authorization(conn, params)},
  439. %User{} = user <- Repo.preload(auth, :user).user,
  440. {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
  441. conn
  442. |> put_session_registration_id(nil)
  443. |> after_create_authorization(auth, params)
  444. else
  445. {:create_authorization, error} ->
  446. {:register, handle_create_authorization_error(conn, error, params)}
  447. _ ->
  448. {:register, :generic_error}
  449. end
  450. end
  451. def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
  452. with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
  453. %Registration{} = registration <- Repo.get(Registration, registration_id),
  454. {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
  455. conn
  456. |> put_session_registration_id(nil)
  457. |> create_authorization(
  458. params,
  459. user: user
  460. )
  461. else
  462. {:error, changeset} ->
  463. message =
  464. Enum.map(changeset.errors, fn {field, {error, _}} ->
  465. "#{field} #{error}"
  466. end)
  467. |> Enum.join("; ")
  468. message =
  469. String.replace(
  470. message,
  471. "ap_id has already been taken",
  472. "nickname has already been taken"
  473. )
  474. conn
  475. |> put_status(:forbidden)
  476. |> put_flash(:error, "Error: #{message}.")
  477. |> registration_details(params)
  478. _ ->
  479. {:register, :generic_error}
  480. end
  481. end
  482. defp do_create_authorization(conn, auth_attrs, user \\ nil)
  483. defp do_create_authorization(
  484. %Plug.Conn{} = conn,
  485. %{
  486. "authorization" =>
  487. %{
  488. "client_id" => client_id,
  489. "redirect_uri" => redirect_uri
  490. } = auth_attrs
  491. },
  492. user
  493. ) do
  494. with {_, {:ok, %User{} = user}} <-
  495. {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
  496. %App{} = app <- Repo.get_by(App, client_id: client_id),
  497. true <- redirect_uri in String.split(app.redirect_uris),
  498. requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes),
  499. {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do
  500. {:ok, auth, user}
  501. end
  502. end
  503. defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
  504. when is_list(requested_scopes) do
  505. with {:account_status, :active} <- {:account_status, User.account_status(user)},
  506. {:ok, scopes} <- validate_scopes(app, requested_scopes),
  507. {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
  508. {:ok, auth}
  509. end
  510. end
  511. # Note: intended to be a private function but opened for AccountController that logs in on signup
  512. @doc "If checks pass, creates authorization and token for given user, app and requested scopes."
  513. def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do
  514. with {:ok, auth} <- do_create_authorization(user, app, requested_scopes),
  515. {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
  516. {:ok, token} <- Token.exchange_token(app, auth) do
  517. {:ok, token}
  518. end
  519. end
  520. defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
  521. defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
  522. defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
  523. do: put_session(conn, :registration_id, registration_id)
  524. defp build_and_response_mfa_token(user, auth) do
  525. with {:ok, token} <- MFA.Token.create(user, auth) do
  526. MFAView.render("mfa_response.json", %{token: token, user: user})
  527. end
  528. end
  529. @spec validate_scopes(App.t(), list()) ::
  530. {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
  531. defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
  532. Scopes.validate(requested_scopes, app.scopes)
  533. end
  534. def default_redirect_uri(%App{} = app) do
  535. app.redirect_uris
  536. |> String.split()
  537. |> Enum.at(0)
  538. end
  539. defp render_invalid_credentials_error(conn) do
  540. render_error(conn, :bad_request, "Invalid credentials")
  541. end
  542. end