Browse Source

[#468] Prototype of OAuth2 scopes support. TwitterAPI scope restrictions.

tags/v1.1.4
Ivan Tashkinov 5 years ago
parent
commit
4ad843fb9d
9 changed files with 159 additions and 49 deletions
  1. +29
    -0
      lib/pleroma/plugs/oauth_scopes_plug.ex
  2. +11
    -0
      lib/pleroma/web/oauth.ex
  3. +5
    -1
      lib/pleroma/web/oauth/authorization.ex
  4. +4
    -8
      lib/pleroma/web/oauth/oauth_controller.ex
  5. +6
    -2
      lib/pleroma/web/oauth/token.ex
  6. +61
    -37
      lib/pleroma/web/router.ex
  7. +3
    -1
      lib/pleroma/web/templates/o_auth/o_auth/show.html.eex
  8. +11
    -0
      priv/repo/migrations/20190208131753_add_scope_to_o_auth_entities.exs
  9. +29
    -0
      priv/repo/migrations/20190209123318_data_migration_populate_o_auth_scope.exs

+ 29
- 0
lib/pleroma/plugs/oauth_scopes_plug.ex View 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
- 0
lib/pleroma/web/oauth.ex View 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

+ 5
- 1
lib/pleroma/web/oauth/authorization.ex View File

@@ -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 @@ defmodule Pleroma.Web.OAuth.Authorization do
used: false,
user_id: user.id,
app_id: app.id,
scope: Enum.join(scopes, " "),
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
}



+ 4
- 8
lib/pleroma/web/oauth/oauth_controller.ex View File

@@ -38,7 +38,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{: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 @@ defmodule Pleroma.Web.OAuth.OAuthController do
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 @@ defmodule Pleroma.Web.OAuth.OAuthController 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 @@ defmodule Pleroma.Web.OAuth.OAuthController 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 @@ defmodule Pleroma.Web.OAuth.OAuthController do
%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)


+ 6
- 2
lib/pleroma/web/oauth/token.ex View File

@@ -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)


+ 61
- 37
lib/pleroma/web/router.ex View File

@@ -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)
get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)

# 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("/friends/ids", TwitterAPI.Controller, :friends_ids)
get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array)

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("/mutes/users/ids", TwitterAPI.Controller, :empty_array)
get("/qvitter/mutes", TwitterAPI.Controller, :raw_empty_array)

post("/statuses/pin/:id", TwitterAPI.Controller, :pin)
post("/statuses/unpin/:id", TwitterAPI.Controller, :unpin)
get("/externalprofile/show", TwitterAPI.Controller, :external_profile)

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)
post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
end

scope [] do
pipe_through(:oauth_write)

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("/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("/statusnet/media/upload", TwitterAPI.Controller, :upload)
post("/media/upload", TwitterAPI.Controller, :upload_json)
post("/media/metadata/create", TwitterAPI.Controller, :update_media)
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)

post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
post("/favorites/create", TwitterAPI.Controller, :favorite)
post("/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite)
post("/statuses/pin/:id", TwitterAPI.Controller, :pin)
post("/statuses/unpin/:id", TwitterAPI.Controller, :unpin)

post("/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar)
post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
post("/media/upload", TwitterAPI.Controller, :upload_json)
post("/media/metadata/create", TwitterAPI.Controller, :update_media)

get("/friends/ids", TwitterAPI.Controller, :friends_ids)
get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array)
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

get("/mutes/users/ids", TwitterAPI.Controller, :empty_array)
get("/qvitter/mutes", TwitterAPI.Controller, :raw_empty_array)
scope [] do
pipe_through(:oauth_follow)

get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
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


+ 3
- 1
lib/pleroma/web/templates/o_auth/o_auth/show.html.eex View File

@@ -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 %>

+ 11
- 0
priv/repo/migrations/20190208131753_add_scope_to_o_auth_entities.exs View File

@@ -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

+ 29
- 0
priv/repo/migrations/20190209123318_data_migration_populate_o_auth_scope.exs View File

@@ -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…
Cancel
Save