Merge remote-tracking branch 'upstream/develop' into by-approval
[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, {:account_status, :approval_pending}) do
341 render_error(
342 conn,
343 :forbidden,
344 "Your account is awaiting approval.",
345 %{},
346 "awaiting_approval"
347 )
348 end
349
350 defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
351 render_invalid_credentials_error(conn)
352 end
353
354 def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
355 with {:ok, app} <- Token.Utils.fetch_app(conn),
356 {:ok, _token} <- RevokeToken.revoke(app, params) do
357 json(conn, %{})
358 else
359 _error ->
360 # RFC 7009: invalid tokens [in the request] do not cause an error response
361 json(conn, %{})
362 end
363 end
364
365 def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
366
367 # Response for bad request
368 defp bad_request(%Plug.Conn{} = conn, _) do
369 render_error(conn, :internal_server_error, "Bad request")
370 end
371
372 @doc "Prepares OAuth request to provider for Ueberauth"
373 def prepare_request(%Plug.Conn{} = conn, %{
374 "provider" => provider,
375 "authorization" => auth_attrs
376 }) do
377 scope =
378 auth_attrs
379 |> Scopes.fetch_scopes([])
380 |> Scopes.to_string()
381
382 state =
383 auth_attrs
384 |> Map.delete("scopes")
385 |> Map.put("scope", scope)
386 |> Jason.encode!()
387
388 params =
389 auth_attrs
390 |> Map.drop(~w(scope scopes client_id redirect_uri))
391 |> Map.put("state", state)
392
393 # Handing the request to Ueberauth
394 redirect(conn, to: o_auth_path(conn, :request, provider, params))
395 end
396
397 def request(%Plug.Conn{} = conn, params) do
398 message =
399 if params["provider"] do
400 dgettext("errors", "Unsupported OAuth provider: %{provider}.",
401 provider: params["provider"]
402 )
403 else
404 dgettext("errors", "Bad OAuth request.")
405 end
406
407 conn
408 |> put_flash(:error, message)
409 |> redirect(to: "/")
410 end
411
412 def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
413 params = callback_params(params)
414 messages = for e <- Map.get(failure, :errors, []), do: e.message
415 message = Enum.join(messages, "; ")
416
417 conn
418 |> put_flash(
419 :error,
420 dgettext("errors", "Failed to authenticate: %{message}.", message: message)
421 )
422 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
423 end
424
425 def callback(%Plug.Conn{} = conn, params) do
426 params = callback_params(params)
427
428 with {:ok, registration} <- Authenticator.get_registration(conn) do
429 auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
430
431 case Repo.get_assoc(registration, :user) do
432 {:ok, user} ->
433 create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
434
435 _ ->
436 registration_params =
437 Map.merge(auth_attrs, %{
438 "nickname" => Registration.nickname(registration),
439 "email" => Registration.email(registration)
440 })
441
442 conn
443 |> put_session_registration_id(registration.id)
444 |> registration_details(%{"authorization" => registration_params})
445 end
446 else
447 error ->
448 Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
449
450 conn
451 |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
452 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
453 end
454 end
455
456 defp callback_params(%{"state" => state} = params) do
457 Map.merge(params, Jason.decode!(state))
458 end
459
460 def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
461 render(conn, "register.html", %{
462 client_id: auth_attrs["client_id"],
463 redirect_uri: auth_attrs["redirect_uri"],
464 state: auth_attrs["state"],
465 scopes: Scopes.fetch_scopes(auth_attrs, []),
466 nickname: auth_attrs["nickname"],
467 email: auth_attrs["email"]
468 })
469 end
470
471 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
472 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
473 %Registration{} = registration <- Repo.get(Registration, registration_id),
474 {_, {:ok, auth, _user}} <-
475 {:create_authorization, do_create_authorization(conn, params)},
476 %User{} = user <- Repo.preload(auth, :user).user,
477 {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
478 conn
479 |> put_session_registration_id(nil)
480 |> after_create_authorization(auth, params)
481 else
482 {:create_authorization, error} ->
483 {:register, handle_create_authorization_error(conn, error, params)}
484
485 _ ->
486 {:register, :generic_error}
487 end
488 end
489
490 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
491 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
492 %Registration{} = registration <- Repo.get(Registration, registration_id),
493 {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
494 conn
495 |> put_session_registration_id(nil)
496 |> create_authorization(
497 params,
498 user: user
499 )
500 else
501 {:error, changeset} ->
502 message =
503 Enum.map(changeset.errors, fn {field, {error, _}} ->
504 "#{field} #{error}"
505 end)
506 |> Enum.join("; ")
507
508 message =
509 String.replace(
510 message,
511 "ap_id has already been taken",
512 "nickname has already been taken"
513 )
514
515 conn
516 |> put_status(:forbidden)
517 |> put_flash(:error, "Error: #{message}.")
518 |> registration_details(params)
519
520 _ ->
521 {:register, :generic_error}
522 end
523 end
524
525 defp do_create_authorization(
526 %Plug.Conn{} = conn,
527 %{
528 "authorization" =>
529 %{
530 "client_id" => client_id,
531 "redirect_uri" => redirect_uri
532 } = auth_attrs
533 },
534 user \\ nil
535 ) do
536 with {_, {:ok, %User{} = user}} <-
537 {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
538 %App{} = app <- Repo.get_by(App, client_id: client_id),
539 true <- redirect_uri in String.split(app.redirect_uris),
540 {:ok, scopes} <- validate_scopes(app, auth_attrs),
541 {:account_status, :active} <- {:account_status, User.account_status(user)},
542 {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
543 {:ok, auth, user}
544 end
545 end
546
547 # Special case: Local MastodonFE
548 defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
549
550 defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
551
552 defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
553
554 defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
555 do: put_session(conn, :registration_id, registration_id)
556
557 defp build_and_response_mfa_token(user, auth) do
558 with {:ok, token} <- MFA.Token.create_token(user, auth) do
559 MFAView.render("mfa_response.json", %{token: token, user: user})
560 end
561 end
562
563 @spec validate_scopes(App.t(), map()) ::
564 {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
565 defp validate_scopes(%App{} = app, params) do
566 params
567 |> Scopes.fetch_scopes(app.scopes)
568 |> Scopes.validate(app.scopes)
569 end
570
571 def default_redirect_uri(%App{} = app) do
572 app.redirect_uris
573 |> String.split()
574 |> Enum.at(0)
575 end
576
577 defp render_invalid_credentials_error(conn) do
578 render_error(conn, :bad_request, "Invalid credentials")
579 end
580 end