[#1682] Fixed Basic Auth permissions issue by disabling OAuth scopes checks when password is provided. Refactored plugs skipping functionality.
This commit is contained in:
parent
da4923f2e5
commit
862d4886c9
8 changed files with 100 additions and 23 deletions
|
@ -4,8 +4,11 @@
|
||||||
|
|
||||||
defmodule Pleroma.Plugs.AuthenticationPlug do
|
defmodule Pleroma.Plugs.AuthenticationPlug do
|
||||||
alias Comeonin.Pbkdf2
|
alias Comeonin.Pbkdf2
|
||||||
import Plug.Conn
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
|
||||||
|
import Plug.Conn
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
def init(options), do: options
|
def init(options), do: options
|
||||||
|
@ -37,6 +40,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
|
||||||
if Pbkdf2.checkpw(password, password_hash) do
|
if Pbkdf2.checkpw(password, password_hash) do
|
||||||
conn
|
conn
|
||||||
|> assign(:user, auth_user)
|
|> assign(:user, auth_user)
|
||||||
|
|> OAuthScopesPlug.skip_plug()
|
||||||
else
|
else
|
||||||
conn
|
conn
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
defmodule Pleroma.Plugs.LegacyAuthenticationPlug do
|
defmodule Pleroma.Plugs.LegacyAuthenticationPlug do
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
|
|
||||||
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
|
||||||
def init(options) do
|
def init(options) do
|
||||||
|
@ -27,6 +29,7 @@ defmodule Pleroma.Plugs.LegacyAuthenticationPlug do
|
||||||
conn
|
conn
|
||||||
|> assign(:auth_user, user)
|
|> assign(:auth_user, user)
|
||||||
|> assign(:user, user)
|
|> assign(:user, user)
|
||||||
|
|> OAuthScopesPlug.skip_plug()
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
conn
|
conn
|
||||||
|
|
|
@ -5,30 +5,32 @@
|
||||||
defmodule Pleroma.Plugs.PlugHelper do
|
defmodule Pleroma.Plugs.PlugHelper do
|
||||||
@moduledoc "Pleroma Plug helper"
|
@moduledoc "Pleroma Plug helper"
|
||||||
|
|
||||||
def append_to_called_plugs(conn, plug_module) do
|
@called_plugs_list_id :called_plugs
|
||||||
append_to_private_list(conn, :called_plugs, plug_module)
|
def called_plugs_list_id, do: @called_plugs_list_id
|
||||||
end
|
|
||||||
|
|
||||||
def append_to_skipped_plugs(conn, plug_module) do
|
@skipped_plugs_list_id :skipped_plugs
|
||||||
append_to_private_list(conn, :skipped_plugs, plug_module)
|
def skipped_plugs_list_id, do: @skipped_plugs_list_id
|
||||||
end
|
|
||||||
|
|
||||||
|
@doc "Returns `true` if specified plug was called."
|
||||||
def plug_called?(conn, plug_module) do
|
def plug_called?(conn, plug_module) do
|
||||||
contained_in_private_list?(conn, :called_plugs, plug_module)
|
contained_in_private_list?(conn, @called_plugs_list_id, plug_module)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc "Returns `true` if specified plug was explicitly marked as skipped."
|
||||||
def plug_skipped?(conn, plug_module) do
|
def plug_skipped?(conn, plug_module) do
|
||||||
contained_in_private_list?(conn, :skipped_plugs, plug_module)
|
contained_in_private_list?(conn, @skipped_plugs_list_id, plug_module)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc "Returns `true` if specified plug was either called or explicitly marked as skipped."
|
||||||
def plug_called_or_skipped?(conn, plug_module) do
|
def plug_called_or_skipped?(conn, plug_module) do
|
||||||
plug_called?(conn, plug_module) || plug_skipped?(conn, plug_module)
|
plug_called?(conn, plug_module) || plug_skipped?(conn, plug_module)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp append_to_private_list(conn, private_variable, value) do
|
# Appends plug to known list (skipped, called). Intended to be used from within plug code only.
|
||||||
list = conn.private[private_variable] || []
|
def append_to_private_list(conn, list_id, value) do
|
||||||
|
list = conn.private[list_id] || []
|
||||||
modified_list = Enum.uniq(list ++ [value])
|
modified_list = Enum.uniq(list ++ [value])
|
||||||
Plug.Conn.put_private(conn, private_variable, modified_list)
|
Plug.Conn.put_private(conn, list_id, modified_list)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp contained_in_private_list?(conn, private_variable, value) do
|
defp contained_in_private_list?(conn, private_variable, value) do
|
||||||
|
|
|
@ -40,17 +40,22 @@ defmodule Pleroma.Web do
|
||||||
# Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain
|
# Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain
|
||||||
defp skip_plug(conn, plug_module) do
|
defp skip_plug(conn, plug_module) do
|
||||||
try do
|
try do
|
||||||
plug_module.ensure_skippable()
|
plug_module.skip_plug(conn)
|
||||||
rescue
|
rescue
|
||||||
UndefinedFunctionError ->
|
UndefinedFunctionError ->
|
||||||
raise "#{plug_module} is not skippable. Append `use Pleroma.Web, :plug` to its code."
|
raise "#{plug_module} is not skippable. Append `use Pleroma.Web, :plug` to its code."
|
||||||
end
|
end
|
||||||
|
|
||||||
PlugHelper.append_to_skipped_plugs(conn, plug_module)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Here we can apply before-action hooks (e.g. verify whether auth checks were preformed)
|
# Executed just before actual controller action, invokes before-action hooks (callbacks)
|
||||||
defp action(conn, params) do
|
defp action(conn, params) do
|
||||||
|
with %Plug.Conn{halted: false} <- maybe_halt_on_missing_oauth_scopes_check(conn) do
|
||||||
|
super(conn, params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check
|
||||||
|
defp maybe_halt_on_missing_oauth_scopes_check(conn) do
|
||||||
if Pleroma.Plugs.AuthExpectedPlug.auth_expected?(conn) &&
|
if Pleroma.Plugs.AuthExpectedPlug.auth_expected?(conn) &&
|
||||||
not PlugHelper.plug_called_or_skipped?(conn, Pleroma.Plugs.OAuthScopesPlug) do
|
not PlugHelper.plug_called_or_skipped?(conn, Pleroma.Plugs.OAuthScopesPlug) do
|
||||||
conn
|
conn
|
||||||
|
@ -60,7 +65,7 @@ defmodule Pleroma.Web do
|
||||||
)
|
)
|
||||||
|> halt()
|
|> halt()
|
||||||
else
|
else
|
||||||
super(conn, params)
|
conn
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -129,7 +134,16 @@ defmodule Pleroma.Web do
|
||||||
quote do
|
quote do
|
||||||
alias Pleroma.Plugs.PlugHelper
|
alias Pleroma.Plugs.PlugHelper
|
||||||
|
|
||||||
def ensure_skippable, do: :noop
|
@doc """
|
||||||
|
Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain.
|
||||||
|
"""
|
||||||
|
def skip_plug(conn) do
|
||||||
|
PlugHelper.append_to_private_list(
|
||||||
|
conn,
|
||||||
|
PlugHelper.skipped_plugs_list_id(),
|
||||||
|
__MODULE__
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
@impl Plug
|
@impl Plug
|
||||||
@doc "If marked as skipped, returns `conn`, and calls `perform/2` otherwise."
|
@doc "If marked as skipped, returns `conn`, and calls `perform/2` otherwise."
|
||||||
|
@ -138,7 +152,7 @@ defmodule Pleroma.Web do
|
||||||
conn
|
conn
|
||||||
else
|
else
|
||||||
conn
|
conn
|
||||||
|> PlugHelper.append_to_called_plugs(__MODULE__)
|
|> PlugHelper.append_to_private_list(PlugHelper.called_plugs_list_id(), __MODULE__)
|
||||||
|> perform(options)
|
|> perform(options)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Plugs.AuthenticationPlugTest do
|
||||||
use Pleroma.Web.ConnCase, async: true
|
use Pleroma.Web.ConnCase, async: true
|
||||||
|
|
||||||
alias Pleroma.Plugs.AuthenticationPlug
|
alias Pleroma.Plugs.AuthenticationPlug
|
||||||
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
|
alias Pleroma.Plugs.PlugHelper
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
|
||||||
import ExUnit.CaptureLog
|
import ExUnit.CaptureLog
|
||||||
|
@ -36,13 +38,16 @@ defmodule Pleroma.Plugs.AuthenticationPlugTest do
|
||||||
assert ret_conn == conn
|
assert ret_conn == conn
|
||||||
end
|
end
|
||||||
|
|
||||||
test "with a correct password in the credentials, it assigns the auth_user", %{conn: conn} do
|
test "with a correct password in the credentials, " <>
|
||||||
|
"it assigns the auth_user and marks OAuthScopesPlug as skipped",
|
||||||
|
%{conn: conn} do
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:auth_credentials, %{password: "guy"})
|
|> assign(:auth_credentials, %{password: "guy"})
|
||||||
|> AuthenticationPlug.call(%{})
|
|> AuthenticationPlug.call(%{})
|
||||||
|
|
||||||
assert conn.assigns.user == conn.assigns.auth_user
|
assert conn.assigns.user == conn.assigns.auth_user
|
||||||
|
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "with a wrong password in the credentials, it does nothing", %{conn: conn} do
|
test "with a wrong password in the credentials, it does nothing", %{conn: conn} do
|
||||||
|
|
|
@ -8,6 +8,8 @@ defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do
|
||||||
import Pleroma.Factory
|
import Pleroma.Factory
|
||||||
|
|
||||||
alias Pleroma.Plugs.LegacyAuthenticationPlug
|
alias Pleroma.Plugs.LegacyAuthenticationPlug
|
||||||
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
|
alias Pleroma.Plugs.PlugHelper
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
|
@ -36,7 +38,8 @@ defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag :skip_on_mac
|
@tag :skip_on_mac
|
||||||
test "it authenticates the auth_user if present and password is correct and resets the password",
|
test "if `auth_user` is present and password is correct, " <>
|
||||||
|
"it authenticates the user, resets the password, marks OAuthScopesPlug as skipped",
|
||||||
%{
|
%{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
user: user
|
user: user
|
||||||
|
@ -49,6 +52,7 @@ defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do
|
||||||
conn = LegacyAuthenticationPlug.call(conn, %{})
|
conn = LegacyAuthenticationPlug.call(conn, %{})
|
||||||
|
|
||||||
assert conn.assigns.user.id == user.id
|
assert conn.assigns.user.id == user.id
|
||||||
|
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag :skip_on_mac
|
@tag :skip_on_mac
|
||||||
|
|
|
@ -7,7 +7,6 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
|
||||||
|
|
||||||
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
|
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||||
alias Pleroma.Plugs.OAuthScopesPlug
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
alias Pleroma.Plugs.PlugHelper
|
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
|
|
||||||
import Mock
|
import Mock
|
||||||
|
@ -21,7 +20,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
|
||||||
with_mock OAuthScopesPlug, [:passthrough], perform: &passthrough([&1, &2]) do
|
with_mock OAuthScopesPlug, [:passthrough], perform: &passthrough([&1, &2]) do
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> PlugHelper.append_to_skipped_plugs(OAuthScopesPlug)
|
|> OAuthScopesPlug.skip_plug()
|
||||||
|> OAuthScopesPlug.call(%{scopes: ["random_scope"]})
|
|> OAuthScopesPlug.call(%{scopes: ["random_scope"]})
|
||||||
|
|
||||||
refute called(OAuthScopesPlug.perform(:_, :_))
|
refute called(OAuthScopesPlug.perform(:_, :_))
|
||||||
|
|
46
test/web/auth/basic_auth_test.exs
Normal file
46
test/web/auth/basic_auth_test.exs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.Auth.BasicAuthTest do
|
||||||
|
use Pleroma.Web.ConnCase
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
test "with HTTP Basic Auth used, grants access to OAuth scope-restricted endpoints", %{
|
||||||
|
conn: conn
|
||||||
|
} do
|
||||||
|
user = insert(:user)
|
||||||
|
assert Comeonin.Pbkdf2.checkpw("test", user.password_hash)
|
||||||
|
|
||||||
|
basic_auth_contents =
|
||||||
|
(URI.encode_www_form(user.nickname) <> ":" <> URI.encode_www_form("test"))
|
||||||
|
|> Base.encode64()
|
||||||
|
|
||||||
|
# Succeeds with HTTP Basic Auth
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> put_req_header("authorization", "Basic " <> basic_auth_contents)
|
||||||
|
|> get("/api/v1/accounts/verify_credentials")
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
user_nickname = user.nickname
|
||||||
|
assert %{"username" => ^user_nickname} = response
|
||||||
|
|
||||||
|
# Succeeds with a properly scoped OAuth token
|
||||||
|
valid_token = insert(:oauth_token, scopes: ["read:accounts"])
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_req_header("authorization", "Bearer #{valid_token.token}")
|
||||||
|
|> get("/api/v1/accounts/verify_credentials")
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
# Fails with a wrong-scoped OAuth token (proof of restriction)
|
||||||
|
invalid_token = insert(:oauth_token, scopes: ["read:something"])
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_req_header("authorization", "Bearer #{invalid_token.token}")
|
||||||
|
|> get("/api/v1/accounts/verify_credentials")
|
||||||
|
|> json_response(403)
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue