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