logo

pleroma

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

oauth_controller.ex (20277B)


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