[#468] Prototype of OAuth2 scopes support. TwitterAPI scope restrictions.
This commit is contained in:
parent
99fd199bda
commit
4ad843fb9d
9 changed files with 159 additions and 49 deletions
29
lib/pleroma/plugs/oauth_scopes_plug.ex
Normal file
29
lib/pleroma/plugs/oauth_scopes_plug.ex
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# 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
|
11
lib/pleroma/web/oauth.ex
Normal file
11
lib/pleroma/web/oauth.ex
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OAuth do
|
||||
def parse_scopes(scopes) do
|
||||
scopes
|
||||
|> to_string()
|
||||
|> String.split([" ", ","])
|
||||
end
|
||||
end
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -8,10 +8,12 @@
|
|||
<%= label f, :password, "Password" %>
|
||||
<%= password_input f, :password %>
|
||||
<br>
|
||||
<%= label f, :scope, "Scopes" %>
|
||||
<%= text_input f, :scope, value: @scope %>
|
||||
<br>
|
||||
<%= 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 %>
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in a new issue