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.modify_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 # Enforcing the view to reuse the template when calling from other controllers
150 |> put_view(OAuthView)
151 |> render("oob_authorization_created.html", %{auth: auth})
154 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
155 "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
157 app = Repo.preload(auth, :app).app
159 # An extra safety measure before we redirect (also done in `do_create_authorization/2`)
160 if redirect_uri in String.split(app.redirect_uris) do
161 redirect_uri = redirect_uri(conn, redirect_uri)
162 url_params = %{code: auth.token}
163 url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
164 url = UriHelper.modify_uri_params(redirect_uri, url_params)
165 redirect(conn, external: url)
168 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
169 |> redirect(external: redirect_uri(conn, redirect_uri))
173 defp handle_create_authorization_error(
175 {:error, scopes_issue},
176 %{"authorization" => _} = params
178 when scopes_issue in [:unsupported_scopes, :missing_scopes] do
179 # Per https://github.com/tootsuite/mastodon/blob/
180 # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
182 |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
183 |> put_status(:unauthorized)
187 defp handle_create_authorization_error(
189 {:account_status, :confirmation_pending},
190 %{"authorization" => _} = params
193 |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
194 |> put_status(:forbidden)
198 defp handle_create_authorization_error(
200 {:mfa_required, user, auth, _},
203 {:ok, token} = MFA.Token.create(user, auth)
206 "mfa_token" => token.token,
207 "redirect_uri" => params["authorization"]["redirect_uri"],
208 "state" => params["authorization"]["state"]
211 MFAController.show(conn, data)
214 defp handle_create_authorization_error(
216 {:account_status, :password_reset_pending},
217 %{"authorization" => _} = params
220 |> put_flash(:error, dgettext("errors", "Password reset is required"))
221 |> put_status(:forbidden)
225 defp handle_create_authorization_error(
227 {:account_status, :deactivated},
228 %{"authorization" => _} = params
231 |> put_flash(:error, dgettext("errors", "Your account is currently disabled"))
232 |> put_status(:forbidden)
236 defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
237 Authenticator.handle_error(conn, error)
240 @doc "Renew access_token with refresh_token"
243 %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
245 with {:ok, app} <- Token.Utils.fetch_app(conn),
246 {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
247 {:ok, token} <- RefreshToken.grant(token) do
248 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
250 _error -> render_invalid_credentials_error(conn)
254 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do
255 with {:ok, app} <- Token.Utils.fetch_app(conn),
256 fixed_token = Token.Utils.fix_padding(params["code"]),
257 {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
258 %User{} = user <- User.get_cached_by_id(auth.user_id),
259 {:ok, token} <- Token.exchange_token(app, auth) do
260 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
263 handle_token_exchange_error(conn, error)
269 %{"grant_type" => "password"} = params
271 with {:ok, %User{} = user} <- Authenticator.get_user(conn),
272 {:ok, app} <- Token.Utils.fetch_app(conn),
273 requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
274 {:ok, token} <- login(user, app, requested_scopes) do
275 json(conn, OAuthView.render("token.json", %{user: user, token: token}))
278 handle_token_exchange_error(conn, error)
284 %{"grant_type" => "password", "name" => name, "password" => _password} = params
288 |> Map.delete("name")
289 |> Map.put("username", name)
291 token_exchange(conn, params)
294 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
295 with {:ok, app} <- Token.Utils.fetch_app(conn),
296 {:ok, auth} <- Authorization.create_authorization(app, %User{}),
297 {:ok, token} <- Token.exchange_token(app, auth) do
298 json(conn, OAuthView.render("token.json", %{token: token}))
301 handle_token_exchange_error(conn, :invalid_credentails)
306 def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
308 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
310 |> put_status(:forbidden)
311 |> json(build_and_response_mfa_token(user, auth))
314 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
318 "Your account is currently disabled",
320 "account_is_disabled"
324 defp handle_token_exchange_error(
326 {:account_status, :password_reset_pending}
331 "Password reset is required",
333 "password_reset_required"
337 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do
341 "Your login is missing a confirmed e-mail address",
343 "missing_confirmed_email"
347 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
351 "Your account is awaiting approval.",
357 defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
358 render_invalid_credentials_error(conn)
361 def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
362 with {:ok, app} <- Token.Utils.fetch_app(conn),
363 {:ok, _token} <- RevokeToken.revoke(app, params) do
367 # RFC 7009: invalid tokens [in the request] do not cause an error response
372 def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
374 # Response for bad request
375 defp bad_request(%Plug.Conn{} = conn, _) do
376 render_error(conn, :internal_server_error, "Bad request")
379 @doc "Prepares OAuth request to provider for Ueberauth"
380 def prepare_request(%Plug.Conn{} = conn, %{
381 "provider" => provider,
382 "authorization" => auth_attrs
386 |> Scopes.fetch_scopes([])
387 |> Scopes.to_string()
391 |> Map.delete("scopes")
392 |> Map.put("scope", scope)
397 |> Map.drop(~w(scope scopes client_id redirect_uri))
398 |> Map.put("state", state)
400 # Handing the request to Ueberauth
401 redirect(conn, to: o_auth_path(conn, :request, provider, params))
404 def request(%Plug.Conn{} = conn, params) do
406 if params["provider"] do
407 dgettext("errors", "Unsupported OAuth provider: %{provider}.",
408 provider: params["provider"]
411 dgettext("errors", "Bad OAuth request.")
415 |> put_flash(:error, message)
419 def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
420 params = callback_params(params)
421 messages = for e <- Map.get(failure, :errors, []), do: e.message
422 message = Enum.join(messages, "; ")
427 dgettext("errors", "Failed to authenticate: %{message}.", message: message)
429 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
432 def callback(%Plug.Conn{} = conn, params) do
433 params = callback_params(params)
435 with {:ok, registration} <- Authenticator.get_registration(conn) do
436 auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
438 case Repo.get_assoc(registration, :user) do
440 create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
443 registration_params =
444 Map.merge(auth_attrs, %{
445 "nickname" => Registration.nickname(registration),
446 "email" => Registration.email(registration)
450 |> put_session_registration_id(registration.id)
451 |> registration_details(%{"authorization" => registration_params})
455 Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
458 |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
459 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
463 defp callback_params(%{"state" => state} = params) do
464 Map.merge(params, Jason.decode!(state))
467 def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
468 render(conn, "register.html", %{
469 client_id: auth_attrs["client_id"],
470 redirect_uri: auth_attrs["redirect_uri"],
471 state: auth_attrs["state"],
472 scopes: Scopes.fetch_scopes(auth_attrs, []),
473 nickname: auth_attrs["nickname"],
474 email: auth_attrs["email"]
478 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
479 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
480 %Registration{} = registration <- Repo.get(Registration, registration_id),
481 {_, {:ok, auth, _user}} <-
482 {:create_authorization, do_create_authorization(conn, params)},
483 %User{} = user <- Repo.preload(auth, :user).user,
484 {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
486 |> put_session_registration_id(nil)
487 |> after_create_authorization(auth, params)
489 {:create_authorization, error} ->
490 {:register, handle_create_authorization_error(conn, error, params)}
493 {:register, :generic_error}
497 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
498 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
499 %Registration{} = registration <- Repo.get(Registration, registration_id),
500 {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
502 |> put_session_registration_id(nil)
503 |> create_authorization(
508 {:error, changeset} ->
510 Enum.map(changeset.errors, fn {field, {error, _}} ->
518 "ap_id has already been taken",
519 "nickname has already been taken"
523 |> put_status(:forbidden)
524 |> put_flash(:error, "Error: #{message}.")
525 |> registration_details(params)
528 {:register, :generic_error}
532 defp do_create_authorization(conn, auth_attrs, user \\ nil)
534 defp do_create_authorization(
539 "client_id" => client_id,
540 "redirect_uri" => redirect_uri
545 with {_, {:ok, %User{} = user}} <-
546 {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
547 %App{} = app <- Repo.get_by(App, client_id: client_id),
548 true <- redirect_uri in String.split(app.redirect_uris),
549 requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes),
550 {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do
555 defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
556 when is_list(requested_scopes) do
557 with {:account_status, :active} <- {:account_status, User.account_status(user)},
558 {:ok, scopes} <- validate_scopes(app, requested_scopes),
559 {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
564 # Note: intended to be a private function but opened for AccountController that logs in on signup
565 @doc "If checks pass, creates authorization and token for given user, app and requested scopes."
566 def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do
567 with {:ok, auth} <- do_create_authorization(user, app, requested_scopes),
568 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
569 {:ok, token} <- Token.exchange_token(app, auth) do
574 # Special case: Local MastodonFE
575 defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
577 defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
579 defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
581 defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
582 do: put_session(conn, :registration_id, registration_id)
584 defp build_and_response_mfa_token(user, auth) do
585 with {:ok, token} <- MFA.Token.create(user, auth) do
586 MFAView.render("mfa_response.json", %{token: token, user: user})
590 @spec validate_scopes(App.t(), map() | list()) ::
591 {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
592 defp validate_scopes(%App{} = app, params) when is_map(params) do
593 requested_scopes = Scopes.fetch_scopes(params, app.scopes)
594 validate_scopes(app, requested_scopes)
597 defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
598 Scopes.validate(requested_scopes, app.scopes)
601 def default_redirect_uri(%App{} = app) do
607 defp render_invalid_credentials_error(conn) do
608 render_error(conn, :bad_request, "Invalid credentials")