Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

614 lignes
20KB

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