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 %>
diff --git a/test/pleroma/web/o_auth/ldap_authorization_test.exs b/test/pleroma/web/o_auth/ldap_authorization_test.exs index a839ed4a7..ae5ffcf41 100644 --- a/test/pleroma/web/o_auth/ldap_authorization_test.exs +++ b/test/pleroma/web/o_auth/ldap_authorization_test.exs @@ -37,13 +37,17 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do ] do conn = build_conn() - |> post("/oauth/token?#{URI.encode_query(%{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - })}") + |> post( + "/oauth/token?#{ + URI.encode_query(%{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + }" + ) assert %{"access_token" => token} = json_response_and_validate_schema(conn, 200) @@ -81,13 +85,17 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do ] do conn = build_conn() - |> post("/oauth/token?#{URI.encode_query(%{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - })}") + |> post( + "/oauth/token?#{ + URI.encode_query(%{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + }" + ) assert %{"access_token" => token} = json_response_and_validate_schema(conn, 200) @@ -120,13 +128,17 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do ] do conn = build_conn() - |> post("/oauth/token?#{URI.encode_query(%{ - "grant_type" => "password", - "username" => user.nickname, - "password" => password, - "client_id" => app.client_id, - "client_secret" => app.client_secret - })}") + |> post( + "/oauth/token?#{ + URI.encode_query(%{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + }" + ) assert %{"error" => "Invalid credentials"} = json_response_and_validate_schema(conn, 400) assert_received :close_connection diff --git a/test/pleroma/web/o_auth/o_auth_browser_controller_test.exs b/test/pleroma/web/o_auth/o_auth_browser_controller_test.exs new file mode 100644 index 000000000..278af5d95 --- /dev/null +++ b/test/pleroma/web/o_auth/o_auth_browser_controller_test.exs @@ -0,0 +1,576 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.OAuthBrowserControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Helpers.AuthHelper + alias Pleroma.MFA + alias Pleroma.MFA.TOTP + alias Pleroma.Repo + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.OAuthController + + @session_opts [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + setup do + clear_config([:instance, :account_activation_required]) + clear_config([:instance, :account_approval_required]) + end + + describe "in OAuth consumer mode, " do + setup do + [ + app: insert(:oauth_app), + conn: + build_conn() + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + ] + end + + setup do: clear_config([:auth, :oauth_consumer_strategies], ~w(twitter facebook)) + + test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{ + app: app, + conn: conn + } do + conn = + get( + conn, + "/oauth/prepare_request", + %{ + "provider" => "twitter", + "authorization" => %{ + "scope" => "read follow", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "a_state" + } + } + ) + + assert html_response(conn, 302) + + redirect_query = URI.parse(redirected_to(conn)).query + assert %{"state" => state_param} = URI.decode_query(redirect_query) + assert {:ok, state_components} = Jason.decode(state_param) + + expected_client_id = app.client_id + expected_redirect_uri = app.redirect_uris + + assert %{ + "scope" => "read follow", + "client_id" => ^expected_client_id, + "redirect_uri" => ^expected_redirect_uri, + "state" => "a_state" + } = state_components + end + + test "with user-bound registration, GET /oauth//callback redirects to `redirect_uri` with `code`", + %{app: app, conn: conn} do + registration = insert(:registration) + redirect_uri = OAuthController.default_redirect_uri(app) + + state_params = %{ + "scope" => Enum.join(app.scopes, " "), + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "" + } + + conn = + conn + |> assign(:ueberauth_auth, %{provider: registration.provider, uid: registration.uid}) + |> get( + "/oauth/twitter/callback", + %{ + "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", + "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", + "provider" => "twitter", + "state" => Jason.encode!(state_params) + } + ) + + assert html_response(conn, 302) + assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ + end + + test "with user-unbound registration, GET /oauth//callback renders registration_details page", + %{app: app, conn: conn} do + user = insert(:user) + + state_params = %{ + "scope" => "read write", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "a_state" + } + + conn = + conn + |> assign(:ueberauth_auth, %{ + provider: "twitter", + uid: "171799000", + info: %{nickname: user.nickname, email: user.email, name: user.name, description: nil} + }) + |> get( + "/oauth/twitter/callback", + %{ + "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", + "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", + "provider" => "twitter", + "state" => Jason.encode!(state_params) + } + ) + + assert response = html_response(conn, 200) + assert response =~ ~r/name="op" type="submit" value="register"/ + assert response =~ ~r/name="op" type="submit" value="connect"/ + assert response =~ user.email + assert response =~ user.nickname + end + + test "on authentication error, GET /oauth//callback redirects to `redirect_uri`", %{ + app: app, + conn: conn + } do + state_params = %{ + "scope" => Enum.join(app.scopes, " "), + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "" + } + + conn = + conn + |> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]}) + |> get( + "/oauth/twitter/callback", + %{ + "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", + "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", + "provider" => "twitter", + "state" => Jason.encode!(state_params) + } + ) + + assert html_response(conn, 302) + assert redirected_to(conn) == app.redirect_uris + assert get_flash(conn, :error) == "Failed to authenticate: (error description)." + end + + test "GET /oauth/registration_details renders registration details form", %{ + app: app, + conn: conn + } do + conn = + get( + conn, + "/oauth/registration_details", + %{ + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "a_state", + "nickname" => nil, + "email" => "john@doe.com" + } + } + ) + + assert response = html_response(conn, 200) + assert response =~ ~r/name="op" type="submit" value="register"/ + assert response =~ ~r/name="op" type="submit" value="connect"/ + end + + test "with valid params, POST /oauth/register?op=register redirects to `redirect_uri` with `code`", + %{ + app: app, + conn: conn + } do + registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) + redirect_uri = OAuthController.default_redirect_uri(app) + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post( + "/oauth/register", + %{ + "op" => "register", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "a_state", + "nickname" => "availablenick", + "email" => "available@email.com" + } + } + ) + + assert html_response(conn, 302) + assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ + end + + test "with unlisted `redirect_uri`, POST /oauth/register?op=register results in HTTP 401", + %{ + app: app, + conn: conn + } do + registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) + unlisted_redirect_uri = "http://cross-site-request.com" + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post( + "/oauth/register", + %{ + "op" => "register", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => unlisted_redirect_uri, + "state" => "a_state", + "nickname" => "availablenick", + "email" => "available@email.com" + } + } + ) + + assert html_response(conn, 401) + end + + test "with invalid params, POST /oauth/register?op=register renders registration_details page", + %{ + app: app, + conn: conn + } do + another_user = insert(:user) + registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) + + params = %{ + "op" => "register", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "a_state", + "nickname" => "availablenickname", + "email" => "available@email.com" + } + } + + for {bad_param, bad_param_value} <- + [{"nickname", another_user.nickname}, {"email", another_user.email}] do + bad_registration_attrs = %{ + "authorization" => Map.put(params["authorization"], bad_param, bad_param_value) + } + + bad_params = Map.merge(params, bad_registration_attrs) + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post("/oauth/register", bad_params) + + assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/ + assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken." + end + end + + test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`", + %{ + app: app, + conn: conn + } do + user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("testpassword")) + registration = insert(:registration, user: nil) + redirect_uri = OAuthController.default_redirect_uri(app) + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post( + "/oauth/register", + %{ + "op" => "connect", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "a_state", + "name" => user.nickname, + "password" => "testpassword" + } + } + ) + + assert html_response(conn, 302) + assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ + end + + test "with unlisted `redirect_uri`, POST /oauth/register?op=connect results in HTTP 401`", + %{ + app: app, + conn: conn + } do + user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("testpassword")) + registration = insert(:registration, user: nil) + unlisted_redirect_uri = "http://cross-site-request.com" + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post( + "/oauth/register", + %{ + "op" => "connect", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => unlisted_redirect_uri, + "state" => "a_state", + "name" => user.nickname, + "password" => "testpassword" + } + } + ) + + assert html_response(conn, 401) + end + + test "with invalid params, POST /oauth/register?op=connect renders registration_details page", + %{ + app: app, + conn: conn + } do + user = insert(:user) + registration = insert(:registration, user: nil) + + params = %{ + "op" => "connect", + "authorization" => %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "a_state", + "name" => user.nickname, + "password" => "wrong password" + } + } + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post("/oauth/register", params) + + assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/ + assert get_flash(conn, :error) == "Invalid Username/Password" + end + end + + describe "POST /oauth/authorize_callback" do + test "redirects with oauth authorization, " <> + "granting requested app-supported scopes to both admin- and non-admin users" do + app_scopes = ["read", "write", "admin", "secret_scope"] + app = insert(:oauth_app, scopes: app_scopes) + redirect_uri = OAuthController.default_redirect_uri(app) + + non_admin = insert(:user, is_admin: false) + admin = insert(:user, is_admin: true) + scopes_subset = ["read:subscope", "write", "admin"] + + # In case scope param is missing, expecting _all_ app-supported scopes to be granted + for user <- [non_admin, admin], + {requested_scopes, expected_scopes} <- + %{scopes_subset => scopes_subset, nil: app_scopes} do + conn = + post( + build_conn(), + "/oauth/authorize_callback", + %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "scope" => requested_scopes, + "state" => "statepassed" + } + } + ) + + target = redirected_to(conn) + assert target =~ redirect_uri + + query = URI.parse(target).query |> URI.query_decoder() |> Map.new() + + assert %{"state" => "statepassed", "code" => code} = query + auth = Repo.get_by(Authorization, token: code) + assert auth + assert auth.scopes == expected_scopes + end + end + + test "authorize from cookie" do + user = insert(:user) + app = insert(:oauth_app) + oauth_token = insert(:oauth_token, user: user, app: app) + redirect_uri = OAuthController.default_redirect_uri(app) + + conn = + build_conn() + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + |> AuthHelper.put_session_token(oauth_token.token) + |> post( + "/oauth/authorize_callback", + %{ + "authorization" => %{ + "name" => user.nickname, + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "scope" => app.scopes, + "state" => "statepassed" + } + } + ) + + target = redirected_to(conn) + assert target =~ redirect_uri + + query = URI.parse(target).query |> URI.query_decoder() |> Map.new() + + assert %{"state" => "statepassed", "code" => code} = query + auth = Repo.get_by(Authorization, token: code) + assert auth + assert auth.scopes == app.scopes + end + + test "redirect to on two-factor auth page" do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + app = insert(:oauth_app, scopes: ["read", "write", "follow"]) + + conn = + build_conn() + |> post("/oauth/authorize_callback", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "scope" => "read write", + "state" => "statepassed" + } + }) + + result = html_response(conn, 200) + + mfa_token = Repo.get_by(MFA.Token, user_id: user.id) + assert result =~ app.redirect_uris + assert result =~ "statepassed" + assert result =~ mfa_token.token + assert result =~ "Two-factor authentication" + end + + test "returns 401 for wrong credentials", %{conn: conn} do + user = insert(:user) + app = insert(:oauth_app) + redirect_uri = OAuthController.default_redirect_uri(app) + + result = + conn + |> post("/oauth/authorize_callback", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "wrong", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "statepassed", + "scope" => Enum.join(app.scopes, " ") + } + }) + |> html_response(:unauthorized) + + # Keep the details + assert result =~ app.client_id + assert result =~ redirect_uri + + # Error message + assert result =~ "Invalid Username/Password" + end + + test "returns 401 for missing scopes" do + user = insert(:user, is_admin: false) + app = insert(:oauth_app, scopes: ["read", "write", "admin"]) + redirect_uri = OAuthController.default_redirect_uri(app) + + result = + build_conn() + |> post("/oauth/authorize_callback", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "statepassed", + "scope" => "" + } + }) + |> html_response(:unauthorized) + + # Keep the details + assert result =~ app.client_id + assert result =~ redirect_uri + + # Error message + assert result =~ "This action is outside the authorized scopes" + end + + test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + redirect_uri = OAuthController.default_redirect_uri(app) + + result = + conn + |> post("/oauth/authorize_callback", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "statepassed", + "scope" => "read write follow" + } + }) + |> html_response(:unauthorized) + + # Keep the details + assert result =~ app.client_id + assert result =~ redirect_uri + + # Error message + assert result =~ "This action is outside the authorized scopes" + end + end +end diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs index c0f243a75..4e51edb63 100644 --- a/test/pleroma/web/o_auth/o_auth_controller_test.exs +++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs @@ -59,708 +59,6 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do assert response =~ "Sign in with Twitter" assert response =~ o_auth_path(conn, :prepare_request) end - - test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{ - app: app, - conn: conn - } do - conn = - get( - conn, - "/oauth/prepare_request", - %{ - "provider" => "twitter", - "authorization" => %{ - "scope" => "read follow", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "a_state" - } - } - ) - - assert html_response(conn, 302) - - redirect_query = URI.parse(redirected_to(conn)).query - assert %{"state" => state_param} = URI.decode_query(redirect_query) - assert {:ok, state_components} = Jason.decode(state_param) - - expected_client_id = app.client_id - expected_redirect_uri = app.redirect_uris - - assert %{ - "scope" => "read follow", - "client_id" => ^expected_client_id, - "redirect_uri" => ^expected_redirect_uri, - "state" => "a_state" - } = state_components - end - - test "with user-bound registration, GET /oauth//callback redirects to `redirect_uri` with `code`", - %{app: app, conn: conn} do - registration = insert(:registration) - redirect_uri = OAuthController.default_redirect_uri(app) - - state_params = %{ - "scope" => Enum.join(app.scopes, " "), - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "" - } - - conn = - conn - |> assign(:ueberauth_auth, %{provider: registration.provider, uid: registration.uid}) - |> get( - "/oauth/twitter/callback", - %{ - "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", - "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", - "provider" => "twitter", - "state" => Jason.encode!(state_params) - } - ) - - assert html_response(conn, 302) - assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ - end - - test "with user-unbound registration, GET /oauth//callback renders registration_details page", - %{app: app, conn: conn} do - user = insert(:user) - - state_params = %{ - "scope" => "read write", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "a_state" - } - - conn = - conn - |> assign(:ueberauth_auth, %{ - provider: "twitter", - uid: "171799000", - info: %{nickname: user.nickname, email: user.email, name: user.name, description: nil} - }) - |> get( - "/oauth/twitter/callback", - %{ - "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", - "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", - "provider" => "twitter", - "state" => Jason.encode!(state_params) - } - ) - - assert response = html_response(conn, 200) - assert response =~ ~r/name="op" type="submit" value="register"/ - assert response =~ ~r/name="op" type="submit" value="connect"/ - assert response =~ user.email - assert response =~ user.nickname - end - - test "on authentication error, GET /oauth//callback redirects to `redirect_uri`", %{ - app: app, - conn: conn - } do - state_params = %{ - "scope" => Enum.join(app.scopes, " "), - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "" - } - - conn = - conn - |> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]}) - |> get( - "/oauth/twitter/callback", - %{ - "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", - "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", - "provider" => "twitter", - "state" => Jason.encode!(state_params) - } - ) - - assert html_response(conn, 302) - assert redirected_to(conn) == app.redirect_uris - assert get_flash(conn, :error) == "Failed to authenticate: (error description)." - end - - test "GET /oauth/registration_details renders registration details form", %{ - app: app, - conn: conn - } do - conn = - get( - conn, - "/oauth/registration_details", - %{ - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "a_state", - "nickname" => nil, - "email" => "john@doe.com" - } - } - ) - - assert response = html_response(conn, 200) - assert response =~ ~r/name="op" type="submit" value="register"/ - assert response =~ ~r/name="op" type="submit" value="connect"/ - end - - test "with valid params, POST /oauth/register?op=register redirects to `redirect_uri` with `code`", - %{ - app: app, - conn: conn - } do - registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) - redirect_uri = OAuthController.default_redirect_uri(app) - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post( - "/oauth/register", - %{ - "op" => "register", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "a_state", - "nickname" => "availablenick", - "email" => "available@email.com" - } - } - ) - - assert html_response(conn, 302) - assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ - end - - test "with unlisted `redirect_uri`, POST /oauth/register?op=register results in HTTP 401", - %{ - app: app, - conn: conn - } do - registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) - unlisted_redirect_uri = "http://cross-site-request.com" - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post( - "/oauth/register", - %{ - "op" => "register", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => unlisted_redirect_uri, - "state" => "a_state", - "nickname" => "availablenick", - "email" => "available@email.com" - } - } - ) - - assert html_response(conn, 401) - end - - test "with invalid params, POST /oauth/register?op=register renders registration_details page", - %{ - app: app, - conn: conn - } do - another_user = insert(:user) - registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) - - params = %{ - "op" => "register", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "a_state", - "nickname" => "availablenickname", - "email" => "available@email.com" - } - } - - for {bad_param, bad_param_value} <- - [{"nickname", another_user.nickname}, {"email", another_user.email}] do - bad_registration_attrs = %{ - "authorization" => Map.put(params["authorization"], bad_param, bad_param_value) - } - - bad_params = Map.merge(params, bad_registration_attrs) - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post("/oauth/register", bad_params) - - assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/ - assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken." - end - end - - test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`", - %{ - app: app, - conn: conn - } do - user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("testpassword")) - registration = insert(:registration, user: nil) - redirect_uri = OAuthController.default_redirect_uri(app) - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post( - "/oauth/register", - %{ - "op" => "connect", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "a_state", - "name" => user.nickname, - "password" => "testpassword" - } - } - ) - - assert html_response(conn, 302) - assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/ - end - - test "with unlisted `redirect_uri`, POST /oauth/register?op=connect results in HTTP 401`", - %{ - app: app, - conn: conn - } do - user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("testpassword")) - registration = insert(:registration, user: nil) - unlisted_redirect_uri = "http://cross-site-request.com" - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post( - "/oauth/register", - %{ - "op" => "connect", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => unlisted_redirect_uri, - "state" => "a_state", - "name" => user.nickname, - "password" => "testpassword" - } - } - ) - - assert html_response(conn, 401) - end - - test "with invalid params, POST /oauth/register?op=connect renders registration_details page", - %{ - app: app, - conn: conn - } do - user = insert(:user) - registration = insert(:registration, user: nil) - - params = %{ - "op" => "connect", - "authorization" => %{ - "scopes" => app.scopes, - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "a_state", - "name" => user.nickname, - "password" => "wrong password" - } - } - - conn = - conn - |> put_session(:registration_id, registration.id) - |> post("/oauth/register", params) - - assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/ - assert get_flash(conn, :error) == "Invalid Username/Password" - end - end - - describe "GET /oauth/authorize" do - setup do - [ - app: insert(:oauth_app, redirect_uris: "https://redirect.url"), - conn: - build_conn() - |> Plug.Session.call(Plug.Session.init(@session_opts)) - |> fetch_session() - ] - end - - test "renders authentication page", %{app: app, conn: conn} do - conn = - get( - conn, - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "scope" => "read" - } - ) - - assert html_response(conn, 200) =~ ~s(type="submit") - end - - test "properly handles internal calls with `authorization`-wrapped params", %{ - app: app, - conn: conn - } do - conn = - get( - conn, - "/oauth/authorize", - %{ - "authorization" => %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "scope" => "read" - } - } - ) - - assert html_response(conn, 200) =~ ~s(type="submit") - end - - test "renders authentication page if user is already authenticated but `force_login` is tru-ish", - %{app: app, conn: conn} do - token = insert(:oauth_token, app: app) - - conn = - conn - |> AuthHelper.put_session_token(token.token) - |> get( - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "scope" => "read", - "force_login" => "true" - } - ) - - assert html_response(conn, 200) =~ ~s(type="submit") - end - - test "renders authentication page if user is already authenticated but user request with another client", - %{ - app: app, - conn: conn - } do - token = insert(:oauth_token, app: app) - - conn = - conn - |> AuthHelper.put_session_token(token.token) - |> get( - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => "another_client_id", - "redirect_uri" => OAuthController.default_redirect_uri(app), - "scope" => "read" - } - ) - - assert html_response(conn, 200) =~ ~s(type="submit") - end - - test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params", - %{ - app: app, - conn: conn - } do - token = insert(:oauth_token, app: app) - - conn = - conn - |> AuthHelper.put_session_token(token.token) - |> get( - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => OAuthController.default_redirect_uri(app), - "state" => "specific_client_state", - "scope" => "read" - } - ) - - assert URI.decode(redirected_to(conn)) == - "https://redirect.url?access_token=#{token.token}&state=specific_client_state" - end - - test "with existing authentication and unlisted non-OOB `redirect_uri`, redirects without credentials", - %{ - app: app, - conn: conn - } do - unlisted_redirect_uri = "http://cross-site-request.com" - token = insert(:oauth_token, app: app) - - conn = - conn - |> AuthHelper.put_session_token(token.token) - |> get( - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => unlisted_redirect_uri, - "state" => "specific_client_state", - "scope" => "read" - } - ) - - assert redirected_to(conn) == unlisted_redirect_uri - end - - test "with existing authentication and OOB `redirect_uri`, redirects to app with `token` and `state` params", - %{ - app: app, - conn: conn - } do - token = insert(:oauth_token, app: app) - - conn = - conn - |> AuthHelper.put_session_token(token.token) - |> get( - "/oauth/authorize", - %{ - "response_type" => "code", - "client_id" => app.client_id, - "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", - "scope" => "read" - } - ) - - assert html_response(conn, 200) =~ "Authorization exists" - end - end - - describe "POST /oauth/authorize" do - test "redirects with oauth authorization, " <> - "granting requested app-supported scopes to both admin- and non-admin users" do - app_scopes = ["read", "write", "admin", "secret_scope"] - app = insert(:oauth_app, scopes: app_scopes) - redirect_uri = OAuthController.default_redirect_uri(app) - - non_admin = insert(:user, is_admin: false) - admin = insert(:user, is_admin: true) - scopes_subset = ["read:subscope", "write", "admin"] - - # In case scope param is missing, expecting _all_ app-supported scopes to be granted - for user <- [non_admin, admin], - {requested_scopes, expected_scopes} <- - %{scopes_subset => scopes_subset, nil: app_scopes} do - conn = - post( - build_conn(), - "/oauth/authorize", - %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "scope" => requested_scopes, - "state" => "statepassed" - } - } - ) - - target = redirected_to(conn) - assert target =~ redirect_uri - - query = URI.parse(target).query |> URI.query_decoder() |> Map.new() - - assert %{"state" => "statepassed", "code" => code} = query - auth = Repo.get_by(Authorization, token: code) - assert auth - assert auth.scopes == expected_scopes - end - end - - test "authorize from cookie" do - user = insert(:user) - app = insert(:oauth_app) - oauth_token = insert(:oauth_token, user: user, app: app) - redirect_uri = OAuthController.default_redirect_uri(app) - - conn = - build_conn() - |> Plug.Session.call(Plug.Session.init(@session_opts)) - |> fetch_session() - |> AuthHelper.put_session_token(oauth_token.token) - |> post( - "/oauth/authorize", - %{ - "authorization" => %{ - "name" => user.nickname, - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "scope" => app.scopes, - "state" => "statepassed" - } - } - ) - - target = redirected_to(conn) - assert target =~ redirect_uri - - query = URI.parse(target).query |> URI.query_decoder() |> Map.new() - - assert %{"state" => "statepassed", "code" => code} = query - auth = Repo.get_by(Authorization, token: code) - assert auth - assert auth.scopes == app.scopes - end - - test "redirect to on two-factor auth page" do - otp_secret = TOTP.generate_secret() - - user = - insert(:user, - multi_factor_authentication_settings: %MFA.Settings{ - enabled: true, - totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} - } - ) - - app = insert(:oauth_app, scopes: ["read", "write", "follow"]) - - conn = - build_conn() - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => app.redirect_uris, - "scope" => "read write", - "state" => "statepassed" - } - }) - - result = html_response(conn, 200) - - mfa_token = Repo.get_by(MFA.Token, user_id: user.id) - assert result =~ app.redirect_uris - assert result =~ "statepassed" - assert result =~ mfa_token.token - assert result =~ "Two-factor authentication" - end - - test "returns 401 for wrong credentials", %{conn: conn} do - user = insert(:user) - app = insert(:oauth_app) - redirect_uri = OAuthController.default_redirect_uri(app) - - result = - conn - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "wrong", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "statepassed", - "scope" => Enum.join(app.scopes, " ") - } - }) - |> html_response(:unauthorized) - - # Keep the details - assert result =~ app.client_id - assert result =~ redirect_uri - - # Error message - assert result =~ "Invalid Username/Password" - end - - test "returns 401 for missing scopes" do - user = insert(:user, is_admin: false) - app = insert(:oauth_app, scopes: ["read", "write", "admin"]) - redirect_uri = OAuthController.default_redirect_uri(app) - - result = - build_conn() - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "statepassed", - "scope" => "" - } - }) - |> html_response(:unauthorized) - - # Keep the details - assert result =~ app.client_id - assert result =~ redirect_uri - - # Error message - assert result =~ "This action is outside the authorized scopes" - end - - test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do - user = insert(:user) - app = insert(:oauth_app, scopes: ["read", "write"]) - redirect_uri = OAuthController.default_redirect_uri(app) - - result = - conn - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "statepassed", - "scope" => "read write follow" - } - }) - |> html_response(:unauthorized) - - # Keep the details - assert result =~ app.client_id - assert result =~ redirect_uri - - # Error message - assert result =~ "This action is outside the authorized scopes" - end end describe "POST /oauth/token" do @@ -1136,12 +434,16 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do response = build_conn() - |> post("/oauth/token", %{ - "grant_type" => "refresh_token", - "refresh_token" => token.refresh_token, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) + |> post( + "/oauth/token?#{ + URI.encode_query(%{ + "grant_type" => "refresh_token", + "refresh_token" => token.refresh_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + }" + ) |> json_response_and_validate_schema(200) ap_id = user.ap_id @@ -1174,12 +476,16 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do response = build_conn() - |> post("/oauth/token", %{ - "grant_type" => "refresh_token", - "refresh_token" => token.token, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) + |> post( + "/oauth/token?#{ + URI.encode_query(%{ + "grant_type" => "refresh_token", + "refresh_token" => token.token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + }" + ) |> json_response_and_validate_schema(400) assert %{"error" => "Invalid credentials"} == response @@ -1190,12 +496,16 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do response = build_conn() - |> post("/oauth/token", %{ - "grant_type" => "refresh_token", - "refresh_token" => "token.refresh_token", - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) + |> post( + "/oauth/token?#{ + URI.encode_query(%{ + "grant_type" => "refresh_token", + "refresh_token" => "token.refresh_token", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + }" + ) |> json_response_and_validate_schema(400) assert %{"error" => "Invalid credentials"} == response @@ -1218,12 +528,16 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do response = build_conn() - |> post("/oauth/token", %{ - "grant_type" => "refresh_token", - "refresh_token" => access_token.refresh_token, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) + |> post( + "/oauth/token?#{ + URI.encode_query(%{ + "grant_type" => "refresh_token", + "refresh_token" => access_token.refresh_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + }" + ) |> json_response_and_validate_schema(200) ap_id = user.ap_id @@ -1304,4 +618,170 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do assert %{"error" => "Bad request"} == response end end + + describe "GET /oauth/authorize" do + setup do + [ + app: insert(:oauth_app, redirect_uris: "https://redirect.url"), + conn: + build_conn() + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + ] + end + + test "renders authentication page", %{app: app, conn: conn} do + conn = + get( + conn, + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "scope" => "read" + } + ) + + assert html_response(conn, 200) =~ ~s(type="submit") + end + + test "properly handles internal calls with `authorization`-wrapped params", %{ + app: app, + conn: conn + } do + conn = + get( + conn, + "/oauth/authorize", + %{ + "authorization" => %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "scope" => "read" + } + } + ) + + assert html_response(conn, 200) =~ ~s(type="submit") + end + + test "renders authentication page if user is already authenticated but `force_login` is tru-ish", + %{app: app, conn: conn} do + token = insert(:oauth_token, app: app) + + conn = + conn + |> AuthHelper.put_session_token(token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "scope" => "read", + "force_login" => "true" + } + ) + + assert html_response(conn, 200) =~ ~s(type="submit") + end + + test "renders authentication page if user is already authenticated but user request with another client", + %{ + app: app, + conn: conn + } do + token = insert(:oauth_token, app: app) + + conn = + conn + |> AuthHelper.put_session_token(token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => "another_client_id", + "redirect_uri" => OAuthController.default_redirect_uri(app), + "scope" => "read" + } + ) + + assert html_response(conn, 200) =~ ~s(type="submit") + end + + test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params", + %{ + app: app, + conn: conn + } do + token = insert(:oauth_token, app: app) + + conn = + conn + |> AuthHelper.put_session_token(token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => OAuthController.default_redirect_uri(app), + "state" => "specific_client_state", + "scope" => "read" + } + ) + + assert URI.decode(redirected_to(conn)) == + "https://redirect.url?access_token=#{token.token}&state=specific_client_state" + end + + test "with existing authentication and unlisted non-OOB `redirect_uri`, redirects without credentials", + %{ + app: app, + conn: conn + } do + unlisted_redirect_uri = "http://cross-site-request.com" + token = insert(:oauth_token, app: app) + + conn = + conn + |> AuthHelper.put_session_token(token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => unlisted_redirect_uri, + "state" => "specific_client_state", + "scope" => "read" + } + ) + + assert redirected_to(conn) == unlisted_redirect_uri + end + + test "with existing authentication and OOB `redirect_uri`, redirects to app with `token` and `state` params", + %{ + app: app, + conn: conn + } do + token = insert(:oauth_token, app: app) + + conn = + conn + |> AuthHelper.put_session_token(token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "scope" => "read" + } + ) + + assert html_response(conn, 200) =~ "Authorization exists" + end + end end