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{} = oauth_token} <- RevokeToken.revoke(app, params) do
368 with session_token = get_session(conn, :oauth_token),
369 %Token{token: ^session_token} <- oauth_token do
370 delete_session(conn, :oauth_token)
378 # RFC 7009: invalid tokens [in the request] do not cause an error response
383 def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
385 # Response for bad request
386 defp bad_request(%Plug.Conn{} = conn, _) do
387 render_error(conn, :internal_server_error, "Bad request")
390 @doc "Prepares OAuth request to provider for Ueberauth"
391 def prepare_request(%Plug.Conn{} = conn, %{
392 "provider" => provider,
393 "authorization" => auth_attrs
397 |> Scopes.fetch_scopes([])
398 |> Scopes.to_string()
402 |> Map.delete("scopes")
403 |> Map.put("scope", scope)
408 |> Map.drop(~w(scope scopes client_id redirect_uri))
409 |> Map.put("state", state)
411 # Handing the request to Ueberauth
412 redirect(conn, to: o_auth_path(conn, :request, provider, params))
415 def request(%Plug.Conn{} = conn, params) do
417 if params["provider"] do
418 dgettext("errors", "Unsupported OAuth provider: %{provider}.",
419 provider: params["provider"]
422 dgettext("errors", "Bad OAuth request.")
426 |> put_flash(:error, message)
430 def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
431 params = callback_params(params)
432 messages = for e <- Map.get(failure, :errors, []), do: e.message
433 message = Enum.join(messages, "; ")
438 dgettext("errors", "Failed to authenticate: %{message}.", message: message)
440 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
443 def callback(%Plug.Conn{} = conn, params) do
444 params = callback_params(params)
446 with {:ok, registration} <- Authenticator.get_registration(conn) do
447 auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
449 case Repo.get_assoc(registration, :user) do
451 create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
454 registration_params =
455 Map.merge(auth_attrs, %{
456 "nickname" => Registration.nickname(registration),
457 "email" => Registration.email(registration)
461 |> put_session_registration_id(registration.id)
462 |> registration_details(%{"authorization" => registration_params})
466 Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
469 |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
470 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
474 defp callback_params(%{"state" => state} = params) do
475 Map.merge(params, Jason.decode!(state))
478 def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
479 render(conn, "register.html", %{
480 client_id: auth_attrs["client_id"],
481 redirect_uri: auth_attrs["redirect_uri"],
482 state: auth_attrs["state"],
483 scopes: Scopes.fetch_scopes(auth_attrs, []),
484 nickname: auth_attrs["nickname"],
485 email: auth_attrs["email"]
489 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
490 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
491 %Registration{} = registration <- Repo.get(Registration, registration_id),
492 {_, {:ok, auth, _user}} <-
493 {:create_authorization, do_create_authorization(conn, params)},
494 %User{} = user <- Repo.preload(auth, :user).user,
495 {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
497 |> put_session_registration_id(nil)
498 |> after_create_authorization(auth, params)
500 {:create_authorization, error} ->
501 {:register, handle_create_authorization_error(conn, error, params)}
504 {:register, :generic_error}
508 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
509 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
510 %Registration{} = registration <- Repo.get(Registration, registration_id),
511 {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
513 |> put_session_registration_id(nil)
514 |> create_authorization(
519 {:error, changeset} ->
521 Enum.map(changeset.errors, fn {field, {error, _}} ->
529 "ap_id has already been taken",
530 "nickname has already been taken"
534 |> put_status(:forbidden)
535 |> put_flash(:error, "Error: #{message}.")
536 |> registration_details(params)
539 {:register, :generic_error}
543 defp do_create_authorization(conn, auth_attrs, user \\ nil)
545 defp do_create_authorization(
550 "client_id" => client_id,
551 "redirect_uri" => redirect_uri
556 with {_, {:ok, %User{} = user}} <-
557 {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
558 %App{} = app <- Repo.get_by(App, client_id: client_id),
559 true <- redirect_uri in String.split(app.redirect_uris),
560 requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes),
561 {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do
566 defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
567 when is_list(requested_scopes) do
568 with {:account_status, :active} <- {:account_status, User.account_status(user)},
569 {:ok, scopes} <- validate_scopes(app, requested_scopes),
570 {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
575 # Note: intended to be a private function but opened for AccountController that logs in on signup
576 @doc "If checks pass, creates authorization and token for given user, app and requested scopes."
577 def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do
578 with {:ok, auth} <- do_create_authorization(user, app, requested_scopes),
579 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
580 {:ok, token} <- Token.exchange_token(app, auth) do
585 # Special case: Local MastodonFE
586 defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
588 defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
590 defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
592 defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
593 do: put_session(conn, :registration_id, registration_id)
595 defp build_and_response_mfa_token(user, auth) do
596 with {:ok, token} <- MFA.Token.create(user, auth) do
597 MFAView.render("mfa_response.json", %{token: token, user: user})
601 @spec validate_scopes(App.t(), map() | list()) ::
602 {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
603 defp validate_scopes(%App{} = app, params) when is_map(params) do
604 requested_scopes = Scopes.fetch_scopes(params, app.scopes)
605 validate_scopes(app, requested_scopes)
608 defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
609 Scopes.validate(requested_scopes, app.scopes)
612 def default_redirect_uri(%App{} = app) do
618 defp render_invalid_credentials_error(conn) do
619 render_error(conn, :bad_request, "Invalid credentials")