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 {:account_status, :active} <- {:account_status, User.account_status(user)},
264 {:ok, scopes} <- validate_scopes(app, params),
265 {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
266 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
267 {:ok, token} <- Token.exchange_token(app, auth) do
268 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
271 handle_token_exchange_error(conn, error)
277 %{"grant_type" => "password", "name" => name, "password" => _password} = params
281 |> Map.delete("name")
282 |> Map.put("username", name)
284 token_exchange(conn, params)
287 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
288 with {:ok, app} <- Token.Utils.fetch_app(conn),
289 {:ok, auth} <- Authorization.create_authorization(app, %User{}),
290 {:ok, token} <- Token.exchange_token(app, auth) do
291 json(conn, OAuthView.render("token.json", %{token: token}))
294 handle_token_exchange_error(conn, :invalid_credentails)
299 def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
301 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
303 |> put_status(:forbidden)
304 |> json(build_and_response_mfa_token(user, auth))
307 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
311 "Your account is currently disabled",
313 "account_is_disabled"
317 defp handle_token_exchange_error(
319 {:account_status, :password_reset_pending}
324 "Password reset is required",
326 "password_reset_required"
330 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do
334 "Your login is missing a confirmed e-mail address",
336 "missing_confirmed_email"
340 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
344 "Your account is awaiting approval.",
350 defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
351 render_invalid_credentials_error(conn)
354 def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
355 with {:ok, app} <- Token.Utils.fetch_app(conn),
356 {:ok, _token} <- RevokeToken.revoke(app, params) do
360 # RFC 7009: invalid tokens [in the request] do not cause an error response
365 def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
367 # Response for bad request
368 defp bad_request(%Plug.Conn{} = conn, _) do
369 render_error(conn, :internal_server_error, "Bad request")
372 @doc "Prepares OAuth request to provider for Ueberauth"
373 def prepare_request(%Plug.Conn{} = conn, %{
374 "provider" => provider,
375 "authorization" => auth_attrs
379 |> Scopes.fetch_scopes([])
380 |> Scopes.to_string()
384 |> Map.delete("scopes")
385 |> Map.put("scope", scope)
390 |> Map.drop(~w(scope scopes client_id redirect_uri))
391 |> Map.put("state", state)
393 # Handing the request to Ueberauth
394 redirect(conn, to: o_auth_path(conn, :request, provider, params))
397 def request(%Plug.Conn{} = conn, params) do
399 if params["provider"] do
400 dgettext("errors", "Unsupported OAuth provider: %{provider}.",
401 provider: params["provider"]
404 dgettext("errors", "Bad OAuth request.")
408 |> put_flash(:error, message)
412 def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
413 params = callback_params(params)
414 messages = for e <- Map.get(failure, :errors, []), do: e.message
415 message = Enum.join(messages, "; ")
420 dgettext("errors", "Failed to authenticate: %{message}.", message: message)
422 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
425 def callback(%Plug.Conn{} = conn, params) do
426 params = callback_params(params)
428 with {:ok, registration} <- Authenticator.get_registration(conn) do
429 auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
431 case Repo.get_assoc(registration, :user) do
433 create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
436 registration_params =
437 Map.merge(auth_attrs, %{
438 "nickname" => Registration.nickname(registration),
439 "email" => Registration.email(registration)
443 |> put_session_registration_id(registration.id)
444 |> registration_details(%{"authorization" => registration_params})
448 Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
451 |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
452 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
456 defp callback_params(%{"state" => state} = params) do
457 Map.merge(params, Jason.decode!(state))
460 def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
461 render(conn, "register.html", %{
462 client_id: auth_attrs["client_id"],
463 redirect_uri: auth_attrs["redirect_uri"],
464 state: auth_attrs["state"],
465 scopes: Scopes.fetch_scopes(auth_attrs, []),
466 nickname: auth_attrs["nickname"],
467 email: auth_attrs["email"]
471 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
472 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
473 %Registration{} = registration <- Repo.get(Registration, registration_id),
474 {_, {:ok, auth, _user}} <-
475 {:create_authorization, do_create_authorization(conn, params)},
476 %User{} = user <- Repo.preload(auth, :user).user,
477 {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
479 |> put_session_registration_id(nil)
480 |> after_create_authorization(auth, params)
482 {:create_authorization, error} ->
483 {:register, handle_create_authorization_error(conn, error, params)}
486 {:register, :generic_error}
490 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
491 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
492 %Registration{} = registration <- Repo.get(Registration, registration_id),
493 {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
495 |> put_session_registration_id(nil)
496 |> create_authorization(
501 {:error, changeset} ->
503 Enum.map(changeset.errors, fn {field, {error, _}} ->
511 "ap_id has already been taken",
512 "nickname has already been taken"
516 |> put_status(:forbidden)
517 |> put_flash(:error, "Error: #{message}.")
518 |> registration_details(params)
521 {:register, :generic_error}
525 defp do_create_authorization(
530 "client_id" => client_id,
531 "redirect_uri" => redirect_uri
536 with {_, {:ok, %User{} = user}} <-
537 {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
538 %App{} = app <- Repo.get_by(App, client_id: client_id),
539 true <- redirect_uri in String.split(app.redirect_uris),
540 {:ok, scopes} <- validate_scopes(app, auth_attrs),
541 {:account_status, :active} <- {:account_status, User.account_status(user)},
542 {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
547 # Special case: Local MastodonFE
548 defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
550 defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
552 defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
554 defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
555 do: put_session(conn, :registration_id, registration_id)
557 defp build_and_response_mfa_token(user, auth) do
558 with {:ok, token} <- MFA.Token.create_token(user, auth) do
559 MFAView.render("mfa_response.json", %{token: token, user: user})
563 @spec validate_scopes(App.t(), map()) ::
564 {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
565 defp validate_scopes(%App{} = app, params) do
567 |> Scopes.fetch_scopes(app.scopes)
568 |> Scopes.validate(app.scopes)
571 def default_redirect_uri(%App{} = app) do
577 defp render_invalid_credentials_error(conn) do
578 render_error(conn, :bad_request, "Invalid credentials")