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.Registration
14 alias Pleroma.Web.Auth.Authenticator
15 alias Pleroma.Web.ControllerHelper
16 alias Pleroma.Web.OAuth.App
17 alias Pleroma.Web.OAuth.Authorization
18 alias Pleroma.Web.OAuth.MFAController
19 alias Pleroma.Web.OAuth.MFAView
20 alias Pleroma.Web.OAuth.OAuthView
21 alias Pleroma.Web.OAuth.Scopes
22 alias Pleroma.Web.OAuth.Token
23 alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
24 alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
25 alias Pleroma.Web.Plugs.RateLimiter
29 if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
35 Pleroma.Web.Plugs.OAuthScopesPlug,
36 Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
39 plug(RateLimiter, [name: :authentication] when action == :create_authorization)
41 action_fallback(Pleroma.Web.OAuth.FallbackController)
43 @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"
45 # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg
46 def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do
47 {auth_attrs, params} = Map.pop(params, "authorization")
48 authorize(conn, Map.merge(params, auth_attrs))
51 def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do
52 if ControllerHelper.truthy_param?(params["force_login"]) do
53 do_authorize(conn, params)
55 handle_existing_authorization(conn, params)
59 # Note: the token is set in oauth_plug, but the token and client do not always go together.
60 # For example, MastodonFE's token is set if user requests with another client,
61 # after user already authorized to MastodonFE.
62 # So we have to check client and token.
64 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
65 %{"client_id" => client_id} = params
67 with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app),
68 ^client_id <- t.app.client_id do
69 handle_existing_authorization(conn, params)
71 _ -> do_authorize(conn, params)
75 def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
77 defp do_authorize(%Plug.Conn{} = conn, params) do
78 app = Repo.get_by(App, client_id: params["client_id"])
79 available_scopes = (app && app.scopes) || []
80 scopes = Scopes.fetch_scopes(params, available_scopes)
89 # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
90 render(conn, Authenticator.auth_template(), %{
91 response_type: params["response_type"],
92 client_id: params["client_id"],
93 available_scopes: available_scopes,
95 redirect_uri: params["redirect_uri"],
96 state: params["state"],
101 defp handle_existing_authorization(
102 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
103 %{"redirect_uri" => @oob_token_redirect_uri}
105 render(conn, "oob_token_exists.html", %{token: token})
108 defp handle_existing_authorization(
109 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
112 app = Repo.preload(token, :app).app
115 if is_binary(params["redirect_uri"]) do
116 params["redirect_uri"]
118 default_redirect_uri(app)
121 if redirect_uri in String.split(app.redirect_uris) do
122 redirect_uri = redirect_uri(conn, redirect_uri)
123 url_params = %{access_token: token.token}
124 url_params = Maps.put_if_present(url_params, :state, params["state"])
125 url = UriHelper.modify_uri_params(redirect_uri, url_params)
126 redirect(conn, external: url)
129 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
130 |> redirect(external: redirect_uri(conn, redirect_uri))
134 def create_authorization(
136 %{"authorization" => _} = params,
139 with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
140 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
141 after_create_authorization(conn, auth, params)
144 handle_create_authorization_error(conn, error, params)
148 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
149 "authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
151 # Enforcing the view to reuse the template when calling from other controllers
153 |> put_view(OAuthView)
154 |> render("oob_authorization_created.html", %{auth: auth})
157 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
158 "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
160 app = Repo.preload(auth, :app).app
162 # An extra safety measure before we redirect (also done in `do_create_authorization/2`)
163 if redirect_uri in String.split(app.redirect_uris) do
164 redirect_uri = redirect_uri(conn, redirect_uri)
165 url_params = %{code: auth.token}
166 url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
167 url = UriHelper.modify_uri_params(redirect_uri, url_params)
168 redirect(conn, external: url)
171 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
172 |> redirect(external: redirect_uri(conn, redirect_uri))
176 defp handle_create_authorization_error(
178 {:error, scopes_issue},
179 %{"authorization" => _} = params
181 when scopes_issue in [:unsupported_scopes, :missing_scopes] do
182 # Per https://github.com/tootsuite/mastodon/blob/
183 # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
185 |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
186 |> put_status(:unauthorized)
190 defp handle_create_authorization_error(
192 {:account_status, :confirmation_pending},
193 %{"authorization" => _} = params
196 |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
197 |> put_status(:forbidden)
201 defp handle_create_authorization_error(
203 {:mfa_required, user, auth, _},
206 {:ok, token} = MFA.Token.create(user, auth)
209 "mfa_token" => token.token,
210 "redirect_uri" => params["authorization"]["redirect_uri"],
211 "state" => params["authorization"]["state"]
214 MFAController.show(conn, data)
217 defp handle_create_authorization_error(
219 {:account_status, :password_reset_pending},
220 %{"authorization" => _} = params
223 |> put_flash(:error, dgettext("errors", "Password reset is required"))
224 |> put_status(:forbidden)
228 defp handle_create_authorization_error(
230 {:account_status, :deactivated},
231 %{"authorization" => _} = params
234 |> put_flash(:error, dgettext("errors", "Your account is currently disabled"))
235 |> put_status(:forbidden)
239 defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
240 Authenticator.handle_error(conn, error)
243 @doc "Renew access_token with refresh_token"
246 %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
248 with {:ok, app} <- Token.Utils.fetch_app(conn),
249 {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
250 {:ok, token} <- RefreshToken.grant(token) do
251 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
253 _error -> render_invalid_credentials_error(conn)
257 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do
258 with {:ok, app} <- Token.Utils.fetch_app(conn),
259 fixed_token = Token.Utils.fix_padding(params["code"]),
260 {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
261 %User{} = user <- User.get_cached_by_id(auth.user_id),
262 {:ok, token} <- Token.exchange_token(app, auth) do
263 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
266 handle_token_exchange_error(conn, error)
272 %{"grant_type" => "password"} = params
274 with {:ok, %User{} = user} <- Authenticator.get_user(conn),
275 {:ok, app} <- Token.Utils.fetch_app(conn),
276 requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
277 {:ok, token} <- login(user, app, requested_scopes) do
278 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
281 handle_token_exchange_error(conn, error)
287 %{"grant_type" => "password", "name" => name, "password" => _password} = params
291 |> Map.delete("name")
292 |> Map.put("username", name)
294 token_exchange(conn, params)
297 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
298 with {:ok, app} <- Token.Utils.fetch_app(conn),
299 {:ok, auth} <- Authorization.create_authorization(app, %User{}),
300 {:ok, token} <- Token.exchange_token(app, auth) do
301 json(conn, OAuthView.render("token.json", %{token: token}))
304 handle_token_exchange_error(conn, :invalid_credentails)
309 def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
311 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
313 |> put_status(:forbidden)
314 |> json(build_and_response_mfa_token(user, auth))
317 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
321 "Your account is currently disabled",
323 "account_is_disabled"
327 defp handle_token_exchange_error(
329 {:account_status, :password_reset_pending}
334 "Password reset is required",
336 "password_reset_required"
340 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do
344 "Your login is missing a confirmed e-mail address",
346 "missing_confirmed_email"
350 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
354 "Your account is awaiting approval.",
360 defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
361 render_invalid_credentials_error(conn)
364 def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
365 with {:ok, app} <- Token.Utils.fetch_app(conn),
366 {:ok, _token} <- RevokeToken.revoke(app, params) do
370 # RFC 7009: invalid tokens [in the request] do not cause an error response
375 def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
377 # Response for bad request
378 defp bad_request(%Plug.Conn{} = conn, _) do
379 render_error(conn, :internal_server_error, "Bad request")
382 @doc "Prepares OAuth request to provider for Ueberauth"
383 def prepare_request(%Plug.Conn{} = conn, %{
384 "provider" => provider,
385 "authorization" => auth_attrs
389 |> Scopes.fetch_scopes([])
390 |> Scopes.to_string()
394 |> Map.delete("scopes")
395 |> Map.put("scope", scope)
400 |> Map.drop(~w(scope scopes client_id redirect_uri))
401 |> Map.put("state", state)
403 # Handing the request to Ueberauth
404 redirect(conn, to: o_auth_path(conn, :request, provider, params))
407 def request(%Plug.Conn{} = conn, params) do
409 if params["provider"] do
410 dgettext("errors", "Unsupported OAuth provider: %{provider}.",
411 provider: params["provider"]
414 dgettext("errors", "Bad OAuth request.")
418 |> put_flash(:error, message)
422 def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
423 params = callback_params(params)
424 messages = for e <- Map.get(failure, :errors, []), do: e.message
425 message = Enum.join(messages, "; ")
430 dgettext("errors", "Failed to authenticate: %{message}.", message: message)
432 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
435 def callback(%Plug.Conn{} = conn, params) do
436 params = callback_params(params)
438 with {:ok, registration} <- Authenticator.get_registration(conn) do
439 auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
441 case Repo.get_assoc(registration, :user) do
443 create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
446 registration_params =
447 Map.merge(auth_attrs, %{
448 "nickname" => Registration.nickname(registration),
449 "email" => Registration.email(registration)
453 |> put_session_registration_id(registration.id)
454 |> registration_details(%{"authorization" => registration_params})
458 Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
461 |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
462 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
466 defp callback_params(%{"state" => state} = params) do
467 Map.merge(params, Jason.decode!(state))
470 def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
471 render(conn, "register.html", %{
472 client_id: auth_attrs["client_id"],
473 redirect_uri: auth_attrs["redirect_uri"],
474 state: auth_attrs["state"],
475 scopes: Scopes.fetch_scopes(auth_attrs, []),
476 nickname: auth_attrs["nickname"],
477 email: auth_attrs["email"]
481 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
482 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
483 %Registration{} = registration <- Repo.get(Registration, registration_id),
484 {_, {:ok, auth, _user}} <-
485 {:create_authorization, do_create_authorization(conn, params)},
486 %User{} = user <- Repo.preload(auth, :user).user,
487 {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
489 |> put_session_registration_id(nil)
490 |> after_create_authorization(auth, params)
492 {:create_authorization, error} ->
493 {:register, handle_create_authorization_error(conn, error, params)}
496 {:register, :generic_error}
500 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
501 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
502 %Registration{} = registration <- Repo.get(Registration, registration_id),
503 {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
505 |> put_session_registration_id(nil)
506 |> create_authorization(
511 {:error, changeset} ->
513 Enum.map(changeset.errors, fn {field, {error, _}} ->
521 "ap_id has already been taken",
522 "nickname has already been taken"
526 |> put_status(:forbidden)
527 |> put_flash(:error, "Error: #{message}.")
528 |> registration_details(params)
531 {:register, :generic_error}
535 defp do_create_authorization(conn, auth_attrs, user \\ nil)
537 defp do_create_authorization(
542 "client_id" => client_id,
543 "redirect_uri" => redirect_uri
548 with {_, {:ok, %User{} = user}} <-
549 {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
550 %App{} = app <- Repo.get_by(App, client_id: client_id),
551 true <- redirect_uri in String.split(app.redirect_uris),
552 requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes),
553 {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do
558 defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
559 when is_list(requested_scopes) do
560 with {:account_status, :active} <- {:account_status, User.account_status(user)},
561 {:ok, scopes} <- validate_scopes(app, requested_scopes),
562 {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
567 # Note: intended to be a private function but opened for AccountController that logs in on signup
568 @doc "If checks pass, creates authorization and token for given user, app and requested scopes."
569 def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do
570 with {:ok, auth} <- do_create_authorization(user, app, requested_scopes),
571 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
572 {:ok, token} <- Token.exchange_token(app, auth) do
577 # Special case: Local MastodonFE
578 defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
580 defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
582 defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
584 defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
585 do: put_session(conn, :registration_id, registration_id)
587 defp build_and_response_mfa_token(user, auth) do
588 with {:ok, token} <- MFA.Token.create(user, auth) do
589 MFAView.render("mfa_response.json", %{token: token, user: user})
593 @spec validate_scopes(App.t(), map() | list()) ::
594 {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
595 defp validate_scopes(%App{} = app, params) when is_map(params) do
596 requested_scopes = Scopes.fetch_scopes(params, app.scopes)
597 validate_scopes(app, requested_scopes)
600 defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
601 Scopes.validate(requested_scopes, app.scopes)
604 def default_redirect_uri(%App{} = app) do
610 defp render_invalid_credentials_error(conn) do
611 render_error(conn, :bad_request, "Invalid credentials")