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)
86 # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
87 render(conn, Authenticator.auth_template(), %{
88 response_type: params["response_type"],
89 client_id: params["client_id"],
90 available_scopes: available_scopes,
92 redirect_uri: params["redirect_uri"],
93 state: params["state"],
98 defp handle_existing_authorization(
99 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
100 %{"redirect_uri" => @oob_token_redirect_uri}
102 render(conn, "oob_token_exists.html", %{token: token})
105 defp handle_existing_authorization(
106 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
109 app = Repo.preload(token, :app).app
112 if is_binary(params["redirect_uri"]) do
113 params["redirect_uri"]
115 default_redirect_uri(app)
118 if redirect_uri in String.split(app.redirect_uris) do
119 redirect_uri = redirect_uri(conn, redirect_uri)
120 url_params = %{access_token: token.token}
121 url_params = Maps.put_if_present(url_params, :state, params["state"])
122 url = UriHelper.append_uri_params(redirect_uri, url_params)
123 redirect(conn, external: url)
126 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
127 |> redirect(external: redirect_uri(conn, redirect_uri))
131 def create_authorization(
133 %{"authorization" => _} = params,
136 with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
137 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
138 after_create_authorization(conn, auth, params)
141 handle_create_authorization_error(conn, error, params)
145 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
146 "authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
148 render(conn, "oob_authorization_created.html", %{auth: auth})
151 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
152 "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
154 app = Repo.preload(auth, :app).app
156 # An extra safety measure before we redirect (also done in `do_create_authorization/2`)
157 if redirect_uri in String.split(app.redirect_uris) do
158 redirect_uri = redirect_uri(conn, redirect_uri)
159 url_params = %{code: auth.token}
160 url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
161 url = UriHelper.append_uri_params(redirect_uri, url_params)
162 redirect(conn, external: url)
165 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
166 |> redirect(external: redirect_uri(conn, redirect_uri))
170 defp handle_create_authorization_error(
172 {:error, scopes_issue},
173 %{"authorization" => _} = params
175 when scopes_issue in [:unsupported_scopes, :missing_scopes] do
176 # Per https://github.com/tootsuite/mastodon/blob/
177 # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
179 |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
180 |> put_status(:unauthorized)
184 defp handle_create_authorization_error(
186 {:account_status, :confirmation_pending},
187 %{"authorization" => _} = params
190 |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
191 |> put_status(:forbidden)
195 defp handle_create_authorization_error(
197 {:mfa_required, user, auth, _},
200 {:ok, token} = MFA.Token.create_token(user, auth)
203 "mfa_token" => token.token,
204 "redirect_uri" => params["authorization"]["redirect_uri"],
205 "state" => params["authorization"]["state"]
208 MFAController.show(conn, data)
211 defp handle_create_authorization_error(
213 {:account_status, :password_reset_pending},
214 %{"authorization" => _} = params
217 |> put_flash(:error, dgettext("errors", "Password reset is required"))
218 |> put_status(:forbidden)
222 defp handle_create_authorization_error(
224 {:account_status, :deactivated},
225 %{"authorization" => _} = params
228 |> put_flash(:error, dgettext("errors", "Your account is currently disabled"))
229 |> put_status(:forbidden)
233 defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
234 Authenticator.handle_error(conn, error)
237 @doc "Renew access_token with refresh_token"
240 %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
242 with {:ok, app} <- Token.Utils.fetch_app(conn),
243 {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
244 {:ok, token} <- RefreshToken.grant(token) do
245 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
247 _error -> render_invalid_credentials_error(conn)
251 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do
252 with {:ok, app} <- Token.Utils.fetch_app(conn),
253 fixed_token = Token.Utils.fix_padding(params["code"]),
254 {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
255 %User{} = user <- User.get_cached_by_id(auth.user_id),
256 {:ok, token} <- Token.exchange_token(app, auth) do
257 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
260 handle_token_exchange_error(conn, error)
266 %{"grant_type" => "password"} = params
268 with {:ok, %User{} = user} <- Authenticator.get_user(conn),
269 {:ok, app} <- Token.Utils.fetch_app(conn),
270 requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
271 {:ok, token} <- login(user, app, requested_scopes) do
272 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
275 handle_token_exchange_error(conn, error)
281 %{"grant_type" => "password", "name" => name, "password" => _password} = params
285 |> Map.delete("name")
286 |> Map.put("username", name)
288 token_exchange(conn, params)
291 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
292 with {:ok, app} <- Token.Utils.fetch_app(conn),
293 {:ok, auth} <- Authorization.create_authorization(app, %User{}),
294 {:ok, token} <- Token.exchange_token(app, auth) do
295 json(conn, OAuthView.render("token.json", %{token: token}))
298 handle_token_exchange_error(conn, :invalid_credentails)
303 def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
305 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
307 |> put_status(:forbidden)
308 |> json(build_and_response_mfa_token(user, auth))
311 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
315 "Your account is currently disabled",
317 "account_is_disabled"
321 defp handle_token_exchange_error(
323 {:account_status, :password_reset_pending}
328 "Password reset is required",
330 "password_reset_required"
334 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do
338 "Your login is missing a confirmed e-mail address",
340 "missing_confirmed_email"
344 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
348 "Your account is awaiting approval.",
354 defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
355 render_invalid_credentials_error(conn)
358 def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
359 with {:ok, app} <- Token.Utils.fetch_app(conn),
360 {:ok, _token} <- RevokeToken.revoke(app, params) do
364 # RFC 7009: invalid tokens [in the request] do not cause an error response
369 def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
371 # Response for bad request
372 defp bad_request(%Plug.Conn{} = conn, _) do
373 render_error(conn, :internal_server_error, "Bad request")
376 @doc "Prepares OAuth request to provider for Ueberauth"
377 def prepare_request(%Plug.Conn{} = conn, %{
378 "provider" => provider,
379 "authorization" => auth_attrs
383 |> Scopes.fetch_scopes([])
384 |> Scopes.to_string()
388 |> Map.delete("scopes")
389 |> Map.put("scope", scope)
394 |> Map.drop(~w(scope scopes client_id redirect_uri))
395 |> Map.put("state", state)
397 # Handing the request to Ueberauth
398 redirect(conn, to: o_auth_path(conn, :request, provider, params))
401 def request(%Plug.Conn{} = conn, params) do
403 if params["provider"] do
404 dgettext("errors", "Unsupported OAuth provider: %{provider}.",
405 provider: params["provider"]
408 dgettext("errors", "Bad OAuth request.")
412 |> put_flash(:error, message)
416 def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
417 params = callback_params(params)
418 messages = for e <- Map.get(failure, :errors, []), do: e.message
419 message = Enum.join(messages, "; ")
424 dgettext("errors", "Failed to authenticate: %{message}.", message: message)
426 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
429 def callback(%Plug.Conn{} = conn, params) do
430 params = callback_params(params)
432 with {:ok, registration} <- Authenticator.get_registration(conn) do
433 auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
435 case Repo.get_assoc(registration, :user) do
437 create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
440 registration_params =
441 Map.merge(auth_attrs, %{
442 "nickname" => Registration.nickname(registration),
443 "email" => Registration.email(registration)
447 |> put_session_registration_id(registration.id)
448 |> registration_details(%{"authorization" => registration_params})
452 Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
455 |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
456 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
460 defp callback_params(%{"state" => state} = params) do
461 Map.merge(params, Jason.decode!(state))
464 def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
465 render(conn, "register.html", %{
466 client_id: auth_attrs["client_id"],
467 redirect_uri: auth_attrs["redirect_uri"],
468 state: auth_attrs["state"],
469 scopes: Scopes.fetch_scopes(auth_attrs, []),
470 nickname: auth_attrs["nickname"],
471 email: auth_attrs["email"]
475 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
476 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
477 %Registration{} = registration <- Repo.get(Registration, registration_id),
478 {_, {:ok, auth, _user}} <-
479 {:create_authorization, do_create_authorization(conn, params)},
480 %User{} = user <- Repo.preload(auth, :user).user,
481 {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
483 |> put_session_registration_id(nil)
484 |> after_create_authorization(auth, params)
486 {:create_authorization, error} ->
487 {:register, handle_create_authorization_error(conn, error, params)}
490 {:register, :generic_error}
494 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
495 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
496 %Registration{} = registration <- Repo.get(Registration, registration_id),
497 {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
499 |> put_session_registration_id(nil)
500 |> create_authorization(
505 {:error, changeset} ->
507 Enum.map(changeset.errors, fn {field, {error, _}} ->
515 "ap_id has already been taken",
516 "nickname has already been taken"
520 |> put_status(:forbidden)
521 |> put_flash(:error, "Error: #{message}.")
522 |> registration_details(params)
525 {:register, :generic_error}
529 defp do_create_authorization(conn, auth_attrs, user \\ nil)
531 defp do_create_authorization(
536 "client_id" => client_id,
537 "redirect_uri" => redirect_uri
542 with {_, {:ok, %User{} = user}} <-
543 {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
544 %App{} = app <- Repo.get_by(App, client_id: client_id),
545 true <- redirect_uri in String.split(app.redirect_uris),
546 requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes),
547 {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do
552 defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
553 when is_list(requested_scopes) do
554 with {:account_status, :active} <- {:account_status, User.account_status(user)},
555 {:ok, scopes} <- validate_scopes(app, requested_scopes),
556 {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
561 # Note: intended to be a private function but opened for AccountController that logs in on signup
562 @doc "If checks pass, creates authorization and token for given user, app and requested scopes."
563 def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do
564 with {:ok, auth} <- do_create_authorization(user, app, requested_scopes),
565 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
566 {:ok, token} <- Token.exchange_token(app, auth) do
571 # Special case: Local MastodonFE
572 defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
574 defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
576 defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
578 defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
579 do: put_session(conn, :registration_id, registration_id)
581 defp build_and_response_mfa_token(user, auth) do
582 with {:ok, token} <- MFA.Token.create_token(user, auth) do
583 MFAView.render("mfa_response.json", %{token: token, user: user})
587 @spec validate_scopes(App.t(), map() | list()) ::
588 {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
589 defp validate_scopes(%App{} = app, params) when is_map(params) do
590 requested_scopes = Scopes.fetch_scopes(params, app.scopes)
591 validate_scopes(app, requested_scopes)
594 defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
595 Scopes.validate(requested_scopes, app.scopes)
598 def default_redirect_uri(%App{} = app) do
604 defp render_invalid_credentials_error(conn) do
605 render_error(conn, :bad_request, "Invalid credentials")