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