From 48b8efa7590fcdb09b3f338a7746c9f944257fe8 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 4 Mar 2021 02:54:55 +0100 Subject: [PATCH] Fixup OAuth [wip] --- .../api_spec/operations/o_auth_operation.ex | 31 +- lib/pleroma/web/o_auth/fallback_controller.ex | 5 +- lib/pleroma/web/o_auth/mfa_controller.ex | 3 +- .../web/o_auth/o_auth_browser_controller.ex | 308 ++++++ lib/pleroma/web/o_auth/o_auth_controller.ex | 399 ++------ lib/pleroma/web/router.ex | 23 +- .../web/templates/o_auth/o_auth/show.html.eex | 2 +- .../web/o_auth/ldap_authorization_test.exs | 54 +- .../o_auth/o_auth_browser_controller_test.exs | 576 +++++++++++ .../web/o_auth/o_auth_controller_test.exs | 932 ++++-------------- 10 files changed, 1216 insertions(+), 1117 deletions(-) create mode 100644 lib/pleroma/web/o_auth/o_auth_browser_controller.ex create mode 100644 test/pleroma/web/o_auth/o_auth_browser_controller_test.exs diff --git a/lib/pleroma/web/api_spec/operations/o_auth_operation.ex b/lib/pleroma/web/api_spec/operations/o_auth_operation.ex index d507fddd5..cb049e5e8 100644 --- a/lib/pleroma/web/api_spec/operations/o_auth_operation.ex +++ b/lib/pleroma/web/api_spec/operations/o_auth_operation.ex @@ -77,8 +77,18 @@ defmodule Pleroma.Web.ApiSpec.OAuthOperation do "Set equal to `authorization_code` if `code` is provided in order to gain user-level access. Set equal to `password` if `username` and `password` are provided. Otherwise, set equal to `client_credentials` to obtain app-level access only.", required: true ), - Operation.parameter(:username, :query, :string, "User's username, used with `grant_type=password`"), - Operation.parameter(:password, :query, :string, "User's password, used with `grant_type=password`") + Operation.parameter( + :username, + :query, + :string, + "User's username, used with `grant_type=password`" + ), + Operation.parameter( + :password, + :query, + :string, + "User's password, used with `grant_type=password`" + ) ], responses: %{ 200 => @@ -161,23 +171,6 @@ defmodule Pleroma.Web.ApiSpec.OAuthOperation do } end - def create_authorization_operation do - %Operation{ - tags: ["OAuth"], - summary: "Create Authorization", - operationId: "OAuthController.create_authorization", - parameters: [], - responses: %{ - 200 => - Operation.response("Success", "application/json", %Schema{ - type: :object, - properties: %{status: %Schema{type: :string, example: "success"}} - }), - 400 => Operation.response("Error", "application/json", ApiError) - } - } - end - def prepare_request_operation do %Operation{ tags: ["OAuth"], diff --git a/lib/pleroma/web/o_auth/fallback_controller.ex b/lib/pleroma/web/o_auth/fallback_controller.ex index df68cbfc1..4bcf12253 100644 --- a/lib/pleroma/web/o_auth/fallback_controller.ex +++ b/lib/pleroma/web/o_auth/fallback_controller.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.OAuth.FallbackController do use Pleroma.Web, :controller + alias Pleroma.Web.OAuth.OAuthBrowserController alias Pleroma.Web.OAuth.OAuthController def call(conn, {:register, :generic_error}) do @@ -13,14 +14,14 @@ defmodule Pleroma.Web.OAuth.FallbackController do :error, dgettext("errors", "Unknown error, please check the details and try again.") ) - |> OAuthController.registration_details(conn.params) + |> OAuthBrowserController.registration_details(conn.params) end def call(conn, {:register, _error}) do conn |> put_status(:unauthorized) |> put_flash(:error, dgettext("errors", "Invalid Username/Password")) - |> OAuthController.registration_details(conn.params) + |> OAuthBrowserController.registration_details(conn.params) end def call(conn, _error) do diff --git a/lib/pleroma/web/o_auth/mfa_controller.ex b/lib/pleroma/web/o_auth/mfa_controller.ex index b38b00213..97b307301 100644 --- a/lib/pleroma/web/o_auth/mfa_controller.ex +++ b/lib/pleroma/web/o_auth/mfa_controller.ex @@ -12,6 +12,7 @@ defmodule Pleroma.Web.OAuth.MFAController do alias Pleroma.MFA alias Pleroma.Web.Auth.TOTPAuthenticator alias Pleroma.Web.OAuth.MFAView, as: View + alias Pleroma.Web.OAuth.OAuthBrowserController alias Pleroma.Web.OAuth.OAuthController alias Pleroma.Web.OAuth.Token @@ -40,7 +41,7 @@ defmodule Pleroma.Web.OAuth.MFAController do with {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), {:ok, _} <- validates_challenge(user, mfa_params) do conn - |> OAuthController.after_create_authorization(auth, %{ + |> OAuthBrowserController.after_create_authorization(auth, %{ "authorization" => %{ "redirect_uri" => mfa_params["redirect_uri"], "state" => mfa_params["state"] diff --git a/lib/pleroma/web/o_auth/o_auth_browser_controller.ex b/lib/pleroma/web/o_auth/o_auth_browser_controller.ex new file mode 100644 index 000000000..3f78f6ef1 --- /dev/null +++ b/lib/pleroma/web/o_auth/o_auth_browser_controller.ex @@ -0,0 +1,308 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.OAuthBrowserController do + use Pleroma.Web, :controller + + alias Pleroma.Helpers.UriHelper + alias Pleroma.Maps + alias Pleroma.MFA + alias Pleroma.Registration + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.Auth.Authenticator + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.OAuthController + alias Pleroma.Web.OAuth.MFAController + alias Pleroma.Web.OAuth.OAuthView + alias Pleroma.Web.OAuth.Scopes + alias Pleroma.Web.Plugs.RateLimiter + + require Logger + + if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) + + plug(:fetch_session) + plug(:fetch_flash) + + plug(:skip_plug, [ + Pleroma.Web.Plugs.OAuthScopesPlug, + Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug + ]) + + plug(RateLimiter, name: :authentication) + + action_fallback(Pleroma.Web.OAuth.FallbackController) + + @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" + + def authorize_callback(_, _, opts \\ []) + + def authorize_callback(%Plug.Conn{assigns: %{user: %User{} = user}} = conn, params, []) do + authorize_callback(conn, params, user: user) + end + + def authorize_callback(%Plug.Conn{} = conn, %{"authorization" => _} = params, opts) do + with {:ok, auth, user} <- OAuthController.do_create_authorization(conn, params, opts[:user]), + {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do + after_create_authorization(conn, auth, params) + else + error -> + handle_create_authorization_error(conn, error, params) + end + end + + def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ + "authorization" => %{"redirect_uri" => @oob_token_redirect_uri} + }) do + # Enforcing the view to reuse the template when calling from other controllers + conn + |> put_view(OAuthView) + |> render("oob_authorization_created.html", %{auth: auth}) + end + + def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ + "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs + }) do + app = Repo.preload(auth, :app).app + + # An extra safety measure before we redirect (also done in `do_create_authorization/2`) + if redirect_uri in String.split(app.redirect_uris) do + redirect_uri = OAuthController.redirect_uri(conn, redirect_uri) + url_params = %{code: auth.token} + url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"]) + url = UriHelper.modify_uri_params(redirect_uri, url_params) + redirect(conn, external: url) + else + conn + |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) + |> redirect(external: OAuthController.redirect_uri(conn, redirect_uri)) + end + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:error, scopes_issue}, + %{"authorization" => _} = params + ) + when scopes_issue in [:unsupported_scopes, :missing_scopes] do + # Per https://github.com/tootsuite/mastodon/blob/ + # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 + conn + |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes")) + |> put_status(:unauthorized) + |> OAuthController.authorize(params) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:account_status, :confirmation_pending}, + %{"authorization" => _} = params + ) do + conn + |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address")) + |> put_status(:forbidden) + |> OAuthController.authorize(params) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:mfa_required, user, auth, _}, + params + ) do + {:ok, token} = MFA.Token.create(user, auth) + + data = %{ + "mfa_token" => token.token, + "redirect_uri" => params["authorization"]["redirect_uri"], + "state" => params["authorization"]["state"] + } + + MFAController.show(conn, data) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:account_status, :password_reset_pending}, + %{"authorization" => _} = params + ) do + conn + |> put_flash(:error, dgettext("errors", "Password reset is required")) + |> put_status(:forbidden) + |> OAuthController.authorize(params) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:account_status, :deactivated}, + %{"authorization" => _} = params + ) do + conn + |> put_flash(:error, dgettext("errors", "Your account is currently disabled")) + |> put_status(:forbidden) + |> OAuthController.authorize(params) + end + + defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do + Authenticator.handle_error(conn, error) + end + + @doc "Prepares OAuth request to provider for Ueberauth" + def prepare_request(%Plug.Conn{} = conn, %{ + "provider" => provider, + "authorization" => auth_attrs + }) do + scope = + auth_attrs + |> Scopes.fetch_scopes([]) + |> Scopes.to_string() + + state = + auth_attrs + |> Map.delete("scopes") + |> Map.put("scope", scope) + |> Jason.encode!() + + params = + auth_attrs + |> Map.drop(~w(scope scopes client_id redirect_uri)) + |> Map.put("state", state) + + # Handing the request to Ueberauth + redirect(conn, to: o_auth_browser_path(conn, :provider_request, provider, params)) + end + + def provider_request(%Plug.Conn{} = conn, params) do + message = + if params["provider"] do + dgettext("errors", "Unsupported OAuth provider: %{provider}.", + provider: params["provider"] + ) + else + dgettext("errors", "Bad OAuth request.") + end + + conn + |> put_flash(:error, message) + |> redirect(to: "/") + end + + def provider_callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do + params = callback_params(params) + messages = for e <- Map.get(failure, :errors, []), do: e.message + message = Enum.join(messages, "; ") + + conn + |> put_flash( + :error, + dgettext("errors", "Failed to authenticate: %{message}.", message: message) + ) + |> redirect(external: OAuthController.redirect_uri(conn, params["redirect_uri"])) + end + + def provider_callback(%Plug.Conn{} = conn, params) do + params = callback_params(params) + + with {:ok, registration} <- Authenticator.get_registration(conn) do + auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state)) + + case Repo.get_assoc(registration, :user) do + {:ok, user} -> + authorize_callback(conn, %{"authorization" => auth_attrs}, user: user) + + _ -> + registration_params = + Map.merge(auth_attrs, %{ + "nickname" => Registration.nickname(registration), + "email" => Registration.email(registration) + }) + + conn + |> put_session_registration_id(registration.id) + |> registration_details(%{authorization: registration_params}) + end + else + error -> + Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns])) + + conn + |> put_flash(:error, dgettext("errors", "Failed to set up user account.")) + |> redirect(external: OAuthController.redirect_uri(conn, params["redirect_uri"])) + end + end + + defp callback_params(%{"state" => state} = params) do + Map.merge(params, Jason.decode!(state)) + end + + def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do + render(conn, "register.html", %{ + client_id: auth_attrs["client_id"], + redirect_uri: auth_attrs["redirect_uri"], + state: auth_attrs["state"], + scopes: Scopes.fetch_scopes(auth_attrs, []), + nickname: auth_attrs["nickname"], + email: auth_attrs["email"] + }) + end + + def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do + with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), + %Registration{} = registration <- Repo.get(Registration, registration_id), + {_, {:ok, auth, _user}} <- + {:create_authorization, OAuthController.do_create_authorization(conn, params)}, + %User{} = user <- Repo.preload(auth, :user).user, + {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do + conn + |> put_session_registration_id(nil) + |> after_create_authorization(auth, params) + else + {:create_authorization, error} -> + {:register, handle_create_authorization_error(conn, error, params)} + + _ -> + {:register, :generic_error} + end + end + + def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do + with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), + %Registration{} = registration <- Repo.get(Registration, registration_id), + {:ok, user} <- Authenticator.create_from_registration(conn, registration) do + conn + |> put_session_registration_id(nil) + |> authorize_callback( + params, + user: user + ) + else + {:error, changeset} -> + message = + Enum.map(changeset.errors, fn {field, {error, _}} -> + "#{field} #{error}" + end) + |> Enum.join("; ") + + message = + String.replace( + message, + "ap_id has already been taken", + "nickname has already been taken" + ) + + conn + |> put_status(:forbidden) + |> put_flash(:error, "Error: #{message}.") + |> registration_details(params) + + _ -> + {:register, :generic_error} + end + end + + defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id) + + defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), + do: put_session(conn, :registration_id, registration_id) +end diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex index 741f57195..8087c6754 100644 --- a/lib/pleroma/web/o_auth/o_auth_controller.ex +++ b/lib/pleroma/web/o_auth/o_auth_controller.ex @@ -7,16 +7,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Helpers.AuthHelper alias Pleroma.Helpers.UriHelper - alias Pleroma.Maps alias Pleroma.MFA - alias Pleroma.Registration + alias Pleroma.Maps alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.Auth.Authenticator alias Pleroma.Web.ControllerHelper alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.MFAController alias Pleroma.Web.OAuth.MFAView alias Pleroma.Web.OAuth.OAuthView alias Pleroma.Web.OAuth.Scopes @@ -25,14 +23,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken alias Pleroma.Web.Plugs.RateLimiter - require Logger - if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) - plug( - Pleroma.Web.ApiSpec.CastAndValidate - when action not in [:prepare_request, :callback, :request, :register] - ) + plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:fetch_session) plug(:fetch_flash) @@ -42,7 +35,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug ]) - plug(RateLimiter, [name: :authentication] when action == :create_authorization) + plug(RateLimiter, name: :authentication) action_fallback(Pleroma.Web.OAuth.FallbackController) @@ -148,117 +141,6 @@ defmodule Pleroma.Web.OAuth.OAuthController do end end - def create_authorization(_, _, opts \\ []) - - def create_authorization(%Plug.Conn{assigns: %{user: %User{} = user}} = conn, params, []) do - create_authorization(conn, params, user: user) - end - - def create_authorization(%Plug.Conn{} = conn, %{authorization: _} = params, opts) do - with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]), - {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do - after_create_authorization(conn, auth, params) - else - error -> - handle_create_authorization_error(conn, error, params) - end - end - - def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ - "authorization" => %{"redirect_uri" => @oob_token_redirect_uri} - }) do - # Enforcing the view to reuse the template when calling from other controllers - conn - |> put_view(OAuthView) - |> render("oob_authorization_created.html", %{auth: auth}) - end - - def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ - "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs - }) do - app = Repo.preload(auth, :app).app - - # An extra safety measure before we redirect (also done in `do_create_authorization/2`) - if redirect_uri in String.split(app.redirect_uris) do - redirect_uri = redirect_uri(conn, redirect_uri) - url_params = %{code: auth.token} - url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"]) - url = UriHelper.modify_uri_params(redirect_uri, url_params) - redirect(conn, external: url) - else - conn - |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) - |> redirect(external: redirect_uri(conn, redirect_uri)) - end - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:error, scopes_issue}, - %{"authorization" => _} = params - ) - when scopes_issue in [:unsupported_scopes, :missing_scopes] do - # Per https://github.com/tootsuite/mastodon/blob/ - # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 - conn - |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes")) - |> put_status(:unauthorized) - |> authorize(params) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:account_status, :confirmation_pending}, - %{"authorization" => _} = params - ) do - conn - |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address")) - |> put_status(:forbidden) - |> authorize(params) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:mfa_required, user, auth, _}, - params - ) do - {:ok, token} = MFA.Token.create(user, auth) - - data = %{ - "mfa_token" => token.token, - "redirect_uri" => params["authorization"]["redirect_uri"], - "state" => params["authorization"]["state"] - } - - MFAController.show(conn, data) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:account_status, :password_reset_pending}, - %{"authorization" => _} = params - ) do - conn - |> put_flash(:error, dgettext("errors", "Password reset is required")) - |> put_status(:forbidden) - |> authorize(params) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:account_status, :deactivated}, - %{"authorization" => _} = params - ) do - conn - |> put_flash(:error, dgettext("errors", "Your account is currently disabled")) - |> put_status(:forbidden) - |> authorize(params) - end - - defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do - Authenticator.handle_error(conn, error) - end - @doc "Renew access_token with refresh_token" def token_exchange( %Plug.Conn{} = conn, @@ -325,7 +207,49 @@ defmodule Pleroma.Web.OAuth.OAuthController do end # Bad request - def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) + def token_exchange(%Plug.Conn{} = _conn, _params), do: {:error, :bad_request} + + # Note: intended to be a private function but opened for AccountController that logs in on signup + @doc "If checks pass, creates authorization and token for given user, app and requested scopes." + def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do + with {:ok, auth} <- do_create_authorization(user, app, requested_scopes), + {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, + {:ok, token} <- Token.exchange_token(app, auth) do + {:ok, token} + end + end + + def do_create_authorization(conn, auth_attrs, user \\ nil) + + def do_create_authorization( + %Plug.Conn{} = conn, + %{ + "authorization" => + %{ + "client_id" => client_id, + "redirect_uri" => redirect_uri + } = auth_attrs + }, + user + ) do + with {_, {:ok, %User{} = user}} <- + {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, + %App{} = app <- Repo.get_by(App, client_id: client_id), + true <- redirect_uri in String.split(app.redirect_uris), + requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes), + {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do + {:ok, auth, user} + end + end + + def do_create_authorization(%User{} = user, %App{} = app, requested_scopes) + when is_list(requested_scopes) do + with {:account_status, :active} <- {:account_status, User.account_status(user)}, + {:ok, scopes} <- validate_scopes(app, requested_scopes), + {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do + {:ok, auth} + end + end def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do conn @@ -386,6 +310,16 @@ defmodule Pleroma.Web.OAuth.OAuthController do render_invalid_credentials_error(conn) end + defp render_invalid_credentials_error(conn) do + render_error(conn, :bad_request, "Invalid credentials") + end + + defp build_and_response_mfa_token(user, auth) do + with {:ok, token} <- MFA.Token.create(user, auth) do + MFAView.render("mfa_response.json", %{token: token, user: user}) + end + end + def token_revoke(%Plug.Conn{} = conn, %{token: token}) do with {:ok, %Token{} = oauth_token} <- Token.get_by_token(token), {:ok, oauth_token} <- RevokeToken.revoke(oauth_token) do @@ -405,223 +339,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do end end - def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params) - - # Response for bad request - defp bad_request(%Plug.Conn{} = conn, _) do - render_error(conn, :internal_server_error, "Bad request") - end - - @doc "Prepares OAuth request to provider for Ueberauth" - def prepare_request(%Plug.Conn{} = conn, %{ - "provider" => provider, - "authorization" => auth_attrs - }) do - scope = - auth_attrs - |> Scopes.fetch_scopes([]) - |> Scopes.to_string() - - state = - auth_attrs - |> Map.delete("scopes") - |> Map.put("scope", scope) - |> Jason.encode!() - - params = - auth_attrs - |> Map.drop(~w(scope scopes client_id redirect_uri)) - |> Map.put("state", state) - - # Handing the request to Ueberauth - redirect(conn, to: o_auth_path(conn, :request, provider, params)) - end - - def request(%Plug.Conn{} = conn, params) do - message = - if params["provider"] do - dgettext("errors", "Unsupported OAuth provider: %{provider}.", - provider: params["provider"] - ) - else - dgettext("errors", "Bad OAuth request.") - end - - conn - |> put_flash(:error, message) - |> redirect(to: "/") - end - - def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do - params = callback_params(params) - messages = for e <- Map.get(failure, :errors, []), do: e.message - message = Enum.join(messages, "; ") - - conn - |> put_flash( - :error, - dgettext("errors", "Failed to authenticate: %{message}.", message: message) - ) - |> redirect(external: redirect_uri(conn, params["redirect_uri"])) - end - - def callback(%Plug.Conn{} = conn, params) do - params = callback_params(params) - - with {:ok, registration} <- Authenticator.get_registration(conn) do - auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state)) - - case Repo.get_assoc(registration, :user) do - {:ok, user} -> - create_authorization(conn, %{"authorization" => auth_attrs}, user: user) - - _ -> - registration_params = - Map.merge(auth_attrs, %{ - "nickname" => Registration.nickname(registration), - "email" => Registration.email(registration) - }) - - conn - |> put_session_registration_id(registration.id) - |> registration_details(%{authorization: registration_params}) - end - else - error -> - Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns])) - - conn - |> put_flash(:error, dgettext("errors", "Failed to set up user account.")) - |> redirect(external: redirect_uri(conn, params["redirect_uri"])) - end - end - - defp callback_params(%{"state" => state} = params) do - Map.merge(params, Jason.decode!(state)) - end - - def registration_details(%Plug.Conn{} = conn, %{authorization: auth_attrs}) do - render(conn, "register.html", %{ - client_id: auth_attrs["client_id"], - redirect_uri: auth_attrs["redirect_uri"], - state: auth_attrs["state"], - scopes: Scopes.fetch_scopes(auth_attrs, []), - nickname: auth_attrs["nickname"], - email: auth_attrs["email"] - }) - end - - def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do - with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), - %Registration{} = registration <- Repo.get(Registration, registration_id), - {_, {:ok, auth, _user}} <- - {:create_authorization, do_create_authorization(conn, params)}, - %User{} = user <- Repo.preload(auth, :user).user, - {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do - conn - |> put_session_registration_id(nil) - |> after_create_authorization(auth, params) - else - {:create_authorization, error} -> - {:register, handle_create_authorization_error(conn, error, params)} - - _ -> - {:register, :generic_error} - end - end - - def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do - with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), - %Registration{} = registration <- Repo.get(Registration, registration_id), - {:ok, user} <- Authenticator.create_from_registration(conn, registration) do - conn - |> put_session_registration_id(nil) - |> create_authorization( - params, - user: user - ) - else - {:error, changeset} -> - message = - Enum.map(changeset.errors, fn {field, {error, _}} -> - "#{field} #{error}" - end) - |> Enum.join("; ") - - message = - String.replace( - message, - "ap_id has already been taken", - "nickname has already been taken" - ) - - conn - |> put_status(:forbidden) - |> put_flash(:error, "Error: #{message}.") - |> registration_details(params) - - _ -> - {:register, :generic_error} - end - end - - defp do_create_authorization(conn, auth_attrs, user \\ nil) - - defp do_create_authorization( - %Plug.Conn{} = conn, - %{ - "authorization" => - %{ - "client_id" => client_id, - "redirect_uri" => redirect_uri - } = auth_attrs - }, - user - ) do - with {_, {:ok, %User{} = user}} <- - {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, - %App{} = app <- Repo.get_by(App, client_id: client_id), - true <- redirect_uri in String.split(app.redirect_uris), - requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes), - {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do - {:ok, auth, user} - end - end - - defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes) - when is_list(requested_scopes) do - with {:account_status, :active} <- {:account_status, User.account_status(user)}, - {:ok, scopes} <- validate_scopes(app, requested_scopes), - {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do - {:ok, auth} - end - end - - # Note: intended to be a private function but opened for AccountController that logs in on signup - @doc "If checks pass, creates authorization and token for given user, app and requested scopes." - def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do - with {:ok, auth} <- do_create_authorization(user, app, requested_scopes), - {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, - {:ok, token} <- Token.exchange_token(app, auth) do - {:ok, token} - end - end + def token_revoke(%Plug.Conn{} = _conn, _params), do: {:error, :bad_request} # Special case: Local MastodonFE - defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login) + def redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login) - defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri - - defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id) - - defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), - do: put_session(conn, :registration_id, registration_id) - - defp build_and_response_mfa_token(user, auth) do - with {:ok, token} <- MFA.Token.create(user, auth) do - MFAView.render("mfa_response.json", %{token: token, user: user}) - end - end + def redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri @spec validate_scopes(App.t(), map() | list()) :: {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} @@ -639,8 +362,4 @@ defmodule Pleroma.Web.OAuth.OAuthController do |> String.split() |> Enum.at(0) end - - defp render_invalid_credentials_error(conn) do - render_error(conn, :bad_request, "Invalid credentials") - end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 34df3f365..376c33ee1 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -40,6 +40,10 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Web.Plugs.OAuthPlug) plug(Pleroma.Web.Plugs.UserEnabledPlug) plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug) + end + + pipeline :oauth_api do + plug(:oauth) plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec) end @@ -337,16 +341,21 @@ defmodule Pleroma.Web.Router do scope "/oauth", Pleroma.Web.OAuth do # Note: use /api/v1/accounts/verify_credentials for userinfo of signed-in user - get("/registration_details", OAuthController, :registration_details) + get("/registration_details", OAuthBrowserController, :registration_details) post("/mfa/verify", MFAController, :verify, as: :mfa_verify) get("/mfa", MFAController, :show) scope [] do - pipe_through(:oauth) + pipe_through(:oauth_api) get("/authorize", OAuthController, :authorize) - post("/authorize", OAuthController, :create_authorization) + end + + scope [] do + pipe_through(:oauth) + + post("/authorize_callback", OAuthBrowserController, :authorize_callback) end scope [] do @@ -360,10 +369,10 @@ defmodule Pleroma.Web.Router do scope [] do pipe_through(:browser) - get("/prepare_request", OAuthController, :prepare_request) - get("/:provider", OAuthController, :request) - get("/:provider/callback", OAuthController, :callback) - post("/register", OAuthController, :register) + get("/prepare_request", OAuthBrowserController, :prepare_request) + get("/:provider", OAuthBrowserController, :provider_request) + get("/:provider/callback", OAuthBrowserController, :provider_callback) + post("/register", OAuthBrowserController, :register) end end diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex index 1a85818ec..5a063a923 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -5,7 +5,7 @@ <% end %> -<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %> +<%= form_for @conn, o_auth_path(@conn, :authorize_callback), [as: "authorization"], fn f -> %> <%= if @user do %>