Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into chat-federation...
[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 {:account_status, :active} <- {:account_status, User.account_status(user)},
264 {:ok, scopes} <- validate_scopes(app, params),
265 {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
266 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
267 {:ok, token} <- Token.exchange_token(app, auth) do
268 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
269 else
270 error ->
271 handle_token_exchange_error(conn, error)
272 end
273 end
274
275 def token_exchange(
276 %Plug.Conn{} = conn,
277 %{"grant_type" => "password", "name" => name, "password" => _password} = params
278 ) do
279 params =
280 params
281 |> Map.delete("name")
282 |> Map.put("username", name)
283
284 token_exchange(conn, params)
285 end
286
287 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
288 with {:ok, app} <- Token.Utils.fetch_app(conn),
289 {:ok, auth} <- Authorization.create_authorization(app, %User{}),
290 {:ok, token} <- Token.exchange_token(app, auth) do
291 json(conn, OAuthView.render("token.json", %{token: token}))
292 else
293 _error ->
294 handle_token_exchange_error(conn, :invalid_credentails)
295 end
296 end
297
298 # Bad request
299 def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
300
301 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
302 conn
303 |> put_status(:forbidden)
304 |> json(build_and_response_mfa_token(user, auth))
305 end
306
307 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
308 render_error(
309 conn,
310 :forbidden,
311 "Your account is currently disabled",
312 %{},
313 "account_is_disabled"
314 )
315 end
316
317 defp handle_token_exchange_error(
318 %Plug.Conn{} = conn,
319 {:account_status, :password_reset_pending}
320 ) do
321 render_error(
322 conn,
323 :forbidden,
324 "Password reset is required",
325 %{},
326 "password_reset_required"
327 )
328 end
329
330 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do
331 render_error(
332 conn,
333 :forbidden,
334 "Your login is missing a confirmed e-mail address",
335 %{},
336 "missing_confirmed_email"
337 )
338 end
339
340 defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
341 render_invalid_credentials_error(conn)
342 end
343
344 def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
345 with {:ok, app} <- Token.Utils.fetch_app(conn),
346 {:ok, _token} <- RevokeToken.revoke(app, params) do
347 json(conn, %{})
348 else
349 _error ->
350 # RFC 7009: invalid tokens [in the request] do not cause an error response
351 json(conn, %{})
352 end
353 end
354
355 def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
356
357 # Response for bad request
358 defp bad_request(%Plug.Conn{} = conn, _) do
359 render_error(conn, :internal_server_error, "Bad request")
360 end
361
362 @doc "Prepares OAuth request to provider for Ueberauth"
363 def prepare_request(%Plug.Conn{} = conn, %{
364 "provider" => provider,
365 "authorization" => auth_attrs
366 }) do
367 scope =
368 auth_attrs
369 |> Scopes.fetch_scopes([])
370 |> Scopes.to_string()
371
372 state =
373 auth_attrs
374 |> Map.delete("scopes")
375 |> Map.put("scope", scope)
376 |> Jason.encode!()
377
378 params =
379 auth_attrs
380 |> Map.drop(~w(scope scopes client_id redirect_uri))
381 |> Map.put("state", state)
382
383 # Handing the request to Ueberauth
384 redirect(conn, to: o_auth_path(conn, :request, provider, params))
385 end
386
387 def request(%Plug.Conn{} = conn, params) do
388 message =
389 if params["provider"] do
390 dgettext("errors", "Unsupported OAuth provider: %{provider}.",
391 provider: params["provider"]
392 )
393 else
394 dgettext("errors", "Bad OAuth request.")
395 end
396
397 conn
398 |> put_flash(:error, message)
399 |> redirect(to: "/")
400 end
401
402 def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
403 params = callback_params(params)
404 messages = for e <- Map.get(failure, :errors, []), do: e.message
405 message = Enum.join(messages, "; ")
406
407 conn
408 |> put_flash(
409 :error,
410 dgettext("errors", "Failed to authenticate: %{message}.", message: message)
411 )
412 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
413 end
414
415 def callback(%Plug.Conn{} = conn, params) do
416 params = callback_params(params)
417
418 with {:ok, registration} <- Authenticator.get_registration(conn) do
419 auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
420
421 case Repo.get_assoc(registration, :user) do
422 {:ok, user} ->
423 create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
424
425 _ ->
426 registration_params =
427 Map.merge(auth_attrs, %{
428 "nickname" => Registration.nickname(registration),
429 "email" => Registration.email(registration)
430 })
431
432 conn
433 |> put_session_registration_id(registration.id)
434 |> registration_details(%{"authorization" => registration_params})
435 end
436 else
437 error ->
438 Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
439
440 conn
441 |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
442 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
443 end
444 end
445
446 defp callback_params(%{"state" => state} = params) do
447 Map.merge(params, Jason.decode!(state))
448 end
449
450 def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
451 render(conn, "register.html", %{
452 client_id: auth_attrs["client_id"],
453 redirect_uri: auth_attrs["redirect_uri"],
454 state: auth_attrs["state"],
455 scopes: Scopes.fetch_scopes(auth_attrs, []),
456 nickname: auth_attrs["nickname"],
457 email: auth_attrs["email"]
458 })
459 end
460
461 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
462 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
463 %Registration{} = registration <- Repo.get(Registration, registration_id),
464 {_, {:ok, auth, _user}} <-
465 {:create_authorization, do_create_authorization(conn, params)},
466 %User{} = user <- Repo.preload(auth, :user).user,
467 {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
468 conn
469 |> put_session_registration_id(nil)
470 |> after_create_authorization(auth, params)
471 else
472 {:create_authorization, error} ->
473 {:register, handle_create_authorization_error(conn, error, params)}
474
475 _ ->
476 {:register, :generic_error}
477 end
478 end
479
480 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
481 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
482 %Registration{} = registration <- Repo.get(Registration, registration_id),
483 {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
484 conn
485 |> put_session_registration_id(nil)
486 |> create_authorization(
487 params,
488 user: user
489 )
490 else
491 {:error, changeset} ->
492 message =
493 Enum.map(changeset.errors, fn {field, {error, _}} ->
494 "#{field} #{error}"
495 end)
496 |> Enum.join("; ")
497
498 message =
499 String.replace(
500 message,
501 "ap_id has already been taken",
502 "nickname has already been taken"
503 )
504
505 conn
506 |> put_status(:forbidden)
507 |> put_flash(:error, "Error: #{message}.")
508 |> registration_details(params)
509
510 _ ->
511 {:register, :generic_error}
512 end
513 end
514
515 defp do_create_authorization(
516 %Plug.Conn{} = conn,
517 %{
518 "authorization" =>
519 %{
520 "client_id" => client_id,
521 "redirect_uri" => redirect_uri
522 } = auth_attrs
523 },
524 user \\ nil
525 ) do
526 with {_, {:ok, %User{} = user}} <-
527 {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
528 %App{} = app <- Repo.get_by(App, client_id: client_id),
529 true <- redirect_uri in String.split(app.redirect_uris),
530 {:ok, scopes} <- validate_scopes(app, auth_attrs),
531 {:account_status, :active} <- {:account_status, User.account_status(user)},
532 {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
533 {:ok, auth, user}
534 end
535 end
536
537 # Special case: Local MastodonFE
538 defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
539
540 defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
541
542 defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
543
544 defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
545 do: put_session(conn, :registration_id, registration_id)
546
547 defp build_and_response_mfa_token(user, auth) do
548 with {:ok, token} <- MFA.Token.create_token(user, auth) do
549 MFAView.render("mfa_response.json", %{token: token, user: user})
550 end
551 end
552
553 @spec validate_scopes(App.t(), map()) ::
554 {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
555 defp validate_scopes(%App{} = app, params) do
556 params
557 |> Scopes.fetch_scopes(app.scopes)
558 |> Scopes.validate(app.scopes)
559 end
560
561 def default_redirect_uri(%App{} = app) do
562 app.redirect_uris
563 |> String.split()
564 |> Enum.at(0)
565 end
566
567 defp render_invalid_credentials_error(conn) do
568 render_error(conn, :bad_request, "Invalid credentials")
569 end
570 end