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
10 alias Pleroma.Plugs.RateLimiter
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.Scopes
20 alias Pleroma.Web.OAuth.Token
21 alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
22 alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
26 if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
31 plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug])
33 plug(RateLimiter, [name: :authentication] when action == :create_authorization)
35 action_fallback(Pleroma.Web.OAuth.FallbackController)
37 @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"
39 # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg
40 def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do
41 {auth_attrs, params} = Map.pop(params, "authorization")
42 authorize(conn, Map.merge(params, auth_attrs))
45 def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do
46 if ControllerHelper.truthy_param?(params["force_login"]) do
47 do_authorize(conn, params)
49 handle_existing_authorization(conn, params)
53 # Note: the token is set in oauth_plug, but the token and client do not always go together.
54 # For example, MastodonFE's token is set if user requests with another client,
55 # after user already authorized to MastodonFE.
56 # So we have to check client and token.
58 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
59 %{"client_id" => client_id} = params
61 with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app),
62 ^client_id <- t.app.client_id do
63 handle_existing_authorization(conn, params)
65 _ -> do_authorize(conn, params)
69 def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
71 defp do_authorize(%Plug.Conn{} = conn, params) do
72 app = Repo.get_by(App, client_id: params["client_id"])
73 available_scopes = (app && app.scopes) || []
74 scopes = Scopes.fetch_scopes(params, available_scopes)
76 # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
77 render(conn, Authenticator.auth_template(), %{
78 response_type: params["response_type"],
79 client_id: params["client_id"],
80 available_scopes: available_scopes,
82 redirect_uri: params["redirect_uri"],
83 state: params["state"],
88 defp handle_existing_authorization(
89 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
90 %{"redirect_uri" => @oob_token_redirect_uri}
92 render(conn, "oob_token_exists.html", %{token: token})
95 defp handle_existing_authorization(
96 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
99 app = Repo.preload(token, :app).app
102 if is_binary(params["redirect_uri"]) do
103 params["redirect_uri"]
105 default_redirect_uri(app)
108 if redirect_uri in String.split(app.redirect_uris) do
109 redirect_uri = redirect_uri(conn, redirect_uri)
110 url_params = %{access_token: token.token}
111 url_params = UriHelper.append_param_if_present(url_params, :state, params["state"])
112 url = UriHelper.append_uri_params(redirect_uri, url_params)
113 redirect(conn, external: url)
116 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
117 |> redirect(external: redirect_uri(conn, redirect_uri))
121 def create_authorization(
123 %{"authorization" => _} = params,
126 with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
127 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
128 after_create_authorization(conn, auth, params)
131 handle_create_authorization_error(conn, error, params)
135 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
136 "authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
138 render(conn, "oob_authorization_created.html", %{auth: auth})
141 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
142 "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
144 app = Repo.preload(auth, :app).app
146 # An extra safety measure before we redirect (also done in `do_create_authorization/2`)
147 if redirect_uri in String.split(app.redirect_uris) do
148 redirect_uri = redirect_uri(conn, redirect_uri)
149 url_params = %{code: auth.token}
150 url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"])
151 url = UriHelper.append_uri_params(redirect_uri, url_params)
152 redirect(conn, external: url)
155 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
156 |> redirect(external: redirect_uri(conn, redirect_uri))
160 defp handle_create_authorization_error(
162 {:error, scopes_issue},
163 %{"authorization" => _} = params
165 when scopes_issue in [:unsupported_scopes, :missing_scopes] do
166 # Per https://github.com/tootsuite/mastodon/blob/
167 # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
169 |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
170 |> put_status(:unauthorized)
174 defp handle_create_authorization_error(
176 {:account_status, :confirmation_pending},
177 %{"authorization" => _} = params
180 |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
181 |> put_status(:forbidden)
185 defp handle_create_authorization_error(
187 {:mfa_required, user, auth, _},
190 {:ok, token} = MFA.Token.create_token(user, auth)
193 "mfa_token" => token.token,
194 "redirect_uri" => params["authorization"]["redirect_uri"],
195 "state" => params["authorization"]["state"]
198 MFAController.show(conn, data)
201 defp handle_create_authorization_error(
203 {:account_status, :password_reset_pending},
204 %{"authorization" => _} = params
207 |> put_flash(:error, dgettext("errors", "Password reset is required"))
208 |> put_status(:forbidden)
212 defp handle_create_authorization_error(
214 {:account_status, :deactivated},
215 %{"authorization" => _} = params
218 |> put_flash(:error, dgettext("errors", "Your account is currently disabled"))
219 |> put_status(:forbidden)
223 defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
224 Authenticator.handle_error(conn, error)
227 @doc "Renew access_token with refresh_token"
230 %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
232 with {:ok, app} <- Token.Utils.fetch_app(conn),
233 {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
234 {:ok, token} <- RefreshToken.grant(token) do
235 response_attrs = %{created_at: Token.Utils.format_created_at(token)}
237 json(conn, Token.Response.build(user, token, response_attrs))
239 _error -> render_invalid_credentials_error(conn)
243 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do
244 with {:ok, app} <- Token.Utils.fetch_app(conn),
245 fixed_token = Token.Utils.fix_padding(params["code"]),
246 {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
247 %User{} = user <- User.get_cached_by_id(auth.user_id),
248 {:ok, token} <- Token.exchange_token(app, auth) do
249 response_attrs = %{created_at: Token.Utils.format_created_at(token)}
251 json(conn, Token.Response.build(user, token, response_attrs))
254 handle_token_exchange_error(conn, error)
260 %{"grant_type" => "password"} = params
262 with {:ok, %User{} = user} <- Authenticator.get_user(conn),
263 {:ok, app} <- Token.Utils.fetch_app(conn),
264 {:account_status, :active} <- {:account_status, User.account_status(user)},
265 {:ok, scopes} <- validate_scopes(app, params),
266 {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
267 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
268 {:ok, token} <- Token.exchange_token(app, auth) do
269 json(conn, Token.Response.build(user, token))
272 handle_token_exchange_error(conn, error)
278 %{"grant_type" => "password", "name" => name, "password" => _password} = params
282 |> Map.delete("name")
283 |> Map.put("username", name)
285 token_exchange(conn, params)
288 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
289 with {:ok, app} <- Token.Utils.fetch_app(conn),
290 {:ok, auth} <- Authorization.create_authorization(app, %User{}),
291 {:ok, token} <- Token.exchange_token(app, auth) do
292 json(conn, Token.Response.build_for_client_credentials(token))
295 handle_token_exchange_error(conn, :invalid_credentails)
300 def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
302 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
304 |> put_status(:forbidden)
305 |> json(build_and_response_mfa_token(user, auth))
308 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
312 "Your account is currently disabled",
314 "account_is_disabled"
318 defp handle_token_exchange_error(
320 {:account_status, :password_reset_pending}
325 "Password reset is required",
327 "password_reset_required"
331 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do
335 "Your login is missing a confirmed e-mail address",
337 "missing_confirmed_email"
341 defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
342 render_invalid_credentials_error(conn)
345 def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
346 with {:ok, app} <- Token.Utils.fetch_app(conn),
347 {:ok, _token} <- RevokeToken.revoke(app, params) do
351 # RFC 7009: invalid tokens [in the request] do not cause an error response
356 def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
358 # Response for bad request
359 defp bad_request(%Plug.Conn{} = conn, _) do
360 render_error(conn, :internal_server_error, "Bad request")
363 @doc "Prepares OAuth request to provider for Ueberauth"
364 def prepare_request(%Plug.Conn{} = conn, %{
365 "provider" => provider,
366 "authorization" => auth_attrs
370 |> Scopes.fetch_scopes([])
371 |> Scopes.to_string()
375 |> Map.delete("scopes")
376 |> Map.put("scope", scope)
381 |> Map.drop(~w(scope scopes client_id redirect_uri))
382 |> Map.put("state", state)
384 # Handing the request to Ueberauth
385 redirect(conn, to: o_auth_path(conn, :request, provider, params))
388 def request(%Plug.Conn{} = conn, params) do
390 if params["provider"] do
391 dgettext("errors", "Unsupported OAuth provider: %{provider}.",
392 provider: params["provider"]
395 dgettext("errors", "Bad OAuth request.")
399 |> put_flash(:error, message)
403 def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
404 params = callback_params(params)
405 messages = for e <- Map.get(failure, :errors, []), do: e.message
406 message = Enum.join(messages, "; ")
411 dgettext("errors", "Failed to authenticate: %{message}.", message: message)
413 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
416 def callback(%Plug.Conn{} = conn, params) do
417 params = callback_params(params)
419 with {:ok, registration} <- Authenticator.get_registration(conn) do
420 auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
422 case Repo.get_assoc(registration, :user) do
424 create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
427 registration_params =
428 Map.merge(auth_attrs, %{
429 "nickname" => Registration.nickname(registration),
430 "email" => Registration.email(registration)
434 |> put_session_registration_id(registration.id)
435 |> registration_details(%{"authorization" => registration_params})
439 Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
442 |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
443 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
447 defp callback_params(%{"state" => state} = params) do
448 Map.merge(params, Jason.decode!(state))
451 def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
452 render(conn, "register.html", %{
453 client_id: auth_attrs["client_id"],
454 redirect_uri: auth_attrs["redirect_uri"],
455 state: auth_attrs["state"],
456 scopes: Scopes.fetch_scopes(auth_attrs, []),
457 nickname: auth_attrs["nickname"],
458 email: auth_attrs["email"]
462 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
463 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
464 %Registration{} = registration <- Repo.get(Registration, registration_id),
465 {_, {:ok, auth, _user}} <-
466 {:create_authorization, do_create_authorization(conn, params)},
467 %User{} = user <- Repo.preload(auth, :user).user,
468 {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
470 |> put_session_registration_id(nil)
471 |> after_create_authorization(auth, params)
473 {:create_authorization, error} ->
474 {:register, handle_create_authorization_error(conn, error, params)}
477 {:register, :generic_error}
481 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
482 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
483 %Registration{} = registration <- Repo.get(Registration, registration_id),
484 {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
486 |> put_session_registration_id(nil)
487 |> create_authorization(
492 {:error, changeset} ->
494 Enum.map(changeset.errors, fn {field, {error, _}} ->
502 "ap_id has already been taken",
503 "nickname has already been taken"
507 |> put_status(:forbidden)
508 |> put_flash(:error, "Error: #{message}.")
509 |> registration_details(params)
512 {:register, :generic_error}
516 defp do_create_authorization(
521 "client_id" => client_id,
522 "redirect_uri" => redirect_uri
527 with {_, {:ok, %User{} = user}} <-
528 {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
529 %App{} = app <- Repo.get_by(App, client_id: client_id),
530 true <- redirect_uri in String.split(app.redirect_uris),
531 {:ok, scopes} <- validate_scopes(app, auth_attrs),
532 {:account_status, :active} <- {:account_status, User.account_status(user)},
533 {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
538 # Special case: Local MastodonFE
539 defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
541 defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
543 defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
545 defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
546 do: put_session(conn, :registration_id, registration_id)
548 defp build_and_response_mfa_token(user, auth) do
549 with {:ok, token} <- MFA.Token.create_token(user, auth) do
550 Token.Response.build_for_mfa_token(user, token)
554 @spec validate_scopes(App.t(), map()) ::
555 {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
556 defp validate_scopes(%App{} = app, params) do
558 |> Scopes.fetch_scopes(app.scopes)
559 |> Scopes.validate(app.scopes)
562 def default_redirect_uri(%App{} = app) do
568 defp render_invalid_credentials_error(conn) do
569 render_error(conn, :bad_request, "Invalid credentials")