Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma 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.append_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 render(conn, "oob_authorization_created.html", %{auth: auth})
149 end
150
151 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
152 "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
153 }) do
154 app = Repo.preload(auth, :app).app
155
156 # An extra safety measure before we redirect (also done in `do_create_authorization/2`)
157 if redirect_uri in String.split(app.redirect_uris) do
158 redirect_uri = redirect_uri(conn, redirect_uri)
159 url_params = %{code: auth.token}
160 url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
161 url = UriHelper.append_uri_params(redirect_uri, url_params)
162 redirect(conn, external: url)
163 else
164 conn
165 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
166 |> redirect(external: redirect_uri(conn, redirect_uri))
167 end
168 end
169
170 defp handle_create_authorization_error(
171 %Plug.Conn{} = conn,
172 {:error, scopes_issue},
173 %{"authorization" => _} = params
174 )
175 when scopes_issue in [:unsupported_scopes, :missing_scopes] do
176 # Per https://github.com/tootsuite/mastodon/blob/
177 # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
178 conn
179 |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
180 |> put_status(:unauthorized)
181 |> authorize(params)
182 end
183
184 defp handle_create_authorization_error(
185 %Plug.Conn{} = conn,
186 {:account_status, :confirmation_pending},
187 %{"authorization" => _} = params
188 ) do
189 conn
190 |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
191 |> put_status(:forbidden)
192 |> authorize(params)
193 end
194
195 defp handle_create_authorization_error(
196 %Plug.Conn{} = conn,
197 {:mfa_required, user, auth, _},
198 params
199 ) do
200 {:ok, token} = MFA.Token.create_token(user, auth)
201
202 data = %{
203 "mfa_token" => token.token,
204 "redirect_uri" => params["authorization"]["redirect_uri"],
205 "state" => params["authorization"]["state"]
206 }
207
208 MFAController.show(conn, data)
209 end
210
211 defp handle_create_authorization_error(
212 %Plug.Conn{} = conn,
213 {:account_status, :password_reset_pending},
214 %{"authorization" => _} = params
215 ) do
216 conn
217 |> put_flash(:error, dgettext("errors", "Password reset is required"))
218 |> put_status(:forbidden)
219 |> authorize(params)
220 end
221
222 defp handle_create_authorization_error(
223 %Plug.Conn{} = conn,
224 {:account_status, :deactivated},
225 %{"authorization" => _} = params
226 ) do
227 conn
228 |> put_flash(:error, dgettext("errors", "Your account is currently disabled"))
229 |> put_status(:forbidden)
230 |> authorize(params)
231 end
232
233 defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
234 Authenticator.handle_error(conn, error)
235 end
236
237 @doc "Renew access_token with refresh_token"
238 def token_exchange(
239 %Plug.Conn{} = conn,
240 %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
241 ) do
242 with {:ok, app} <- Token.Utils.fetch_app(conn),
243 {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
244 {:ok, token} <- RefreshToken.grant(token) do
245 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
246 else
247 _error -> render_invalid_credentials_error(conn)
248 end
249 end
250
251 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do
252 with {:ok, app} <- Token.Utils.fetch_app(conn),
253 fixed_token = Token.Utils.fix_padding(params["code"]),
254 {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
255 %User{} = user <- User.get_cached_by_id(auth.user_id),
256 {:ok, token} <- Token.exchange_token(app, auth) do
257 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
258 else
259 error ->
260 handle_token_exchange_error(conn, error)
261 end
262 end
263
264 def token_exchange(
265 %Plug.Conn{} = conn,
266 %{"grant_type" => "password"} = params
267 ) do
268 with {:ok, %User{} = user} <- Authenticator.get_user(conn),
269 {:ok, app} <- Token.Utils.fetch_app(conn),
270 requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
271 {:ok, token} <- login(user, app, requested_scopes) do
272 json(conn, OAuthView.render("token.json", %{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", "name" => name, "password" => _password} = params
282 ) do
283 params =
284 params
285 |> Map.delete("name")
286 |> Map.put("username", name)
287
288 token_exchange(conn, params)
289 end
290
291 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
292 with {:ok, app} <- Token.Utils.fetch_app(conn),
293 {:ok, auth} <- Authorization.create_authorization(app, %User{}),
294 {:ok, token} <- Token.exchange_token(app, auth) do
295 json(conn, OAuthView.render("token.json", %{token: token}))
296 else
297 _error ->
298 handle_token_exchange_error(conn, :invalid_credentails)
299 end
300 end
301
302 # Bad request
303 def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
304
305 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
306 conn
307 |> put_status(:forbidden)
308 |> json(build_and_response_mfa_token(user, auth))
309 end
310
311 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
312 render_error(
313 conn,
314 :forbidden,
315 "Your account is currently disabled",
316 %{},
317 "account_is_disabled"
318 )
319 end
320
321 defp handle_token_exchange_error(
322 %Plug.Conn{} = conn,
323 {:account_status, :password_reset_pending}
324 ) do
325 render_error(
326 conn,
327 :forbidden,
328 "Password reset is required",
329 %{},
330 "password_reset_required"
331 )
332 end
333
334 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do
335 render_error(
336 conn,
337 :forbidden,
338 "Your login is missing a confirmed e-mail address",
339 %{},
340 "missing_confirmed_email"
341 )
342 end
343
344 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
345 render_error(
346 conn,
347 :forbidden,
348 "Your account is awaiting approval.",
349 %{},
350 "awaiting_approval"
351 )
352 end
353
354 defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
355 render_invalid_credentials_error(conn)
356 end
357
358 def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
359 with {:ok, app} <- Token.Utils.fetch_app(conn),
360 {:ok, _token} <- RevokeToken.revoke(app, params) do
361 json(conn, %{})
362 else
363 _error ->
364 # RFC 7009: invalid tokens [in the request] do not cause an error response
365 json(conn, %{})
366 end
367 end
368
369 def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
370
371 # Response for bad request
372 defp bad_request(%Plug.Conn{} = conn, _) do
373 render_error(conn, :internal_server_error, "Bad request")
374 end
375
376 @doc "Prepares OAuth request to provider for Ueberauth"
377 def prepare_request(%Plug.Conn{} = conn, %{
378 "provider" => provider,
379 "authorization" => auth_attrs
380 }) do
381 scope =
382 auth_attrs
383 |> Scopes.fetch_scopes([])
384 |> Scopes.to_string()
385
386 state =
387 auth_attrs
388 |> Map.delete("scopes")
389 |> Map.put("scope", scope)
390 |> Jason.encode!()
391
392 params =
393 auth_attrs
394 |> Map.drop(~w(scope scopes client_id redirect_uri))
395 |> Map.put("state", state)
396
397 # Handing the request to Ueberauth
398 redirect(conn, to: o_auth_path(conn, :request, provider, params))
399 end
400
401 def request(%Plug.Conn{} = conn, params) do
402 message =
403 if params["provider"] do
404 dgettext("errors", "Unsupported OAuth provider: %{provider}.",
405 provider: params["provider"]
406 )
407 else
408 dgettext("errors", "Bad OAuth request.")
409 end
410
411 conn
412 |> put_flash(:error, message)
413 |> redirect(to: "/")
414 end
415
416 def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
417 params = callback_params(params)
418 messages = for e <- Map.get(failure, :errors, []), do: e.message
419 message = Enum.join(messages, "; ")
420
421 conn
422 |> put_flash(
423 :error,
424 dgettext("errors", "Failed to authenticate: %{message}.", message: message)
425 )
426 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
427 end
428
429 def callback(%Plug.Conn{} = conn, params) do
430 params = callback_params(params)
431
432 with {:ok, registration} <- Authenticator.get_registration(conn) do
433 auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
434
435 case Repo.get_assoc(registration, :user) do
436 {:ok, user} ->
437 create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
438
439 _ ->
440 registration_params =
441 Map.merge(auth_attrs, %{
442 "nickname" => Registration.nickname(registration),
443 "email" => Registration.email(registration)
444 })
445
446 conn
447 |> put_session_registration_id(registration.id)
448 |> registration_details(%{"authorization" => registration_params})
449 end
450 else
451 error ->
452 Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
453
454 conn
455 |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
456 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
457 end
458 end
459
460 defp callback_params(%{"state" => state} = params) do
461 Map.merge(params, Jason.decode!(state))
462 end
463
464 def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
465 render(conn, "register.html", %{
466 client_id: auth_attrs["client_id"],
467 redirect_uri: auth_attrs["redirect_uri"],
468 state: auth_attrs["state"],
469 scopes: Scopes.fetch_scopes(auth_attrs, []),
470 nickname: auth_attrs["nickname"],
471 email: auth_attrs["email"]
472 })
473 end
474
475 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
476 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
477 %Registration{} = registration <- Repo.get(Registration, registration_id),
478 {_, {:ok, auth, _user}} <-
479 {:create_authorization, do_create_authorization(conn, params)},
480 %User{} = user <- Repo.preload(auth, :user).user,
481 {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
482 conn
483 |> put_session_registration_id(nil)
484 |> after_create_authorization(auth, params)
485 else
486 {:create_authorization, error} ->
487 {:register, handle_create_authorization_error(conn, error, params)}
488
489 _ ->
490 {:register, :generic_error}
491 end
492 end
493
494 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
495 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
496 %Registration{} = registration <- Repo.get(Registration, registration_id),
497 {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
498 conn
499 |> put_session_registration_id(nil)
500 |> create_authorization(
501 params,
502 user: user
503 )
504 else
505 {:error, changeset} ->
506 message =
507 Enum.map(changeset.errors, fn {field, {error, _}} ->
508 "#{field} #{error}"
509 end)
510 |> Enum.join("; ")
511
512 message =
513 String.replace(
514 message,
515 "ap_id has already been taken",
516 "nickname has already been taken"
517 )
518
519 conn
520 |> put_status(:forbidden)
521 |> put_flash(:error, "Error: #{message}.")
522 |> registration_details(params)
523
524 _ ->
525 {:register, :generic_error}
526 end
527 end
528
529 defp do_create_authorization(conn, auth_attrs, user \\ nil)
530
531 defp do_create_authorization(
532 %Plug.Conn{} = conn,
533 %{
534 "authorization" =>
535 %{
536 "client_id" => client_id,
537 "redirect_uri" => redirect_uri
538 } = auth_attrs
539 },
540 user
541 ) do
542 with {_, {:ok, %User{} = user}} <-
543 {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
544 %App{} = app <- Repo.get_by(App, client_id: client_id),
545 true <- redirect_uri in String.split(app.redirect_uris),
546 requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes),
547 {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do
548 {:ok, auth, user}
549 end
550 end
551
552 defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
553 when is_list(requested_scopes) do
554 with {:account_status, :active} <- {:account_status, User.account_status(user)},
555 {:ok, scopes} <- validate_scopes(app, requested_scopes),
556 {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
557 {:ok, auth}
558 end
559 end
560
561 # Note: intended to be a private function but opened for AccountController that logs in on signup
562 @doc "If checks pass, creates authorization and token for given user, app and requested scopes."
563 def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do
564 with {:ok, auth} <- do_create_authorization(user, app, requested_scopes),
565 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
566 {:ok, token} <- Token.exchange_token(app, auth) do
567 {:ok, token}
568 end
569 end
570
571 # Special case: Local MastodonFE
572 defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
573
574 defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
575
576 defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
577
578 defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
579 do: put_session(conn, :registration_id, registration_id)
580
581 defp build_and_response_mfa_token(user, auth) do
582 with {:ok, token} <- MFA.Token.create_token(user, auth) do
583 MFAView.render("mfa_response.json", %{token: token, user: user})
584 end
585 end
586
587 @spec validate_scopes(App.t(), map() | list()) ::
588 {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
589 defp validate_scopes(%App{} = app, params) when is_map(params) do
590 requested_scopes = Scopes.fetch_scopes(params, app.scopes)
591 validate_scopes(app, requested_scopes)
592 end
593
594 defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
595 Scopes.validate(requested_scopes, app.scopes)
596 end
597
598 def default_redirect_uri(%App{} = app) do
599 app.redirect_uris
600 |> String.split()
601 |> Enum.at(0)
602 end
603
604 defp render_invalid_credentials_error(conn) do
605 render_error(conn, :bad_request, "Invalid credentials")
606 end
607 end