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