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