1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.OAuth.OAuthController do
6 use Pleroma.Web, :controller
8 alias Pleroma.Helpers.AuthHelper
9 alias Pleroma.Helpers.UriHelper
12 alias Pleroma.Registration
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
30 if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
36 Pleroma.Web.Plugs.OAuthScopesPlug,
37 Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
40 plug(RateLimiter, [name: :authentication] when action == :create_authorization)
42 action_fallback(Pleroma.Web.OAuth.FallbackController)
44 @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"
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))
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)
56 handle_existing_authorization(conn, params)
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.
65 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
66 %{"client_id" => client_id} = params
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)
72 _ -> do_authorize(conn, params)
76 def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
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)
84 with %{assigns: %{user: %User{} = user}} <- conn do
97 # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
98 render(conn, Authenticator.auth_template(), %{
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,
105 redirect_uri: params["redirect_uri"],
106 state: params["state"],
111 defp handle_existing_authorization(
112 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
113 %{"redirect_uri" => @oob_token_redirect_uri}
115 render(conn, "oob_token_exists.html", %{token: token})
118 defp handle_existing_authorization(
119 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
122 app = Repo.preload(token, :app).app
125 if is_binary(params["redirect_uri"]) do
126 params["redirect_uri"]
128 default_redirect_uri(app)
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)
139 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
140 |> redirect(external: redirect_uri(conn, redirect_uri))
144 def create_authorization(_, _, opts \\ [])
146 def create_authorization(%Plug.Conn{assigns: %{user: %User{} = user}} = conn, params, []) do
147 create_authorization(conn, params, user: user)
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)
156 handle_create_authorization_error(conn, error, params)
160 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
161 "authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
163 # Enforcing the view to reuse the template when calling from other controllers
165 |> put_view(OAuthView)
166 |> render("oob_authorization_created.html", %{auth: auth})
169 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
170 "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
172 app = Repo.preload(auth, :app).app
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)
183 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
184 |> redirect(external: redirect_uri(conn, redirect_uri))
188 defp handle_create_authorization_error(
190 {:error, scopes_issue},
191 %{"authorization" => _} = params
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
197 |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
198 |> put_status(:unauthorized)
202 defp handle_create_authorization_error(
204 {:account_status, :confirmation_pending},
205 %{"authorization" => _} = params
208 |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
209 |> put_status(:forbidden)
213 defp handle_create_authorization_error(
215 {:mfa_required, user, auth, _},
218 {:ok, token} = MFA.Token.create(user, auth)
221 "mfa_token" => token.token,
222 "redirect_uri" => params["authorization"]["redirect_uri"],
223 "state" => params["authorization"]["state"]
226 MFAController.show(conn, data)
229 defp handle_create_authorization_error(
231 {:account_status, :password_reset_pending},
232 %{"authorization" => _} = params
235 |> put_flash(:error, dgettext("errors", "Password reset is required"))
236 |> put_status(:forbidden)
240 defp handle_create_authorization_error(
242 {:account_status, :deactivated},
243 %{"authorization" => _} = params
246 |> put_flash(:error, dgettext("errors", "Your account is currently disabled"))
247 |> put_status(:forbidden)
251 defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
252 Authenticator.handle_error(conn, error)
255 @doc "Renew access_token with refresh_token"
258 %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
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})
265 _error -> render_invalid_credentials_error(conn)
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})
278 handle_token_exchange_error(conn, error)
284 %{"grant_type" => "password"} = params
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})
293 handle_token_exchange_error(conn, error)
299 %{"grant_type" => "password", "name" => name, "password" => _password} = params
303 |> Map.delete("name")
304 |> Map.put("username", name)
306 token_exchange(conn, params)
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})
316 handle_token_exchange_error(conn, :invalid_credentails)
321 def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
323 def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
325 |> AuthHelper.put_session_token(token.token)
326 |> json(OAuthView.render("token.json", view_params))
329 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
331 |> put_status(:forbidden)
332 |> json(build_and_response_mfa_token(user, auth))
335 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
339 "Your account is currently disabled",
341 "account_is_disabled"
345 defp handle_token_exchange_error(
347 {:account_status, :password_reset_pending}
352 "Password reset is required",
354 "password_reset_required"
358 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do
362 "Your login is missing a confirmed e-mail address",
364 "missing_confirmed_email"
368 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
372 "Your account is awaiting approval.",
378 defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
379 render_invalid_credentials_error(conn)
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
386 with session_token = AuthHelper.get_session_token(conn),
387 %Token{token: ^session_token} <- oauth_token do
388 AuthHelper.delete_session_token(conn)
396 # RFC 7009: invalid tokens [in the request] do not cause an error response
401 def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
403 # Response for bad request
404 defp bad_request(%Plug.Conn{} = conn, _) do
405 render_error(conn, :internal_server_error, "Bad request")
408 @doc "Prepares OAuth request to provider for Ueberauth"
409 def prepare_request(%Plug.Conn{} = conn, %{
410 "provider" => provider,
411 "authorization" => auth_attrs
415 |> Scopes.fetch_scopes([])
416 |> Scopes.to_string()
420 |> Map.delete("scopes")
421 |> Map.put("scope", scope)
426 |> Map.drop(~w(scope scopes client_id redirect_uri))
427 |> Map.put("state", state)
429 # Handing the request to Ueberauth
430 redirect(conn, to: o_auth_path(conn, :request, provider, params))
433 def request(%Plug.Conn{} = conn, params) do
435 if params["provider"] do
436 dgettext("errors", "Unsupported OAuth provider: %{provider}.",
437 provider: params["provider"]
440 dgettext("errors", "Bad OAuth request.")
444 |> put_flash(:error, message)
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, "; ")
456 dgettext("errors", "Failed to authenticate: %{message}.", message: message)
458 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
461 def callback(%Plug.Conn{} = conn, params) do
462 params = callback_params(params)
464 with {:ok, registration} <- Authenticator.get_registration(conn) do
465 auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
467 case Repo.get_assoc(registration, :user) do
469 create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
472 registration_params =
473 Map.merge(auth_attrs, %{
474 "nickname" => Registration.nickname(registration),
475 "email" => Registration.email(registration)
479 |> put_session_registration_id(registration.id)
480 |> registration_details(%{"authorization" => registration_params})
484 Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
487 |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
488 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
492 defp callback_params(%{"state" => state} = params) do
493 Map.merge(params, Jason.decode!(state))
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"]
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
515 |> put_session_registration_id(nil)
516 |> after_create_authorization(auth, params)
518 {:create_authorization, error} ->
519 {:register, handle_create_authorization_error(conn, error, params)}
522 {:register, :generic_error}
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
531 |> put_session_registration_id(nil)
532 |> create_authorization(
537 {:error, changeset} ->
539 Enum.map(changeset.errors, fn {field, {error, _}} ->
547 "ap_id has already been taken",
548 "nickname has already been taken"
552 |> put_status(:forbidden)
553 |> put_flash(:error, "Error: #{message}.")
554 |> registration_details(params)
557 {:register, :generic_error}
561 defp do_create_authorization(conn, auth_attrs, user \\ nil)
563 defp do_create_authorization(
568 "client_id" => client_id,
569 "redirect_uri" => redirect_uri
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
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
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
603 # Special case: Local MastodonFE
604 defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
606 defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
608 defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
610 defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
611 do: put_session(conn, :registration_id, registration_id)
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})
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)
626 defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
627 Scopes.validate(requested_scopes, app.scopes)
630 def default_redirect_uri(%App{} = app) do
636 defp render_invalid_credentials_error(conn) do
637 render_error(conn, :bad_request, "Invalid credentials")