diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex
new file mode 100644
index 000000000..a16adb004
--- /dev/null
+++ b/lib/pleroma/plugs/oauth_scopes_plug.ex
@@ -0,0 +1,29 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.OAuthScopesPlug do
+ import Plug.Conn
+ alias Pleroma.Web.OAuth
+
+ @behaviour Plug
+
+ def init(%{required_scopes: _} = options), do: options
+
+ def call(%Plug.Conn{assigns: assigns} = conn, %{required_scopes: required_scopes}) do
+ token = assigns[:token]
+ granted_scopes = token && OAuth.parse_scopes(token.scope)
+
+ if is_nil(token) || required_scopes -- granted_scopes == [] do
+ conn
+ else
+ missing_scopes = required_scopes -- granted_scopes
+ error_message = "Insufficient permissions: #{Enum.join(missing_scopes, ", ")}."
+
+ conn
+ |> put_resp_content_type("application/json")
+ |> send_resp(403, Jason.encode!(%{error: error_message}))
+ |> halt()
+ end
+ end
+end
diff --git a/lib/pleroma/web/oauth.ex b/lib/pleroma/web/oauth.ex
new file mode 100644
index 000000000..44b83433e
--- /dev/null
+++ b/lib/pleroma/web/oauth.ex
@@ -0,0 +1,11 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.OAuth do
+ def parse_scopes(scopes) do
+ scopes
+ |> to_string()
+ |> String.split([" ", ","])
+ end
+end
diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex
index f8c65602d..0fbaa902b 100644
--- a/lib/pleroma/web/oauth/authorization.ex
+++ b/lib/pleroma/web/oauth/authorization.ex
@@ -6,12 +6,14 @@ defmodule Pleroma.Web.OAuth.Authorization do
use Ecto.Schema
alias Pleroma.{User, Repo}
+ alias Pleroma.Web.OAuth
alias Pleroma.Web.OAuth.{Authorization, App}
import Ecto.{Changeset, Query}
schema "oauth_authorizations" do
field(:token, :string)
+ field(:scope, :string)
field(:valid_until, :naive_datetime)
field(:used, :boolean, default: false)
belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
@@ -20,7 +22,8 @@ defmodule Pleroma.Web.OAuth.Authorization do
timestamps()
end
- def create_authorization(%App{} = app, %User{} = user) do
+ def create_authorization(%App{} = app, %User{} = user, scope \\ nil) do
+ scopes = OAuth.parse_scopes(scope || app.scopes)
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
authorization = %Authorization{
@@ -28,6 +31,7 @@ def create_authorization(%App{} = app, %User{} = user) do
used: false,
user_id: user.id,
app_id: app.id,
+ scope: Enum.join(scopes, " "),
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
}
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 8ec963c79..15345d4ba 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -38,7 +38,7 @@ def create_authorization(conn, %{
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
%App{} = app <- Repo.get_by(App, client_id: client_id),
true <- redirect_uri in String.split(app.redirect_uris),
- {:ok, auth} <- Authorization.create_authorization(app, user) do
+ {:ok, auth} <- Authorization.create_authorization(app, user, params["scope"]) do
# Special case: Local MastodonFE.
redirect_uri =
if redirect_uri == "." do
@@ -81,8 +81,6 @@ def create_authorization(conn, %{
end
end
- # TODO
- # - proper scope handling
def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
with %App{} = app <- get_app_from_request(conn, params),
fixed_token = fix_padding(params["code"]),
@@ -96,7 +94,7 @@ def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
refresh_token: token.refresh_token,
created_at: DateTime.to_unix(inserted_at),
expires_in: 60 * 10,
- scope: "read write follow"
+ scope: token.scope
}
json(conn, response)
@@ -107,8 +105,6 @@ def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
end
end
- # TODO
- # - investigate a way to verify the user wants to grant read/write/follow once scope handling is done
def token_exchange(
conn,
%{"grant_type" => "password", "username" => name, "password" => password} = params
@@ -117,14 +113,14 @@ def token_exchange(
%User{} = user <- User.get_by_nickname_or_email(name),
true <- Pbkdf2.checkpw(password, user.password_hash),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
- {:ok, auth} <- Authorization.create_authorization(app, user),
+ {:ok, auth} <- Authorization.create_authorization(app, user, params["scope"]),
{:ok, token} <- Token.exchange_token(app, auth) do
response = %{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
expires_in: 60 * 10,
- scope: "read write follow"
+ scope: token.scope
}
json(conn, response)
diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex
index 4e01b123b..61f43ed5a 100644
--- a/lib/pleroma/web/oauth/token.ex
+++ b/lib/pleroma/web/oauth/token.ex
@@ -8,11 +8,13 @@ defmodule Pleroma.Web.OAuth.Token do
import Ecto.Query
alias Pleroma.{User, Repo}
+ alias Pleroma.Web.OAuth
alias Pleroma.Web.OAuth.{Token, App, Authorization}
schema "oauth_tokens" do
field(:token, :string)
field(:refresh_token, :string)
+ field(:scope, :string)
field(:valid_until, :naive_datetime)
belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
belongs_to(:app, App)
@@ -23,17 +25,19 @@ defmodule Pleroma.Web.OAuth.Token do
def exchange_token(app, auth) do
with {:ok, auth} <- Authorization.use_token(auth),
true <- auth.app_id == app.id do
- create_token(app, Repo.get(User, auth.user_id))
+ create_token(app, Repo.get(User, auth.user_id), auth.scope)
end
end
- def create_token(%App{} = app, %User{} = user) do
+ def create_token(%App{} = app, %User{} = user, scope \\ nil) do
+ scopes = OAuth.parse_scopes(scope || app.scopes)
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
refresh_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
token = %Token{
token: token,
refresh_token: refresh_token,
+ scope: Enum.join(scopes, " "),
user_id: user.id,
app_id: app.id,
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 7f606ac40..1316d7f98 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -74,6 +74,18 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
+ pipeline :oauth_read do
+ plug(Pleroma.Plugs.OAuthScopesPlug, %{required_scopes: ["read"]})
+ end
+
+ pipeline :oauth_write do
+ plug(Pleroma.Plugs.OAuthScopesPlug, %{required_scopes: ["write"]})
+ end
+
+ pipeline :oauth_follow do
+ plug(Pleroma.Plugs.OAuthScopesPlug, %{required_scopes: ["follow"]})
+ end
+
pipeline :well_known do
plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"])
end
@@ -338,55 +350,67 @@ defmodule Pleroma.Web.Router do
get("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
post("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
- post("/account/update_profile", TwitterAPI.Controller, :update_profile)
- post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner)
- post("/qvitter/update_background_image", TwitterAPI.Controller, :update_background)
+ scope [] do
+ pipe_through(:oauth_read)
- get("/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline)
- get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
- get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
- get("/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline)
- get("/statuses/dm_timeline", TwitterAPI.Controller, :dm_timeline)
- get("/qvitter/statuses/notifications", TwitterAPI.Controller, :notifications)
+ get("/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline)
+ get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
+ get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
+ get("/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline)
+ get("/statuses/dm_timeline", TwitterAPI.Controller, :dm_timeline)
+ get("/qvitter/statuses/notifications", TwitterAPI.Controller, :notifications)
- # XXX: this is really a pleroma API, but we want to keep the pleroma namespace clean
- # for now.
- post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
+ get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)
- post("/statuses/update", TwitterAPI.Controller, :status_update)
- post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
- post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)
- post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
+ get("/friends/ids", TwitterAPI.Controller, :friends_ids)
+ get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array)
- post("/statuses/pin/:id", TwitterAPI.Controller, :pin)
- post("/statuses/unpin/:id", TwitterAPI.Controller, :unpin)
+ get("/mutes/users/ids", TwitterAPI.Controller, :empty_array)
+ get("/qvitter/mutes", TwitterAPI.Controller, :raw_empty_array)
- get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)
- post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)
- post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request)
+ get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
- post("/friendships/create", TwitterAPI.Controller, :follow)
- post("/friendships/destroy", TwitterAPI.Controller, :unfollow)
- post("/blocks/create", TwitterAPI.Controller, :block)
- post("/blocks/destroy", TwitterAPI.Controller, :unblock)
+ post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
+ end
- post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
- post("/media/upload", TwitterAPI.Controller, :upload_json)
- post("/media/metadata/create", TwitterAPI.Controller, :update_media)
+ scope [] do
+ pipe_through(:oauth_write)
- post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
- post("/favorites/create", TwitterAPI.Controller, :favorite)
- post("/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite)
+ post("/account/update_profile", TwitterAPI.Controller, :update_profile)
+ post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner)
+ post("/qvitter/update_background_image", TwitterAPI.Controller, :update_background)
- post("/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar)
+ post("/statuses/update", TwitterAPI.Controller, :status_update)
+ post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
+ post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)
+ post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
- get("/friends/ids", TwitterAPI.Controller, :friends_ids)
- get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array)
+ post("/statuses/pin/:id", TwitterAPI.Controller, :pin)
+ post("/statuses/unpin/:id", TwitterAPI.Controller, :unpin)
- get("/mutes/users/ids", TwitterAPI.Controller, :empty_array)
- get("/qvitter/mutes", TwitterAPI.Controller, :raw_empty_array)
+ post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
+ post("/media/upload", TwitterAPI.Controller, :upload_json)
+ post("/media/metadata/create", TwitterAPI.Controller, :update_media)
- get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
+ post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
+ post("/favorites/create", TwitterAPI.Controller, :favorite)
+ post("/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite)
+
+ post("/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar)
+ end
+
+ scope [] do
+ pipe_through(:oauth_follow)
+
+ post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)
+ post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request)
+
+ post("/friendships/create", TwitterAPI.Controller, :follow)
+ post("/friendships/destroy", TwitterAPI.Controller, :unfollow)
+
+ post("/blocks/create", TwitterAPI.Controller, :block)
+ post("/blocks/destroy", TwitterAPI.Controller, :unblock)
+ end
end
pipeline :ap_relay do
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 de2241ec9..e1c0af975 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
@@ -8,10 +8,12 @@
<%= label f, :password, "Password" %>
<%= password_input f, :password %>
+<%= label f, :scope, "Scopes" %>
+<%= text_input f, :scope, value: @scope %>
+
<%= hidden_input f, :client_id, value: @client_id %>
<%= hidden_input f, :response_type, value: @response_type %>
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
-<%= hidden_input f, :scope, value: @scope %>
<%= hidden_input f, :state, value: @state%>
<%= submit "Authorize" %>
<% end %>
diff --git a/priv/repo/migrations/20190208131753_add_scope_to_o_auth_entities.exs b/priv/repo/migrations/20190208131753_add_scope_to_o_auth_entities.exs
new file mode 100644
index 000000000..809e9ab22
--- /dev/null
+++ b/priv/repo/migrations/20190208131753_add_scope_to_o_auth_entities.exs
@@ -0,0 +1,11 @@
+defmodule Pleroma.Repo.Migrations.AddScopeToOAuthEntities do
+ use Ecto.Migration
+
+ def change do
+ for t <- [:oauth_authorizations, :oauth_tokens] do
+ alter table(t) do
+ add :scope, :string
+ end
+ end
+ end
+end
diff --git a/priv/repo/migrations/20190209123318_data_migration_populate_o_auth_scope.exs b/priv/repo/migrations/20190209123318_data_migration_populate_o_auth_scope.exs
new file mode 100644
index 000000000..722cd6cf9
--- /dev/null
+++ b/priv/repo/migrations/20190209123318_data_migration_populate_o_auth_scope.exs
@@ -0,0 +1,29 @@
+defmodule Pleroma.Repo.Migrations.DataMigrationPopulateOAuthScope do
+ use Ecto.Migration
+
+ require Ecto.Query
+
+ alias Ecto.Query
+ alias Pleroma.Repo
+ alias Pleroma.Web.OAuth
+ alias Pleroma.Web.OAuth.{App, Authorization, Token}
+
+ def up do
+ for app <- Repo.all(Query.from(app in App)) do
+ scopes = OAuth.parse_scopes(app.scopes)
+ scope = Enum.join(scopes, " ")
+
+ Repo.update_all(
+ Query.from(auth in Authorization, where: auth.app_id == ^app.id),
+ set: [scope: scope]
+ )
+
+ Repo.update_all(
+ Query.from(token in Token, where: token.app_id == ^app.id),
+ set: [scope: scope]
+ )
+ end
+ end
+
+ def down, do: :noop
+end