diff --git a/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex b/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex
index 66080c47d..b3eb0b390 100644
--- a/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex
+++ b/lib/pleroma/web/activity_pub/mrf/quiet_reply.ex
@@ -29,12 +29,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuietReply do
} = activity
) do
with true <- is_binary(in_reply_to),
- false <- match?([], cc),
+ true <- Pleroma.Constants.as_public() in to,
%User{follower_address: followers_collection, local: true} <-
User.get_by_ap_id(actor) do
updated_to =
- to
- |> Kernel.++([followers_collection])
+ [followers_collection | to]
|> Kernel.--([Pleroma.Constants.as_public()])
updated_cc =
diff --git a/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs b/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs
index 79e64d650..f66383bf5 100644
--- a/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs
@@ -39,15 +39,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuietReplyTest do
}
}
- expected_to = [batman.ap_id, robin.follower_address]
- expected_cc = [Pleroma.Constants.as_public()]
-
assert {:ok, filtered} = QuietReply.filter(reply)
- assert expected_to == filtered["to"]
- assert expected_cc == filtered["cc"]
- assert expected_to == filtered["object"]["to"]
- assert expected_cc == filtered["object"]["cc"]
+ assert batman.ap_id in filtered["to"]
+ assert batman.ap_id in filtered["object"]["to"]
+ assert robin.follower_address in filtered["to"]
+ assert robin.follower_address in filtered["object"]["to"]
+ assert Pleroma.Constants.as_public() in filtered["cc"]
+ assert Pleroma.Constants.as_public() in filtered["object"]["cc"]
end
test "replying to unlisted post is unmodified" do
diff --git a/test/pleroma/web/o_auth/oauth_authorization_flow_test.exs b/test/pleroma/web/o_auth/oauth_authorization_flow_test.exs
new file mode 100644
index 000000000..fdd8cbdb4
--- /dev/null
+++ b/test/pleroma/web/o_auth/oauth_authorization_flow_test.exs
@@ -0,0 +1,339 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.OAuth.OAuthAuthorizationFlowTest do
+ use Pleroma.Web.ConnCase
+
+ import Pleroma.Factory
+
+ alias Pleroma.Helpers.AuthHelper
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.OAuth.App
+ alias Pleroma.Web.OAuth.Authorization
+ alias Pleroma.Web.OAuth.OAuthController
+ alias Pleroma.Web.OAuth.Token
+
+ @session_opts [
+ store: :cookie,
+ key: "_test",
+ signing_salt: "cooldude"
+ ]
+
+ setup do
+ clear_config([:instance, :account_activation_required], false)
+ clear_config([:instance, :account_approval_required], false)
+ end
+
+ describe "OAuth authorization flow with external integration" do
+ test "complete OAuth flow: create user, create app, authorize, get token, use token" do
+ # Step 1: Create a user
+ user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test"))
+
+ # Step 2: Create a new OAuth client with the required scopes
+ app =
+ insert(:oauth_app,
+ scopes: ["read", "write", "follow", "push"],
+ redirect_uris: "urn:ietf:wg:oauth:2.0:oob"
+ )
+
+ # Step 3: Set up a logged in session
+ conn =
+ build_conn()
+ |> Plug.Session.call(Plug.Session.init(@session_opts))
+ |> fetch_session()
+ |> AuthHelper.put_session_token(insert(:oauth_token, user: user).token)
+
+ # Step 4: Access the /oauth/authorize endpoint with the specified parameters
+ authorize_params = %{
+ "client_id" => app.client_id,
+ "response_type" => "code",
+ "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
+ "scope" => "read write follow push",
+ "force_login" => "False",
+ "state" => "None",
+ "lang" => "None"
+ }
+
+ # First, get the authorization page
+ conn = get(conn, "/oauth/authorize", authorize_params)
+ assert html_response(conn, 200)
+
+ # Step 5: Submit the authorization (simulate user approving the app)
+ authorization_data = %{
+ "authorization" => %{
+ "client_id" => app.client_id,
+ "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
+ "scope" => "read write follow push",
+ "state" => "None"
+ }
+ }
+
+ conn = post(conn, "/oauth/authorize", authorization_data)
+
+ # Should get the OOB authorization page with the code
+ assert html_response(conn, 200)
+
+ # Extract the authorization code from the response
+ response = html_response(conn, 200)
+ assert response =~ "Successfully authorized"
+ assert response =~ "Token code is"
+
+ # Parse the authorization code from the response
+ code_match = Regex.run(~r/Token code is
([a-zA-Z0-9_-]+)/, response)
+ assert code_match
+ [_, authorization_code] = code_match
+
+ # Step 6: Exchange the authorization code for an access token
+ token_conn =
+ build_conn()
+ |> post("/oauth/token", %{
+ "grant_type" => "authorization_code",
+ "code" => authorization_code,
+ "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
+ "client_id" => app.client_id,
+ "client_secret" => app.client_secret
+ })
+
+ token_response = json_response(token_conn, 200)
+ assert %{"access_token" => access_token, "token_type" => "Bearer"} = token_response
+ assert token_response["scope"] == "read write follow push"
+
+ # Verify the token was created in the database
+ token_record = Repo.get_by(Token, token: access_token)
+ assert token_record
+ assert token_record.scopes == ["read", "write", "follow", "push"]
+ assert token_record.user_id == user.id
+ assert token_record.app_id == app.id
+
+ # Step 7: Use the token to access a protected endpoint
+ protected_conn =
+ build_conn()
+ |> put_req_header("authorization", "Bearer #{access_token}")
+ |> get("/api/v1/accounts/verify_credentials")
+
+ # Should get a 200 response with user information
+ user_info = json_response(protected_conn, 200)
+ assert user_info["id"] == to_string(user.id)
+ assert user_info["username"] == user.nickname
+ assert user_info["acct"] == user.nickname
+
+ # Step 8: Test that the token has the correct scopes by accessing different endpoints
+ # Test read:accounts scope (should work)
+ conn_with_token =
+ build_conn()
+ |> put_req_header("authorization", "Bearer #{access_token}")
+
+ # This should work because we have "read" scope
+ conn_with_token
+ |> get("/api/v1/accounts/#{user.id}")
+ |> json_response(200)
+
+ # Test write:accounts scope (should work) - with proper content-type
+ conn_with_token
+ |> put_req_header("content-type", "application/json")
+ |> patch("/api/v1/accounts/update_credentials", %{"display_name" => "Test Name"})
+ |> json_response(200)
+
+ # Test that the token is properly associated with the user
+ assert token_record.user_id == user.id
+ assert token_record.app_id == app.id
+ end
+
+ test "OAuth flow with force_login=false and existing session" do
+ # Create a user and app
+ user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test"))
+
+ app =
+ insert(:oauth_app,
+ scopes: ["read", "write", "follow", "push"],
+ redirect_uris: "urn:ietf:wg:oauth:2.0:oob"
+ )
+
+ # Create an existing token for the same user and app
+ existing_token = insert(:oauth_token, user: user, app: app, scopes: ["read", "write"])
+
+ # Set up a logged in session with the existing token
+ conn =
+ build_conn()
+ |> Plug.Session.call(Plug.Session.init(@session_opts))
+ |> fetch_session()
+ |> AuthHelper.put_session_token(existing_token.token)
+
+ # Access the authorize endpoint with force_login=false
+ authorize_params = %{
+ "client_id" => app.client_id,
+ "response_type" => "code",
+ "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
+ "scope" => "read write follow push",
+ "force_login" => "False",
+ "state" => "test_state"
+ }
+
+ # Should redirect to the OOB page with the existing token
+ conn = get(conn, "/oauth/authorize", authorize_params)
+ assert html_response(conn, 200)
+ assert html_response(conn, 200) =~ "Authorization exists"
+ end
+
+ test "OAuth flow with different scopes than existing token" do
+ # Create a user and app
+ user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test"))
+
+ app =
+ insert(:oauth_app,
+ scopes: ["read", "write", "follow", "push"],
+ redirect_uris: "urn:ietf:wg:oauth:2.0:oob"
+ )
+
+ # Create an existing token with different scopes
+ existing_token = insert(:oauth_token, user: user, app: app, scopes: ["read"])
+
+ # Set up a logged in session
+ conn =
+ build_conn()
+ |> Plug.Session.call(Plug.Session.init(@session_opts))
+ |> fetch_session()
+ |> AuthHelper.put_session_token(existing_token.token)
+
+ # Access the authorize endpoint requesting more scopes
+ authorize_params = %{
+ "client_id" => app.client_id,
+ "response_type" => "code",
+ "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
+ "scope" => "read write follow push",
+ "force_login" => "False",
+ "state" => "test_state"
+ }
+
+ # Should show the authorization page because scopes are different
+ conn = get(conn, "/oauth/authorize", authorize_params)
+ assert html_response(conn, 200)
+ assert html_response(conn, 200) =~ "Authorization exists"
+ end
+
+ test "OAuth flow with invalid client_id" do
+ user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test"))
+
+ conn =
+ build_conn()
+ |> Plug.Session.call(Plug.Session.init(@session_opts))
+ |> fetch_session()
+ |> AuthHelper.put_session_token(insert(:oauth_token, user: user).token)
+
+ # Try to authorize with invalid client_id
+ authorize_params = %{
+ "client_id" => "invalid_client_id",
+ "response_type" => "code",
+ "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
+ "scope" => "read write follow push",
+ "force_login" => "False"
+ }
+
+ conn = get(conn, "/oauth/authorize", authorize_params)
+ # Should still render the page but with error or missing app info
+ assert html_response(conn, 200)
+ end
+
+ test "OAuth flow with unlisted redirect_uri" do
+ user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test"))
+
+ app =
+ insert(:oauth_app,
+ scopes: ["read", "write", "follow", "push"],
+ # Different from requested
+ redirect_uris: "https://example.com/callback"
+ )
+
+ conn =
+ build_conn()
+ |> Plug.Session.call(Plug.Session.init(@session_opts))
+ |> fetch_session()
+ |> AuthHelper.put_session_token(insert(:oauth_token, user: user).token)
+
+ # Try to authorize with unlisted redirect_uri
+ authorize_params = %{
+ "client_id" => app.client_id,
+ "response_type" => "code",
+ "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
+ "scope" => "read write follow push",
+ "force_login" => "False"
+ }
+
+ conn = get(conn, "/oauth/authorize", authorize_params)
+ # Should still render the page but with error about unlisted redirect_uri
+ assert html_response(conn, 200)
+ end
+
+ test "OAuth flow with expired authorization code" do
+ user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test"))
+
+ app =
+ insert(:oauth_app,
+ scopes: ["read", "write", "follow", "push"],
+ redirect_uris: "urn:ietf:wg:oauth:2.0:oob"
+ )
+
+ # Create an expired authorization
+ expired_auth =
+ insert(:oauth_authorization,
+ user: user,
+ app: app,
+ # 1 hour ago
+ valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -3600),
+ scopes: ["read", "write", "follow", "push"]
+ )
+
+ # Try to exchange expired code for token
+ conn =
+ build_conn()
+ |> post("/oauth/token", %{
+ "grant_type" => "authorization_code",
+ "code" => expired_auth.token,
+ "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
+ "client_id" => app.client_id,
+ "client_secret" => app.client_secret
+ })
+
+ # Should get an error
+ response = json_response(conn, 400)
+ assert %{"error" => _} = response
+ end
+
+ test "OAuth flow with used authorization code" do
+ user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test"))
+
+ app =
+ insert(:oauth_app,
+ scopes: ["read", "write", "follow", "push"],
+ redirect_uris: "urn:ietf:wg:oauth:2.0:oob"
+ )
+
+ # Create an authorization and mark it as used
+ auth =
+ insert(:oauth_authorization,
+ user: user,
+ app: app,
+ scopes: ["read", "write", "follow", "push"]
+ )
+
+ {:ok, _} = Authorization.use_token(auth)
+
+ # Try to exchange used code for token
+ conn =
+ build_conn()
+ |> post("/oauth/token", %{
+ "grant_type" => "authorization_code",
+ "code" => auth.token,
+ "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
+ "client_id" => app.client_id,
+ "client_secret" => app.client_secret
+ })
+
+ # Should get an error
+ response = json_response(conn, 400)
+ assert %{"error" => _} = response
+ end
+ end
+end