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