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.OAuthView
21 alias Pleroma.Web.OAuth.MFAView
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, _error) do
341 render_invalid_credentials_error(conn)
344 def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
345 with {:ok, app} <- Token.Utils.fetch_app(conn),
346 {:ok, _token} <- RevokeToken.revoke(app, params) do
350 # RFC 7009: invalid tokens [in the request] do not cause an error response
355 def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
357 # Response for bad request
358 defp bad_request(%Plug.Conn{} = conn, _) do
359 render_error(conn, :internal_server_error, "Bad request")
362 @doc "Prepares OAuth request to provider for Ueberauth"
363 def prepare_request(%Plug.Conn{} = conn, %{
364 "provider" => provider,
365 "authorization" => auth_attrs
369 |> Scopes.fetch_scopes([])
370 |> Scopes.to_string()
374 |> Map.delete("scopes")
375 |> Map.put("scope", scope)
380 |> Map.drop(~w(scope scopes client_id redirect_uri))
381 |> Map.put("state", state)
383 # Handing the request to Ueberauth
384 redirect(conn, to: o_auth_path(conn, :request, provider, params))
387 def request(%Plug.Conn{} = conn, params) do
389 if params["provider"] do
390 dgettext("errors", "Unsupported OAuth provider: %{provider}.",
391 provider: params["provider"]
394 dgettext("errors", "Bad OAuth request.")
398 |> put_flash(:error, message)
402 def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
403 params = callback_params(params)
404 messages = for e <- Map.get(failure, :errors, []), do: e.message
405 message = Enum.join(messages, "; ")
410 dgettext("errors", "Failed to authenticate: %{message}.", message: message)
412 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
415 def callback(%Plug.Conn{} = conn, params) do
416 params = callback_params(params)
418 with {:ok, registration} <- Authenticator.get_registration(conn) do
419 auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
421 case Repo.get_assoc(registration, :user) do
423 create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
426 registration_params =
427 Map.merge(auth_attrs, %{
428 "nickname" => Registration.nickname(registration),
429 "email" => Registration.email(registration)
433 |> put_session_registration_id(registration.id)
434 |> registration_details(%{"authorization" => registration_params})
438 Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
441 |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
442 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
446 defp callback_params(%{"state" => state} = params) do
447 Map.merge(params, Jason.decode!(state))
450 def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
451 render(conn, "register.html", %{
452 client_id: auth_attrs["client_id"],
453 redirect_uri: auth_attrs["redirect_uri"],
454 state: auth_attrs["state"],
455 scopes: Scopes.fetch_scopes(auth_attrs, []),
456 nickname: auth_attrs["nickname"],
457 email: auth_attrs["email"]
461 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
462 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
463 %Registration{} = registration <- Repo.get(Registration, registration_id),
464 {_, {:ok, auth, _user}} <-
465 {:create_authorization, do_create_authorization(conn, params)},
466 %User{} = user <- Repo.preload(auth, :user).user,
467 {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
469 |> put_session_registration_id(nil)
470 |> after_create_authorization(auth, params)
472 {:create_authorization, error} ->
473 {:register, handle_create_authorization_error(conn, error, params)}
476 {:register, :generic_error}
480 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
481 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
482 %Registration{} = registration <- Repo.get(Registration, registration_id),
483 {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
485 |> put_session_registration_id(nil)
486 |> create_authorization(
491 {:error, changeset} ->
493 Enum.map(changeset.errors, fn {field, {error, _}} ->
501 "ap_id has already been taken",
502 "nickname has already been taken"
506 |> put_status(:forbidden)
507 |> put_flash(:error, "Error: #{message}.")
508 |> registration_details(params)
511 {:register, :generic_error}
515 defp do_create_authorization(
520 "client_id" => client_id,
521 "redirect_uri" => redirect_uri
526 with {_, {:ok, %User{} = user}} <-
527 {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
528 %App{} = app <- Repo.get_by(App, client_id: client_id),
529 true <- redirect_uri in String.split(app.redirect_uris),
530 {:ok, scopes} <- validate_scopes(app, auth_attrs),
531 {:account_status, :active} <- {:account_status, User.account_status(user)},
532 {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
537 # Special case: Local MastodonFE
538 defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
540 defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
542 defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
544 defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
545 do: put_session(conn, :registration_id, registration_id)
547 defp build_and_response_mfa_token(user, auth) do
548 with {:ok, token} <- MFA.Token.create_token(user, auth) do
549 MFAView.render("mfa_response.json", %{token: token, user: user})
553 @spec validate_scopes(App.t(), map()) ::
554 {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
555 defp validate_scopes(%App{} = app, params) do
557 |> Scopes.fetch_scopes(app.scopes)
558 |> Scopes.validate(app.scopes)
561 def default_redirect_uri(%App{} = app) do
567 defp render_invalid_credentials_error(conn) do
568 render_error(conn, :bad_request, "Invalid credentials")