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.Scopes
21 alias Pleroma.Web.OAuth.Token
22 alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
23 alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
27 if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
32 plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug])
34 plug(RateLimiter, [name: :authentication] when action == :create_authorization)
36 action_fallback(Pleroma.Web.OAuth.FallbackController)
38 @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"
40 # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg
41 def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do
42 {auth_attrs, params} = Map.pop(params, "authorization")
43 authorize(conn, Map.merge(params, auth_attrs))
46 def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do
47 if ControllerHelper.truthy_param?(params["force_login"]) do
48 do_authorize(conn, params)
50 handle_existing_authorization(conn, params)
54 # Note: the token is set in oauth_plug, but the token and client do not always go together.
55 # For example, MastodonFE's token is set if user requests with another client,
56 # after user already authorized to MastodonFE.
57 # So we have to check client and token.
59 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
60 %{"client_id" => client_id} = params
62 with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app),
63 ^client_id <- t.app.client_id do
64 handle_existing_authorization(conn, params)
66 _ -> do_authorize(conn, params)
70 def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
72 defp do_authorize(%Plug.Conn{} = conn, params) do
73 app = Repo.get_by(App, client_id: params["client_id"])
74 available_scopes = (app && app.scopes) || []
75 scopes = Scopes.fetch_scopes(params, available_scopes)
77 # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
78 render(conn, Authenticator.auth_template(), %{
79 response_type: params["response_type"],
80 client_id: params["client_id"],
81 available_scopes: available_scopes,
83 redirect_uri: params["redirect_uri"],
84 state: params["state"],
89 defp handle_existing_authorization(
90 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
91 %{"redirect_uri" => @oob_token_redirect_uri}
93 render(conn, "oob_token_exists.html", %{token: token})
96 defp handle_existing_authorization(
97 %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
100 app = Repo.preload(token, :app).app
103 if is_binary(params["redirect_uri"]) do
104 params["redirect_uri"]
106 default_redirect_uri(app)
109 if redirect_uri in String.split(app.redirect_uris) do
110 redirect_uri = redirect_uri(conn, redirect_uri)
111 url_params = %{access_token: token.token}
112 url_params = Maps.put_if_present(url_params, :state, params["state"])
113 url = UriHelper.append_uri_params(redirect_uri, url_params)
114 redirect(conn, external: url)
117 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
118 |> redirect(external: redirect_uri(conn, redirect_uri))
122 def create_authorization(
124 %{"authorization" => _} = params,
127 with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
128 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
129 after_create_authorization(conn, auth, params)
132 handle_create_authorization_error(conn, error, params)
136 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
137 "authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
139 render(conn, "oob_authorization_created.html", %{auth: auth})
142 def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
143 "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
145 app = Repo.preload(auth, :app).app
147 # An extra safety measure before we redirect (also done in `do_create_authorization/2`)
148 if redirect_uri in String.split(app.redirect_uris) do
149 redirect_uri = redirect_uri(conn, redirect_uri)
150 url_params = %{code: auth.token}
151 url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
152 url = UriHelper.append_uri_params(redirect_uri, url_params)
153 redirect(conn, external: url)
156 |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
157 |> redirect(external: redirect_uri(conn, redirect_uri))
161 defp handle_create_authorization_error(
163 {:error, scopes_issue},
164 %{"authorization" => _} = params
166 when scopes_issue in [:unsupported_scopes, :missing_scopes] do
167 # Per https://github.com/tootsuite/mastodon/blob/
168 # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
170 |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
171 |> put_status(:unauthorized)
175 defp handle_create_authorization_error(
177 {:account_status, :confirmation_pending},
178 %{"authorization" => _} = params
181 |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
182 |> put_status(:forbidden)
186 defp handle_create_authorization_error(
188 {:mfa_required, user, auth, _},
191 {:ok, token} = MFA.Token.create_token(user, auth)
194 "mfa_token" => token.token,
195 "redirect_uri" => params["authorization"]["redirect_uri"],
196 "state" => params["authorization"]["state"]
199 MFAController.show(conn, data)
202 defp handle_create_authorization_error(
204 {:account_status, :password_reset_pending},
205 %{"authorization" => _} = params
208 |> put_flash(:error, dgettext("errors", "Password reset is required"))
209 |> put_status(:forbidden)
213 defp handle_create_authorization_error(
215 {:account_status, :deactivated},
216 %{"authorization" => _} = params
219 |> put_flash(:error, dgettext("errors", "Your account is currently disabled"))
220 |> put_status(:forbidden)
224 defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
225 Authenticator.handle_error(conn, error)
228 @doc "Renew access_token with refresh_token"
231 %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
233 with {:ok, app} <- Token.Utils.fetch_app(conn),
234 {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
235 {:ok, token} <- RefreshToken.grant(token) do
236 response_attrs = %{created_at: Token.Utils.format_created_at(token)}
238 json(conn, Token.Response.build(user, token, response_attrs))
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 response_attrs = %{created_at: Token.Utils.format_created_at(token)}
252 json(conn, Token.Response.build(user, token, response_attrs))
255 handle_token_exchange_error(conn, error)
261 %{"grant_type" => "password"} = params
263 with {:ok, %User{} = user} <- Authenticator.get_user(conn),
264 {:ok, app} <- Token.Utils.fetch_app(conn),
265 {:account_status, :active} <- {:account_status, User.account_status(user)},
266 {:ok, scopes} <- validate_scopes(app, params),
267 {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
268 {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
269 {:ok, token} <- Token.exchange_token(app, auth) do
270 json(conn, Token.Response.build(user, token))
273 handle_token_exchange_error(conn, error)
279 %{"grant_type" => "password", "name" => name, "password" => _password} = params
283 |> Map.delete("name")
284 |> Map.put("username", name)
286 token_exchange(conn, params)
289 def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
290 with {:ok, app} <- Token.Utils.fetch_app(conn),
291 {:ok, auth} <- Authorization.create_authorization(app, %User{}),
292 {:ok, token} <- Token.exchange_token(app, auth) do
293 json(conn, Token.Response.build_for_client_credentials(token))
296 handle_token_exchange_error(conn, :invalid_credentails)
301 def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
303 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
305 |> put_status(:forbidden)
306 |> json(build_and_response_mfa_token(user, auth))
309 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
313 "Your account is currently disabled",
315 "account_is_disabled"
319 defp handle_token_exchange_error(
321 {:account_status, :password_reset_pending}
326 "Password reset is required",
328 "password_reset_required"
332 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do
336 "Your login is missing a confirmed e-mail address",
338 "missing_confirmed_email"
342 defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
343 render_invalid_credentials_error(conn)
346 def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
347 with {:ok, app} <- Token.Utils.fetch_app(conn),
348 {:ok, _token} <- RevokeToken.revoke(app, params) do
352 # RFC 7009: invalid tokens [in the request] do not cause an error response
357 def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
359 # Response for bad request
360 defp bad_request(%Plug.Conn{} = conn, _) do
361 render_error(conn, :internal_server_error, "Bad request")
364 @doc "Prepares OAuth request to provider for Ueberauth"
365 def prepare_request(%Plug.Conn{} = conn, %{
366 "provider" => provider,
367 "authorization" => auth_attrs
371 |> Scopes.fetch_scopes([])
372 |> Scopes.to_string()
376 |> Map.delete("scopes")
377 |> Map.put("scope", scope)
382 |> Map.drop(~w(scope scopes client_id redirect_uri))
383 |> Map.put("state", state)
385 # Handing the request to Ueberauth
386 redirect(conn, to: o_auth_path(conn, :request, provider, params))
389 def request(%Plug.Conn{} = conn, params) do
391 if params["provider"] do
392 dgettext("errors", "Unsupported OAuth provider: %{provider}.",
393 provider: params["provider"]
396 dgettext("errors", "Bad OAuth request.")
400 |> put_flash(:error, message)
404 def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
405 params = callback_params(params)
406 messages = for e <- Map.get(failure, :errors, []), do: e.message
407 message = Enum.join(messages, "; ")
412 dgettext("errors", "Failed to authenticate: %{message}.", message: message)
414 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
417 def callback(%Plug.Conn{} = conn, params) do
418 params = callback_params(params)
420 with {:ok, registration} <- Authenticator.get_registration(conn) do
421 auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
423 case Repo.get_assoc(registration, :user) do
425 create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
428 registration_params =
429 Map.merge(auth_attrs, %{
430 "nickname" => Registration.nickname(registration),
431 "email" => Registration.email(registration)
435 |> put_session_registration_id(registration.id)
436 |> registration_details(%{"authorization" => registration_params})
440 Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
443 |> put_flash(:error, dgettext("errors", "Failed to set up user account."))
444 |> redirect(external: redirect_uri(conn, params["redirect_uri"]))
448 defp callback_params(%{"state" => state} = params) do
449 Map.merge(params, Jason.decode!(state))
452 def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
453 render(conn, "register.html", %{
454 client_id: auth_attrs["client_id"],
455 redirect_uri: auth_attrs["redirect_uri"],
456 state: auth_attrs["state"],
457 scopes: Scopes.fetch_scopes(auth_attrs, []),
458 nickname: auth_attrs["nickname"],
459 email: auth_attrs["email"]
463 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
464 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
465 %Registration{} = registration <- Repo.get(Registration, registration_id),
466 {_, {:ok, auth, _user}} <-
467 {:create_authorization, do_create_authorization(conn, params)},
468 %User{} = user <- Repo.preload(auth, :user).user,
469 {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
471 |> put_session_registration_id(nil)
472 |> after_create_authorization(auth, params)
474 {:create_authorization, error} ->
475 {:register, handle_create_authorization_error(conn, error, params)}
478 {:register, :generic_error}
482 def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
483 with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
484 %Registration{} = registration <- Repo.get(Registration, registration_id),
485 {:ok, user} <- Authenticator.create_from_registration(conn, registration) do
487 |> put_session_registration_id(nil)
488 |> create_authorization(
493 {:error, changeset} ->
495 Enum.map(changeset.errors, fn {field, {error, _}} ->
503 "ap_id has already been taken",
504 "nickname has already been taken"
508 |> put_status(:forbidden)
509 |> put_flash(:error, "Error: #{message}.")
510 |> registration_details(params)
513 {:register, :generic_error}
517 defp do_create_authorization(
522 "client_id" => client_id,
523 "redirect_uri" => redirect_uri
528 with {_, {:ok, %User{} = user}} <-
529 {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
530 %App{} = app <- Repo.get_by(App, client_id: client_id),
531 true <- redirect_uri in String.split(app.redirect_uris),
532 {:ok, scopes} <- validate_scopes(app, auth_attrs),
533 {:account_status, :active} <- {:account_status, User.account_status(user)},
534 {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
539 # Special case: Local MastodonFE
540 defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
542 defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
544 defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
546 defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
547 do: put_session(conn, :registration_id, registration_id)
549 defp build_and_response_mfa_token(user, auth) do
550 with {:ok, token} <- MFA.Token.create_token(user, auth) do
551 Token.Response.build_for_mfa_token(user, token)
555 @spec validate_scopes(App.t(), map()) ::
556 {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
557 defp validate_scopes(%App{} = app, params) do
559 |> Scopes.fetch_scopes(app.scopes)
560 |> Scopes.validate(app.scopes)
563 def default_redirect_uri(%App{} = app) do
569 defp render_invalid_credentials_error(conn) do
570 render_error(conn, :bad_request, "Invalid credentials")