From 530ae4b339f006f500ad2b736a1016c4b444f510 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 26 Oct 2026 12:26:07 +0400 Subject: [PATCH] feat(oauth): support the profile scope A token with only the profile scope can call verify_credentials, as in Mastodon 4.3. The answer is the public account, so apps can use it to log users in without seeing their email or settings. --- changelog.d/oauth-profile-scope.add | 1 + .../API/differences_in_mastoapi_responses.md | 4 ++ .../api_spec/operations/account_operation.ex | 2 +- .../controllers/account_controller.ex | 22 +++++++- priv/gettext/en/LC_MESSAGES/oauth_scopes.po | 5 ++ priv/gettext/oauth_scopes.pot | 5 ++ .../controllers/account_controller_test.exs | 55 +++++++++++++++++++ 7 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 changelog.d/oauth-profile-scope.add diff --git a/changelog.d/oauth-profile-scope.add b/changelog.d/oauth-profile-scope.add new file mode 100644 index 000000000..e7e022baa --- /dev/null +++ b/changelog.d/oauth-profile-scope.add @@ -0,0 +1 @@ +Support the Mastodon `profile` OAuth scope, which only gives the public account from verify_credentials diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md index 194c98e3f..45498defa 100644 --- a/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/development/API/differences_in_mastoapi_responses.md @@ -303,6 +303,10 @@ Additional fields are returned in the response: - `id`: The primary key of this token in Pleroma's database. - `me` (user tokens only): The ActivityPub ID of the user who owns the token. +### `profile` scope + +As in Mastodon 4.3, a token with the `profile` scope can call `GET /api/v1/accounts/verify_credentials` and no other Mastodon API endpoint. Apps can use it to log users in with their fediverse account. Unless the token also has `read` or `read:accounts`, Pleroma returns only public data: unlike Mastodon, there is no `source.privacy`, no `follow_requests_count`, and none of the private `pleroma` fields such as `email`, `settings_store` and `chat_token`. + ## Account Registration `POST /api/v1/accounts` diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 07eb9328b..5427ef0f7 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -48,7 +48,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do description: "Test to make sure that the user token works.", summary: "Verify account credentials", operationId: "AccountController.verify_credentials", - security: [%{"oAuth" => ["read:accounts"]}], + security: [%{"oAuth" => ["read:accounts"]}, %{"oAuth" => ["profile"]}], responses: %{ 200 => Operation.response("Account", "application/json", Account) } diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 631102843..609eeb8cd 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -24,6 +24,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Web.MastodonAPI.MastodonAPI alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.OAuth.OAuthController + alias Pleroma.Web.OAuth.Token alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Web.Registration @@ -49,8 +50,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do plug( OAuthScopesPlug, - %{scopes: ["read:accounts"]} - when action in [:verify_credentials, :endorsements, :own_endorsements] + %{scopes: ["read:accounts", "profile"]} when action == :verify_credentials + ) + + plug( + OAuthScopesPlug, + %{scopes: ["read:accounts"]} when action in [:endorsements, :own_endorsements] ) plug( @@ -161,7 +166,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do end @doc "GET /api/v1/accounts/verify_credentials" - def verify_credentials(%{assigns: %{user: user}} = conn, _) do + def verify_credentials(%{assigns: %{user: user, token: %Token{scopes: scopes}}} = conn, _) do + if OAuthScopesPlug.filter_descendants(["read:accounts"], scopes) == [] do + # The profile scope only identifies the user: show what anyone can see. + render(conn, "show.json", user: user, skip_visibility_check: true) + else + render_own_account(conn, user) + end + end + + def verify_credentials(%{assigns: %{user: user}} = conn, _), do: render_own_account(conn, user) + + defp render_own_account(conn, user) do chat_token = Phoenix.Token.sign(conn, "user socket", user.id) render(conn, "show.json", diff --git a/priv/gettext/en/LC_MESSAGES/oauth_scopes.po b/priv/gettext/en/LC_MESSAGES/oauth_scopes.po index 105ca022b..48654666f 100644 --- a/priv/gettext/en/LC_MESSAGES/oauth_scopes.po +++ b/priv/gettext/en/LC_MESSAGES/oauth_scopes.po @@ -33,6 +33,11 @@ msgstr "Write all using admin API" msgid "follow" msgstr "Read and write user relationships" +#, elixir-autogen, elixir-format +#: lib/pleroma/web/api_spec/scopes/translator.ex:5 +msgid "profile" +msgstr "Read only your public profile, to log in to other services" + #, elixir-autogen, elixir-format #: lib/pleroma/web/api_spec/scopes/translator.ex:5 msgid "push" diff --git a/priv/gettext/oauth_scopes.pot b/priv/gettext/oauth_scopes.pot index 83328770e..57033c8d2 100644 --- a/priv/gettext/oauth_scopes.pot +++ b/priv/gettext/oauth_scopes.pot @@ -220,6 +220,11 @@ msgstr "" msgid "read:mutes" msgstr "" +#: lib/pleroma/web/api_spec/scopes/translator.ex:5 +#, elixir-autogen, elixir-format +msgid "profile" +msgstr "" + #: lib/pleroma/web/api_spec/scopes/translator.ex:5 #, elixir-autogen, elixir-format msgid "push" diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index e82d795e1..0ac33b477 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.OAuth.Token alias Pleroma.Web.Plugs.SetLocalePlug @@ -2033,6 +2034,60 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do assert id == to_string(user.id) end + + test "the profile scope only gets the public account" do + user = insert(:user, default_scope: "private", pleroma_settings_store: %{"fe" => %{}}) + %{conn: conn} = oauth_access(["profile"], user: user) + + response = + conn + |> get("/api/v1/accounts/verify_credentials") + |> json_response_and_validate_schema(200) + + assert response == + AccountView.render("show.json", user: user, skip_visibility_check: true) + |> Jason.encode!() + |> Jason.decode!() + + refute Map.has_key?(response["source"], "privacy") + refute Map.has_key?(response, "follow_requests_count") + + for key <- ~w(email settings_store chat_token notification_settings + unread_notifications_count unread_conversation_count allow_following_move) do + refute Map.has_key?(response["pleroma"], key), "#{key} must not be shown" + end + end + + test "the profile scope with read:accounts gets the full account" do + %{conn: conn} = oauth_access(["profile", "read:accounts"]) + + response = + conn + |> get("/api/v1/accounts/verify_credentials") + |> json_response_and_validate_schema(200) + + assert response["source"]["privacy"] + assert response["pleroma"]["chat_token"] + end + + test "the profile scope works when unauthenticated users cannot see profiles" do + clear_config([:restrict_unauthenticated, :profiles, :local], true) + %{user: user, conn: conn} = oauth_access(["profile"]) + + assert %{"id" => id} = + conn + |> get("/api/v1/accounts/verify_credentials") + |> json_response_and_validate_schema(200) + + assert id == to_string(user.id) + end + + test "the profile scope does not give access to other endpoints" do + %{conn: conn} = oauth_access(["profile"]) + + assert conn |> get("/api/v1/timelines/home") |> json_response(403) + assert conn |> get("/api/v1/follow_requests") |> json_response(403) + end end describe "user relationships" do