1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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.UriHelper
11 alias Pleroma.Plugs.RateLimiter
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
29 if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
34 plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug])
36 plug(RateLimiter, [name: :authentication] when action == :create_authorization)
38 action_fallback(Pleroma.Web.OAuth.FallbackController)
40 @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"
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))
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)
52 handle_existing_authorization(conn, params)
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.
61 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
62 %{"client_id" => client_id} = params
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)
68 _ -> do_authorize(conn, params)
72 def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
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)
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,
85 redirect_uri: params["redirect_uri"],
86 state: params["state"],
91 defp handle_existing_authorization(
92 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
93 %{"redirect_uri" => @oob_token_redirect_uri}
95 render(conn, "oob_token_exists.html", %{token: token})
98 defp handle_existing_authorization(
99 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
102 app = Repo.preload(token, :app).app
105 if is_binary(params["redirect_uri"]) do
106 params["redirect_uri"]
108 default_redirect_uri(app)
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)
119 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
120 |> redirect(external: redirect_uri(conn, redirect_uri))
124 def create_authorization(
126 %{"authorization" => _} = params,
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)
134 handle_create_authorization_error(conn, error, params)
138 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
139 "authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
141 render(conn, "oob_authorization_created.html", %{auth: auth})
144 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
145 "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
147 app = Repo.preload(auth, :app).app
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)
158 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
159 |> redirect(external: redirect_uri(conn, redirect_uri))
163 defp handle_create_authorization_error(
165 {:error, scopes_issue},
166 %{"authorization" => _} = params
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
172 |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
173 |> put_status(:unauthorized)
177 defp handle_create_authorization_error(
179 {:account_status, :confirmation_pending},
180 %{"authorization" => _} = params
183 |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
184 |> put_status(:forbidden)
188 defp handle_create_authorization_error(
190 {:mfa_required, user, auth, _},
193 {:ok, token} = MFA.Token.create_token(user, auth)
196 "mfa_token" => token.token,
197 "redirect_uri" => params["authorization"]["redirect_uri"],
198 "state" => params["authorization"]["state"]
201 MFAController.show(conn, data)
204 defp handle_create_authorization_error(
206 {:account_status, :password_reset_pending},
207 %{"authorization" => _} = params
210 |> put_flash(:error, dgettext("errors", "Password reset is required"))
211 |> put_status(:forbidden)
215 defp handle_create_authorization_error(
217 {:account_status, :deactivated},
218 %{"authorization" => _} = params
221 |> put_flash(:error, dgettext("errors", "Your account is currently disabled"))
222 |> put_status(:forbidden)
226 defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
227 Authenticator.handle_error(conn, error)
230 @doc "Renew access_token with refresh_token"
233 %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
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}))
240 _error -> render_invalid_credentials_error(conn)
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}))
253 handle_token_exchange_error(conn, error)
259 %{"grant_type" => "password"} = params
261 with {:ok, %User{} = user} <- Authenticator.get_user(conn),
262 {:ok, app} <- Token.Utils.fetch_app(conn),
263 requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
264 {:ok, token} <- login(user, app, requested_scopes) do
265 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
268 handle_token_exchange_error(conn, error)
274 %{"grant_type" => "password", "name" => name, "password" => _password} = params
278 |> Map.delete("name")
279 |> Map.put("username", name)
281 token_exchange(conn, params)
284 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
285 with {:ok, app} <- Token.Utils.fetch_app(conn),
286 {:ok, auth} <- Authorization.create_authorization(app, %User{}),
287 {:ok, token} <- Token.exchange_token(app, auth) do
288 json(conn, OAuthView.render("token.json", %{token: token}))
291 handle_token_exchange_error(conn, :invalid_credentails)
296 def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
298 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
300 |> put_status(:forbidden)
301 |> json(build_and_response_mfa_token(user, auth))
304 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
308 "Your account is currently disabled",
310 "account_is_disabled"
314 defp handle_token_exchange_error(
316 {:account_status, :password_reset_pending}
321 "Password reset is required",
323 "password_reset_required"
327 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do
331 "Your login is missing a confirmed e-mail address",
333 "missing_confirmed_email"
337 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
341 "Your account is awaiting approval.",
347 defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
348 render_invalid_credentials_error(conn)
351 def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
352 with {:ok, app} <- Token.Utils.fetch_app(conn),
353 {:ok, _token} <- RevokeToken.revoke(app, params) do
357 # RFC 7009: invalid tokens [in the request] do not cause an error response
362 def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
364 # Response for bad request
365 defp bad_request(%Plug.Conn{} = conn, _) do
366 render_error(conn, :internal_server_error, "Bad request")
369 @doc "Prepares OAuth request to provider for Ueberauth"
370 def prepare_request(%Plug.Conn{} = conn, %{
371 "provider" => provider,
372 "authorization" => auth_attrs
376 |> Scopes.fetch_scopes([])
377 |> Scopes.to_string()
381 |> Map.delete("scopes")
382 |> Map.put("scope", scope)
387 |> Map.drop(~w(scope scopes client_id redirect_uri))
388 |> Map.put("state", state)
390 # Handing the request to Ueberauth
391 redirect(conn, to: o_auth_path(conn, :request, provider, params))
394 def request(%Plug.Conn{} = conn, params) do
396 if params["provider"] do
397 dgettext("errors", "Unsupported OAuth provider: %{provider}.",
398 provider: params["provider"]
401 dgettext("errors", "Bad OAuth request.")
405 |> put_flash(:error, message)
409 def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
410 params = callback_params(params)
411 messages = for e <- Map.get(failure, :errors, []), do: e.message
412 message = Enum.join(messages, "; ")
417 dgettext("errors", "Failed to authenticate: %{message}.", message: message)
419 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
422 def callback(%Plug.Conn{} = conn, params) do
423 params = callback_params(params)
425 with {:ok, registration} <- Authenticator.get_registration(conn) do
426 auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
428 case Repo.get_assoc(registration, :user) do
430 create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
433 registration_params =
434 Map.merge(auth_attrs, %{
435 "nickname" => Registration.nickname(registration),
436 "email" => Registration.email(registration)
440 |> put_session_registration_id(registration.id)
441 |> registration_details(%{"authorization" => registration_params})
445 Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
448 |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
449 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
453 defp callback_params(%{"state" => state} = params) do
454 Map.merge(params, Jason.decode!(state))
457 def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
458 render(conn, "register.html", %{
459 client_id: auth_attrs["client_id"],
460 redirect_uri: auth_attrs["redirect_uri"],
461 state: auth_attrs["state"],
462 scopes: Scopes.fetch_scopes(auth_attrs, []),
463 nickname: auth_attrs["nickname"],
464 email: auth_attrs["email"]
468 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
469 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
470 %Registration{} = registration <- Repo.get(Registration, registration_id),
471 {_, {:ok, auth, _user}} <-
472 {:create_authorization, do_create_authorization(conn, params)},
473 %User{} = user <- Repo.preload(auth, :user).user,
474 {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
476 |> put_session_registration_id(nil)
477 |> after_create_authorization(auth, params)
479 {:create_authorization, error} ->
480 {:register, handle_create_authorization_error(conn, error, params)}
483 {:register, :generic_error}
487 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
488 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
489 %Registration{} = registration <- Repo.get(Registration, registration_id),
490 {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
492 |> put_session_registration_id(nil)
493 |> create_authorization(
498 {:error, changeset} ->
500 Enum.map(changeset.errors, fn {field, {error, _}} ->
508 "ap_id has already been taken",
509 "nickname has already been taken"
513 |> put_status(:forbidden)
514 |> put_flash(:error, "Error: #{message}.")
515 |> registration_details(params)
518 {:register, :generic_error}
522 defp do_create_authorization(conn, auth_attrs, user \\ nil)
524 defp do_create_authorization(
529 "client_id" => client_id,
530 "redirect_uri" => redirect_uri
535 with {_, {:ok, %User{} = user}} <-
536 {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
537 %App{} = app <- Repo.get_by(App, client_id: client_id),
538 true <- redirect_uri in String.split(app.redirect_uris),
539 requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes),
540 {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do
545 defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
546 when is_list(requested_scopes) do
547 with {:account_status, :active} <- {:account_status, User.account_status(user)},
548 {:ok, scopes} <- validate_scopes(app, requested_scopes),
549 {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
554 # Note: intended to be a private function but opened for AccountController that logs in on signup
555 @doc "If checks pass, creates authorization and token for given user, app and requested scopes."
556 def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do
557 with {:ok, auth} <- do_create_authorization(user, app, requested_scopes),
558 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
559 {:ok, token} <- Token.exchange_token(app, auth) do
564 # Special case: Local MastodonFE
565 defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
567 defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
569 defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
571 defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
572 do: put_session(conn, :registration_id, registration_id)
574 defp build_and_response_mfa_token(user, auth) do
575 with {:ok, token} <- MFA.Token.create_token(user, auth) do
576 MFAView.render("mfa_response.json", %{token: token, user: user})
580 @spec validate_scopes(App.t(), map() | list()) ::
581 {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
582 defp validate_scopes(%App{} = app, params) when is_map(params) do
583 requested_scopes = Scopes.fetch_scopes(params, app.scopes)
584 validate_scopes(app, requested_scopes)
587 defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
588 Scopes.validate(requested_scopes, app.scopes)
591 def default_redirect_uri(%App{} = app) do
597 defp render_invalid_credentials_error(conn) do
598 render_error(conn, :bad_request, "Invalid credentials")