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