Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85711569
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
128 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/pleroma/helpers/auth_helper.ex b/lib/pleroma/helpers/auth_helper.ex
index d56f6f461..13e4c8158 100644
--- a/lib/pleroma/helpers/auth_helper.ex
+++ b/lib/pleroma/helpers/auth_helper.ex
@@ -1,28 +1,46 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Helpers.AuthHelper do
alias Pleroma.Web.Plugs.OAuthScopesPlug
+ alias Plug.Conn
import Plug.Conn
+ @oauth_token_session_key :oauth_token
+
@doc """
Skips OAuth permissions (scopes) checks, assigns nil `:token`.
Intended to be used with explicit authentication and only when OAuth token cannot be determined.
"""
def skip_oauth(conn) do
conn
|> assign(:token, nil)
|> OAuthScopesPlug.skip_plug()
end
@doc "Drops authentication info from connection"
def drop_auth_info(conn) do
# To simplify debugging, setting a private variable on `conn` if auth info is dropped
conn
|> assign(:user, nil)
|> assign(:token, nil)
|> put_private(:authentication_ignored, true)
end
+
+ @doc "Gets OAuth token string from session"
+ def get_session_token(%Conn{} = conn) do
+ get_session(conn, @oauth_token_session_key)
+ end
+
+ @doc "Updates OAuth token string in session"
+ def put_session_token(%Conn{} = conn, token) when is_binary(token) do
+ put_session(conn, @oauth_token_session_key, token)
+ end
+
+ @doc "Deletes OAuth token string from session"
+ def delete_session_token(%Conn{} = conn) do
+ delete_session(conn, @oauth_token_session_key)
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
index f415e5931..4920d65da 100644
--- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -1,105 +1,108 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AuthController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper
alias Pleroma.User
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
alias Pleroma.Web.TwitterAPI.TwitterAPI
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
plug(Pleroma.Web.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)
@local_mastodon_name "Mastodon-Local"
@doc "GET /web/login"
# Local Mastodon FE login callback action
def login(conn, %{"code" => auth_token} = params) do
with {:ok, app} <- local_mastofe_app(),
{:ok, auth} <- Authorization.get_by_token(app, auth_token),
{:ok, oauth_token} <- Token.exchange_token(app, auth) do
redirect_to =
conn
|> local_mastodon_post_login_path()
|> UriHelper.modify_uri_params(%{"access_token" => oauth_token.token})
conn
+ |> AuthHelper.put_session_token(oauth_token.token)
|> redirect(to: redirect_to)
else
_ -> redirect_to_oauth_form(conn, params)
end
end
def login(conn, params) do
- with %{assigns: %{user: %User{}, token: %Token{app_id: app_id, token: token}}} <- conn,
+ with %{assigns: %{user: %User{}, token: %Token{app_id: app_id}}} <- conn,
{:ok, %{id: ^app_id}} <- local_mastofe_app() do
- redirect(conn, to: local_mastodon_post_login_path(conn) <> "?access_token=#{token}")
+ redirect(conn, to: local_mastodon_post_login_path(conn))
else
_ -> redirect_to_oauth_form(conn, params)
end
end
defp redirect_to_oauth_form(conn, _params) do
with {:ok, app} <- local_mastofe_app() do
path =
Routes.o_auth_path(conn, :authorize,
response_type: "code",
client_id: app.client_id,
redirect_uri: ".",
scope: Enum.join(app.scopes, " ")
)
redirect(conn, to: path)
end
end
@doc "DELETE /auth/sign_out"
def logout(conn, _) do
conn =
with %{assigns: %{token: %Token{} = oauth_token}} <- conn,
- {:ok, %Token{token: _session_token}} <- RevokeToken.revoke(oauth_token) do
- conn
+ session_token = AuthHelper.get_session_token(conn),
+ {:ok, %Token{token: ^session_token}} <- RevokeToken.revoke(oauth_token) do
+ AuthHelper.delete_session_token(conn)
else
_ -> conn
end
redirect(conn, to: "/")
end
@doc "POST /auth/password"
def password_reset(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
TwitterAPI.password_reset(nickname_or_email)
json_response(conn, :no_content, "")
end
defp local_mastodon_post_login_path(conn) do
case get_session(conn, :return_to) do
nil ->
Routes.masto_fe_path(conn, :index, ["getting-started"])
return_to ->
delete_session(conn, :return_to)
return_to
end
end
@spec local_mastofe_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
def local_mastofe_app do
App.get_or_make(
%{client_name: @local_mastodon_name, redirect_uris: "."},
["read", "write", "follow", "push", "admin"]
)
end
end
diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex
index 43536f95d..358120fe6 100644
--- a/lib/pleroma/web/o_auth/o_auth_controller.ex
+++ b/lib/pleroma/web/o_auth/o_auth_controller.ex
@@ -1,640 +1,637 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper
alias Pleroma.Maps
alias Pleroma.MFA
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.MFAController
alias Pleroma.Web.OAuth.MFAView
alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
alias Pleroma.Web.Plugs.RateLimiter
alias Pleroma.Web.Utils.Params
require Logger
if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
plug(:fetch_session)
plug(:fetch_flash)
plug(:skip_auth)
plug(RateLimiter, [name: :authentication] when action == :create_authorization)
action_fallback(Pleroma.Web.OAuth.FallbackController)
@oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"
# Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg
def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do
{auth_attrs, params} = Map.pop(params, "authorization")
authorize(conn, Map.merge(params, auth_attrs))
end
def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do
if Params.truthy_param?(params["force_login"]) do
do_authorize(conn, params)
else
handle_existing_authorization(conn, params)
end
end
# Note: the token is set in oauth_plug, but the token and client do not always go together.
# For example, MastodonFE's token is set if user requests with another client,
# after user already authorized to MastodonFE.
# So we have to check client and token.
def authorize(
%Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
%{"client_id" => client_id} = params
) do
with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app),
^client_id <- t.app.client_id do
handle_existing_authorization(conn, params)
else
_ -> do_authorize(conn, params)
end
end
def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
defp do_authorize(%Plug.Conn{} = conn, params) do
app = Repo.get_by(App, client_id: params["client_id"])
available_scopes = (app && app.scopes) || []
scopes = Scopes.fetch_scopes(params, available_scopes)
- # if we already have a token for this specific setup, we can use that
- with false <- Params.truthy_param?(params["force_login"]),
- %App{} <- app,
- {:ok, _} <- Scopes.validate(scopes, app.scopes),
- {:ok, %Token{} = token} <- Token.get_by_app(app) do
- token = Repo.preload(token, :app)
+ user =
+ with %{assigns: %{user: %User{} = user}} <- conn do
+ user
+ else
+ _ -> nil
+ end
- conn
- |> assign(:token, token)
- |> handle_existing_authorization(params)
- else
- _ ->
- user =
- with %{assigns: %{user: %User{} = user}} <- conn do
- user
- else
- _ -> nil
- end
-
- scopes =
- if scopes == [] do
- available_scopes
- else
- scopes
- end
-
- # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
- render(conn, Authenticator.auth_template(), %{
- user: user,
- app: app && Map.delete(app, :client_secret),
- response_type: params["response_type"],
- client_id: params["client_id"],
- available_scopes: available_scopes,
- scopes: scopes,
- redirect_uri: params["redirect_uri"],
- state: params["state"],
- params: params,
- view_module: OAuthView
- })
- end
+ scopes =
+ if scopes == [] do
+ available_scopes
+ else
+ scopes
+ end
+
+ # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
+ render(conn, Authenticator.auth_template(), %{
+ user: user,
+ app: app && Map.delete(app, :client_secret),
+ response_type: params["response_type"],
+ client_id: params["client_id"],
+ available_scopes: available_scopes,
+ scopes: scopes,
+ redirect_uri: params["redirect_uri"],
+ state: params["state"],
+ params: params,
+ view_module: OAuthView
+ })
end
defp handle_existing_authorization(
%Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
%{"redirect_uri" => @oob_token_redirect_uri}
) do
render(conn, "oob_token_exists.html", %{token: token})
end
defp handle_existing_authorization(
%Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
%{} = params
) do
app = Repo.preload(token, :app).app
redirect_uri =
if is_binary(params["redirect_uri"]) do
params["redirect_uri"]
else
default_redirect_uri(app)
end
if redirect_uri in String.split(app.redirect_uris) do
redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{access_token: token.token}
url_params = Maps.put_if_present(url_params, :state, params["state"])
url = UriHelper.modify_uri_params(redirect_uri, url_params)
redirect(conn, external: url)
else
conn
|> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
|> redirect(external: redirect_uri(conn, redirect_uri))
end
end
def create_authorization(_, _, opts \\ [])
def create_authorization(%Plug.Conn{assigns: %{user: %User{} = user}} = conn, params, []) do
create_authorization(conn, params, user: user)
end
def create_authorization(%Plug.Conn{} = conn, %{"authorization" => _} = params, opts) do
with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
after_create_authorization(conn, auth, params)
else
error ->
handle_create_authorization_error(conn, error, params)
end
end
def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
"authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
}) do
# Enforcing the view to reuse the template when calling from other controllers
conn
|> put_view(OAuthView)
|> render("oob_authorization_created.html", %{auth: auth, view_module: OAuthView})
end
def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
"authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
}) do
app = Repo.preload(auth, :app).app
# An extra safety measure before we redirect (also done in `do_create_authorization/2`)
if redirect_uri in String.split(app.redirect_uris) do
redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{code: auth.token}
url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"])
url = UriHelper.modify_uri_params(redirect_uri, url_params)
redirect(conn, external: url)
else
conn
|> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
|> redirect(external: redirect_uri(conn, redirect_uri))
end
end
defp handle_create_authorization_error(
%Plug.Conn{} = conn,
{:error, scopes_issue},
%{"authorization" => _} = params
)
when scopes_issue in [:unsupported_scopes, :missing_scopes] do
# Per https://github.com/tootsuite/mastodon/blob/
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
conn
|> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
|> put_status(:unauthorized)
|> authorize(params)
end
defp handle_create_authorization_error(
%Plug.Conn{} = conn,
{:account_status, :confirmation_pending},
%{"authorization" => _} = params
) do
conn
|> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
|> put_status(:forbidden)
|> authorize(params)
end
defp handle_create_authorization_error(
%Plug.Conn{} = conn,
{:mfa_required, user, auth, _},
params
) do
{:ok, token} = MFA.Token.create(user, auth)
data = %{
"mfa_token" => token.token,
"redirect_uri" => params["authorization"]["redirect_uri"],
"state" => params["authorization"]["state"]
}
MFAController.show(conn, data)
end
defp handle_create_authorization_error(
%Plug.Conn{} = conn,
{:account_status, :password_reset_pending},
%{"authorization" => _} = params
) do
conn
|> put_flash(:error, dgettext("errors", "Password reset is required"))
|> put_status(:forbidden)
|> authorize(params)
end
defp handle_create_authorization_error(
%Plug.Conn{} = conn,
{:account_status, :deactivated},
%{"authorization" => _} = params
) do
conn
|> put_flash(:error, dgettext("errors", "Your account is currently disabled"))
|> put_status(:forbidden)
|> authorize(params)
end
defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
Authenticator.handle_error(conn, error)
end
@doc "Renew access_token with refresh_token"
def token_exchange(
%Plug.Conn{} = conn,
%{"grant_type" => "refresh_token", "refresh_token" => token} = _params
) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
{:ok, token} <- RefreshToken.grant(token) do
after_token_exchange(conn, %{user: user, token: token})
else
_error -> render_invalid_credentials_error(conn)
end
end
def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
fixed_token = Token.Utils.fix_padding(params["code"]),
{:ok, auth} <- Authorization.get_by_token(app, fixed_token),
%User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth) do
after_token_exchange(conn, %{user: user, token: token})
else
error ->
handle_token_exchange_error(conn, error)
end
end
def token_exchange(
%Plug.Conn{} = conn,
%{"grant_type" => "password"} = params
) do
with {:ok, %User{} = user} <- Authenticator.get_user(conn),
{:ok, app} <- Token.Utils.fetch_app(conn),
requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
{:ok, token} <- login(user, app, requested_scopes) do
after_token_exchange(conn, %{user: user, token: token})
else
error ->
handle_token_exchange_error(conn, error)
end
end
def token_exchange(
%Plug.Conn{} = conn,
%{"grant_type" => "password", "name" => name, "password" => _password} = params
) do
params =
params
|> Map.delete("name")
|> Map.put("username", name)
token_exchange(conn, params)
end
def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, auth} <- Authorization.create_authorization(app, %User{}),
{:ok, token} <- Token.exchange_token(app, auth) do
after_token_exchange(conn, %{token: token})
else
_error ->
handle_token_exchange_error(conn, :invalid_credentails)
end
end
# Bad request
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
- def after_token_exchange(%Plug.Conn{} = conn, %{token: _token} = view_params) do
+ def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
conn
+ |> AuthHelper.put_session_token(token.token)
|> json(OAuthView.render("token.json", view_params))
end
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
conn
|> put_status(:forbidden)
|> json(build_and_response_mfa_token(user, auth))
end
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do
render_error(
conn,
:forbidden,
"Your account is currently disabled",
%{},
"account_is_disabled"
)
end
defp handle_token_exchange_error(
%Plug.Conn{} = conn,
{:account_status, :password_reset_pending}
) do
render_error(
conn,
:forbidden,
"Password reset is required",
%{},
"password_reset_required"
)
end
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do
render_error(
conn,
:forbidden,
"Your login is missing a confirmed e-mail address",
%{},
"missing_confirmed_email"
)
end
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
render_error(
conn,
:forbidden,
"Your account is awaiting approval.",
%{},
"awaiting_approval"
)
end
defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
render_invalid_credentials_error(conn)
end
def token_revoke(%Plug.Conn{} = conn, %{"token" => token}) do
with {:ok, %Token{} = oauth_token} <- Token.get_by_token(token),
- {:ok, _oauth_token} <- RevokeToken.revoke(oauth_token) do
+ {:ok, oauth_token} <- RevokeToken.revoke(oauth_token) do
+ conn =
+ with session_token = AuthHelper.get_session_token(conn),
+ %Token{token: ^session_token} <- oauth_token do
+ AuthHelper.delete_session_token(conn)
+ else
+ _ -> conn
+ end
+
json(conn, %{})
else
_error ->
# RFC 7009: invalid tokens [in the request] do not cause an error response
json(conn, %{})
end
end
def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
# Response for bad request
defp bad_request(%Plug.Conn{} = conn, _) do
render_error(conn, :internal_server_error, "Bad request")
end
@doc "Prepares OAuth request to provider for Ueberauth"
def prepare_request(%Plug.Conn{} = conn, %{
"provider" => provider,
"authorization" => auth_attrs
}) do
scope =
auth_attrs
|> Scopes.fetch_scopes([])
|> Scopes.to_string()
state =
auth_attrs
|> Map.delete("scopes")
|> Map.put("scope", scope)
|> Jason.encode!()
params =
auth_attrs
|> Map.drop(~w(scope scopes client_id redirect_uri))
|> Map.put("state", state)
# Handing the request to Ueberauth
redirect(conn, to: Routes.o_auth_path(conn, :request, provider, params))
end
def request(%Plug.Conn{} = conn, params) do
message =
if params["provider"] do
dgettext("errors", "Unsupported OAuth provider: %{provider}.",
provider: params["provider"]
)
else
dgettext("errors", "Bad OAuth request.")
end
conn
|> put_flash(:error, message)
|> redirect(to: "/")
end
def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
params = callback_params(params)
messages = for e <- Map.get(failure, :errors, []), do: e.message
message = Enum.join(messages, "; ")
conn
|> put_flash(
:error,
dgettext("errors", "Failed to authenticate: %{message}.", message: message)
)
|> redirect(external: redirect_uri(conn, params["redirect_uri"]))
end
def callback(%Plug.Conn{} = conn, params) do
params = callback_params(params)
with {:ok, registration} <- Authenticator.get_registration(conn) do
auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
case Repo.get_assoc(registration, :user) do
{:ok, user} ->
create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
_ ->
registration_params =
Map.merge(auth_attrs, %{
"nickname" => Registration.nickname(registration),
"email" => Registration.email(registration)
})
conn
|> put_session_registration_id(registration.id)
|> registration_details(%{"authorization" => registration_params})
end
else
error ->
Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
conn
|> put_flash(:error, dgettext("errors", "Failed to set up user account."))
|> redirect(external: redirect_uri(conn, params["redirect_uri"]))
end
end
defp callback_params(%{"state" => state} = params) do
Map.merge(params, Jason.decode!(state))
end
def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
render(conn, "register.html", %{
client_id: auth_attrs["client_id"],
redirect_uri: auth_attrs["redirect_uri"],
state: auth_attrs["state"],
scopes: Scopes.fetch_scopes(auth_attrs, []),
nickname: auth_attrs["nickname"],
email: auth_attrs["email"]
})
end
def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
%Registration{} = registration <- Repo.get(Registration, registration_id),
{_, {:ok, auth, _user}} <-
{:create_authorization, do_create_authorization(conn, params)},
%User{} = user <- Repo.preload(auth, :user).user,
{:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
conn
|> put_session_registration_id(nil)
|> after_create_authorization(auth, params)
else
{:create_authorization, error} ->
{:register, handle_create_authorization_error(conn, error, params)}
_ ->
{:register, :generic_error}
end
end
def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
%Registration{} = registration <- Repo.get(Registration, registration_id),
{:ok, user} <- Authenticator.create_from_registration(conn, registration) do
conn
|> put_session_registration_id(nil)
|> create_authorization(
params,
user: user
)
else
{:error, changeset} ->
message =
Enum.map(changeset.errors, fn {field, {error, _}} ->
"#{field} #{error}"
end)
|> Enum.join("; ")
message =
String.replace(
message,
"ap_id has already been taken",
"nickname has already been taken"
)
conn
|> put_status(:forbidden)
|> put_flash(:error, "Error: #{message}.")
|> registration_details(params)
_ ->
{:register, :generic_error}
end
end
defp do_create_authorization(conn, auth_attrs, user \\ nil)
defp do_create_authorization(
%Plug.Conn{} = conn,
%{
"authorization" =>
%{
"client_id" => client_id,
"redirect_uri" => redirect_uri
} = auth_attrs
},
user
) do
with {_, {:ok, %User{} = user}} <-
{:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
%App{} = app <- Repo.get_by(App, client_id: client_id),
true <- redirect_uri in String.split(app.redirect_uris),
requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes),
{:ok, auth} <- do_create_authorization(user, app, requested_scopes) do
{:ok, auth, user}
end
end
defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
when is_list(requested_scopes) do
with {:account_status, :active} <- {:account_status, User.account_status(user)},
{:ok, scopes} <- validate_scopes(app, requested_scopes),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
{:ok, auth}
end
end
# Note: intended to be a private function but opened for AccountController that logs in on signup
@doc "If checks pass, creates authorization and token for given user, app and requested scopes."
def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do
with {:ok, auth} <- do_create_authorization(user, app, requested_scopes),
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
{:ok, token} <- Token.exchange_token(app, auth) do
{:ok, token}
end
end
# Special case: Local MastodonFE
defp redirect_uri(%Plug.Conn{} = conn, "."), do: Routes.auth_url(conn, :login)
defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
do: put_session(conn, :registration_id, registration_id)
defp build_and_response_mfa_token(user, auth) do
with {:ok, token} <- MFA.Token.create(user, auth) do
MFAView.render("mfa_response.json", %{token: token, user: user})
end
end
@spec validate_scopes(App.t(), map() | list()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
defp validate_scopes(%App{} = app, params) when is_map(params) do
requested_scopes = Scopes.fetch_scopes(params, app.scopes)
validate_scopes(app, requested_scopes)
end
defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
Scopes.validate(requested_scopes, app.scopes)
end
def default_redirect_uri(%App{} = app) do
app.redirect_uris
|> String.split()
|> Enum.at(0)
end
defp render_invalid_credentials_error(conn) do
render_error(conn, :bad_request, "Invalid credentials")
end
end
diff --git a/lib/pleroma/web/o_auth/token.ex b/lib/pleroma/web/o_auth/token.ex
index 08c8cd298..9d69e9db4 100644
--- a/lib/pleroma/web/o_auth/token.ex
+++ b/lib/pleroma/web/o_auth/token.ex
@@ -1,151 +1,145 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token do
use Ecto.Schema
import Ecto.Changeset
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Query
@type t :: %__MODULE__{}
schema "oauth_tokens" do
field(:token, :string)
field(:refresh_token, :string)
field(:scopes, {:array, :string}, default: [])
field(:valid_until, :naive_datetime_usec)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:app, App)
timestamps()
end
def lifespan do
Pleroma.Config.get!([:oauth2, :token_expires_in])
end
@doc "Gets token by unique access token"
@spec get_by_token(String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_token(token) do
token
|> Query.get_by_token()
|> Repo.find_resource()
end
- def get_by_app(%App{} = app) do
- app.id
- |> Query.get_unexpired_by_app()
- |> Repo.find_resource()
- end
-
@doc "Gets token for app by access token"
@spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_token(%App{id: app_id} = _app, token) do
Query.get_by_app(app_id)
|> Query.get_by_token(token)
|> Repo.find_resource()
end
@doc "Gets token for app by refresh token"
@spec get_by_refresh_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_refresh_token(%App{id: app_id} = _app, token) do
Query.get_by_app(app_id)
|> Query.get_by_refresh_token(token)
|> Query.preload([:user])
|> Repo.find_resource()
end
@spec exchange_token(App.t(), Authorization.t()) :: {:ok, Token.t()} | {:error, Changeset.t()}
def exchange_token(app, auth) do
with {:ok, auth} <- Authorization.use_token(auth),
true <- auth.app_id == app.id do
user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{}
create(
app,
user,
%{scopes: auth.scopes}
)
end
end
defp put_token(changeset) do
changeset
|> change(%{token: Token.Utils.generate_token()})
|> validate_required([:token])
|> unique_constraint(:token)
end
defp put_refresh_token(changeset, attrs) do
refresh_token = Map.get(attrs, :refresh_token, Token.Utils.generate_token())
changeset
|> change(%{refresh_token: refresh_token})
|> validate_required([:refresh_token])
|> unique_constraint(:refresh_token)
end
defp put_valid_until(changeset, attrs) do
valid_until =
Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), lifespan()))
changeset
|> change(%{valid_until: valid_until})
|> validate_required([:valid_until])
end
@spec create(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()}
def create(%App{} = app, %User{} = user, attrs \\ %{}) do
with {:ok, token} <- do_create(app, user, attrs) do
if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do
Pleroma.Workers.PurgeExpiredToken.enqueue(%{
token_id: token.id,
valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"),
mod: __MODULE__
})
end
{:ok, token}
end
end
defp do_create(app, user, attrs) do
%__MODULE__{user_id: user.id, app_id: app.id}
|> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes])
|> validate_required([:scopes, :app_id])
|> put_valid_until(attrs)
|> put_token()
|> put_refresh_token(attrs)
|> Repo.insert()
end
def delete_user_tokens(%User{id: user_id}) do
Query.get_by_user(user_id)
|> Repo.delete_all()
end
def delete_user_token(%User{id: user_id}, token_id) do
Query.get_by_user(user_id)
|> Query.get_by_id(token_id)
|> Repo.delete_all()
end
def get_user_tokens(%User{id: user_id}) do
Query.get_by_user(user_id)
|> Query.preload([:app])
|> Repo.all()
end
def is_expired?(%__MODULE__{valid_until: valid_until}) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0
end
def is_expired?(_), do: false
end
diff --git a/lib/pleroma/web/o_auth/token/query.ex b/lib/pleroma/web/o_auth/token/query.ex
index 8edfcf5d7..d16a759d8 100644
--- a/lib/pleroma/web/o_auth/token/query.ex
+++ b/lib/pleroma/web/o_auth/token/query.ex
@@ -1,55 +1,49 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.Query do
@moduledoc """
Contains queries for OAuth Token.
"""
import Ecto.Query, only: [from: 2]
@type query :: Ecto.Queryable.t() | Token.t()
alias Pleroma.Web.OAuth.Token
@spec get_by_refresh_token(query, String.t()) :: query
def get_by_refresh_token(query \\ Token, refresh_token) do
from(q in query, where: q.refresh_token == ^refresh_token)
end
@spec get_by_token(query, String.t()) :: query
def get_by_token(query \\ Token, token) do
from(q in query, where: q.token == ^token)
end
- @spec get_unexpired_by_app(query, String.t()) :: query
- def get_unexpired_by_app(query \\ Token, app_id) do
- time = NaiveDateTime.utc_now()
- from(q in query, where: q.app_id == ^app_id and q.valid_until > ^time, limit: 1)
- end
-
@spec get_by_app(query, String.t()) :: query
def get_by_app(query \\ Token, app_id) do
- from(q in query, where: q.app_id == ^app_id, limit: 1)
+ from(q in query, where: q.app_id == ^app_id)
end
@spec get_by_id(query, String.t()) :: query
def get_by_id(query \\ Token, id) do
from(q in query, where: q.id == ^id)
end
@spec get_by_user(query, String.t()) :: query
def get_by_user(query \\ Token, user_id) do
from(q in query, where: q.user_id == ^user_id)
end
@spec preload(query, any) :: query
def preload(query \\ Token, assoc_preload \\ [])
def preload(query, assoc_preload) when is_list(assoc_preload) do
from(q in query, preload: ^assoc_preload)
end
def preload(query, _assoc_preload), do: query
end
diff --git a/lib/pleroma/web/plugs/o_auth_plug.ex b/lib/pleroma/web/plugs/o_auth_plug.ex
index 29b3316b3..5e06ac3f6 100644
--- a/lib/pleroma/web/plugs/o_auth_plug.ex
+++ b/lib/pleroma/web/plugs/o_auth_plug.ex
@@ -1,96 +1,107 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.OAuthPlug do
@moduledoc "Performs OAuth authentication by token from params / headers / cookies."
import Plug.Conn
import Ecto.Query
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Token
@realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i")
def init(options), do: options
+ def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
+
def call(conn, _) do
with {:ok, token_str} <- fetch_token_str(conn) do
with {:ok, user, user_token} <- fetch_user_and_token(token_str),
false <- Token.is_expired?(user_token) do
conn
|> assign(:token, user_token)
|> assign(:user, user)
else
_ ->
with {:ok, app, app_token} <- fetch_app_and_token(token_str),
false <- Token.is_expired?(app_token) do
conn
|> assign(:token, app_token)
|> assign(:app, app)
else
_ -> conn
end
end
else
_ -> conn
end
end
# Gets user by token
#
@spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil
defp fetch_user_and_token(token) do
query =
from(t in Token,
where: t.token == ^token,
join: user in assoc(t, :user),
preload: [user: user]
)
with %Token{user: user} = token_record <- Repo.one(query) do
{:ok, user, token_record}
end
end
@spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil
defp fetch_app_and_token(token) do
query =
from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app])
with %Token{app: app} = token_record <- Repo.one(query) do
{:ok, app, token_record}
end
end
# Gets token string from conn (in params / headers / session)
#
@spec fetch_token_str(Plug.Conn.t() | list(String.t())) :: :no_token_found | {:ok, String.t()}
defp fetch_token_str(%Plug.Conn{params: %{"access_token" => access_token}} = _conn) do
{:ok, access_token}
end
defp fetch_token_str(%Plug.Conn{} = conn) do
headers = get_req_header(conn, "authorization")
with {:ok, token} <- fetch_token_str(headers) do
{:ok, token}
else
- _ -> :no_token_found
+ _ -> fetch_token_from_session(conn)
end
end
defp fetch_token_str([token | tail]) do
trimmed_token = String.trim(token)
case Regex.run(@realm_reg, trimmed_token) do
[_, match] -> {:ok, String.trim(match)}
_ -> fetch_token_str(tail)
end
end
defp fetch_token_str([]), do: :no_token_found
+
+ @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
+ defp fetch_token_from_session(conn) do
+ case AuthHelper.get_session_token(conn) do
+ nil -> :no_token_found
+ token -> {:ok, token}
+ end
+ end
end
diff --git a/lib/pleroma/web/plugs/set_user_session_id_plug.ex b/lib/pleroma/web/plugs/set_user_session_id_plug.ex
new file mode 100644
index 000000000..a1cfa0915
--- /dev/null
+++ b/lib/pleroma/web/plugs/set_user_session_id_plug.ex
@@ -0,0 +1,18 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do
+ alias Pleroma.Helpers.AuthHelper
+ alias Pleroma.Web.OAuth.Token
+
+ def init(opts) do
+ opts
+ end
+
+ def call(%{assigns: %{token: %Token{} = oauth_token}} = conn, _) do
+ AuthHelper.put_session_token(conn, oauth_token.token)
+ end
+
+ def call(conn, _), do: conn
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index f2d6b0aff..647d99278 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -1,875 +1,878 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
import Phoenix.LiveDashboard.Router
pipeline :accepts_html do
plug(:accepts, ["html"])
end
pipeline :accepts_html_xml do
plug(:accepts, ["html", "xml", "rss", "atom"])
end
pipeline :accepts_html_json do
plug(:accepts, ["html", "activity+json", "json"])
end
pipeline :accepts_html_xml_json do
plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
end
pipeline :accepts_xml_rss_atom do
plug(:accepts, ["xml", "rss", "atom"])
end
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
end
pipeline :oauth do
plug(:fetch_session)
plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.UserEnabledPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
end
# Note: expects _user_ authentication (user-unbound app-bound tokens don't qualify)
pipeline :expect_user_authentication do
plug(Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug)
end
# Note: expects public instance or _user_ authentication (user-unbound tokens don't qualify)
pipeline :expect_public_instance_or_user_authentication do
plug(Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug)
end
pipeline :authenticate do
plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Web.Plugs.UserFetcherPlug)
plug(Pleroma.Web.Plugs.AuthenticationPlug)
end
pipeline :after_auth do
plug(Pleroma.Web.Plugs.UserEnabledPlug)
+ plug(Pleroma.Web.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
plug(Pleroma.Web.Plugs.UserTrackingPlug)
end
pipeline :base_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(:authenticate)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :no_auth_or_privacy_expectations_api do
plug(:base_api)
plug(:after_auth)
plug(Pleroma.Web.Plugs.IdempotencyPlug)
end
# Pipeline for app-related endpoints (no user auth checks — app-bound tokens must be supported)
pipeline :app_api do
plug(:no_auth_or_privacy_expectations_api)
end
pipeline :api do
plug(:expect_public_instance_or_user_authentication)
plug(:no_auth_or_privacy_expectations_api)
end
pipeline :authenticated_api do
plug(:expect_user_authentication)
plug(:no_auth_or_privacy_expectations_api)
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
end
pipeline :admin_api do
plug(:expect_user_authentication)
plug(:base_api)
plug(Pleroma.Web.Plugs.AdminSecretAuthenticationPlug)
plug(:after_auth)
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Web.Plugs.UserIsStaffPlug)
plug(Pleroma.Web.Plugs.IdempotencyPlug)
end
pipeline :require_privileged_staff do
plug(Pleroma.Web.Plugs.EnsureStaffPrivilegedPlug)
end
pipeline :require_admin do
plug(Pleroma.Web.Plugs.UserIsAdminPlug)
end
pipeline :mastodon_html do
plug(:browser)
plug(:authenticate)
plug(:after_auth)
end
pipeline :pleroma_html do
plug(:browser)
plug(:authenticate)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
end
pipeline :well_known do
plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"])
end
pipeline :config do
plug(:accepts, ["json", "xml"])
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :pleroma_api do
plug(:accepts, ["html", "json"])
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :mailbox_preview do
plug(:accepts, ["html"])
plug(:put_secure_browser_headers, %{
"content-security-policy" =>
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' 'unsafe-eval'"
})
end
pipeline :http_signature do
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
end
pipeline :static_fe do
plug(Pleroma.Web.Plugs.StaticFEPlug)
end
scope "/api/v1/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_api)
get("/password_reset/:token", PasswordController, :reset, as: :reset_password)
post("/password_reset", PasswordController, :do_reset, as: :reset_password)
get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha)
get("/healthcheck", UtilController, :healthcheck)
post("/remote_interaction", UtilController, :remote_interaction)
end
scope "/api/v1/pleroma", Pleroma.Web do
pipe_through(:pleroma_api)
post("/uploader_callback/:upload_path", UploaderController, :callback)
end
# AdminAPI: only admins can perform these actions
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through([:admin_api, :require_admin])
put("/users/disable_mfa", AdminAPIController, :disable_mfa)
get("/users/:nickname/permission_group", AdminAPIController, :right_get)
get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
delete(
"/users/:nickname/permission_group/:permission_group",
AdminAPIController,
:right_delete
)
post("/users/permission_group/:permission_group", AdminAPIController, :right_add_multiple)
delete(
"/users/permission_group/:permission_group",
AdminAPIController,
:right_delete_multiple
)
post("/users/follow", UserController, :follow)
post("/users/unfollow", UserController, :unfollow)
post("/users", UserController, :create)
patch("/users/suggest", UserController, :suggest)
patch("/users/unsuggest", UserController, :unsuggest)
get("/relay", RelayController, :index)
post("/relay", RelayController, :follow)
delete("/relay", RelayController, :unfollow)
patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
get("/instance_document/:name", InstanceDocumentController, :show)
patch("/instance_document/:name", InstanceDocumentController, :update)
delete("/instance_document/:name", InstanceDocumentController, :delete)
patch("/users/confirm_email", AdminAPIController, :confirm_email)
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
get("/config", ConfigController, :show)
post("/config", ConfigController, :update)
get("/config/descriptions", ConfigController, :descriptions)
get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart)
get("/oauth_app", OAuthAppController, :index)
post("/oauth_app", OAuthAppController, :create)
patch("/oauth_app/:id", OAuthAppController, :update)
delete("/oauth_app/:id", OAuthAppController, :delete)
get("/media_proxy_caches", MediaProxyCacheController, :index)
post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
post("/media_proxy_caches/purge", MediaProxyCacheController, :purge)
get("/frontends", FrontendController, :index)
post("/frontends/install", FrontendController, :install)
post("/backups", AdminAPIController, :create_backup)
get("/announcements", AnnouncementController, :index)
post("/announcements", AnnouncementController, :create)
get("/announcements/:id", AnnouncementController, :show)
patch("/announcements/:id", AnnouncementController, :change)
delete("/announcements/:id", AnnouncementController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if enabled by config)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through([:admin_api, :require_privileged_staff])
delete("/users", UserController, :delete)
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/statuses", StatusController, :index)
end
# AdminAPI: admins and mods (staff) can perform these actions
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:admin_api)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
patch("/users/:nickname/toggle_activation", UserController, :toggle_activation)
patch("/users/activate", UserController, :activate)
patch("/users/deactivate", UserController, :deactivate)
patch("/users/approve", UserController, :approve)
post("/users/invite_token", InviteController, :create)
get("/users/invites", InviteController, :index)
post("/users/revoke_invite", InviteController, :revoke)
post("/users/email_invite", InviteController, :email)
get("/users", UserController, :index)
get("/users/:nickname", UserController, :show)
get("/instances/:instance/statuses", InstanceController, :list_statuses)
delete("/instances/:instance", InstanceController, :delete)
get("/reports", ReportController, :index)
get("/reports/:id", ReportController, :show)
patch("/reports", ReportController, :update)
post("/reports/:id/notes", ReportController, :notes_create)
delete("/reports/:report_id/notes/:id", ReportController, :notes_delete)
get("/statuses/:id", StatusController, :show)
put("/statuses/:id", StatusController, :update)
delete("/statuses/:id", StatusController, :delete)
get("/moderation_log", AdminAPIController, :list_log)
post("/reload_emoji", AdminAPIController, :reload_emoji)
get("/stats", AdminAPIController, :stats)
end
scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do
scope "/pack" do
pipe_through(:admin_api)
post("/", EmojiPackController, :create)
patch("/", EmojiPackController, :update)
delete("/", EmojiPackController, :delete)
end
scope "/pack" do
pipe_through(:api)
get("/", EmojiPackController, :show)
end
# Modifying packs
scope "/packs" do
pipe_through(:admin_api)
get("/import", EmojiPackController, :import_from_filesystem)
get("/remote", EmojiPackController, :remote)
post("/download", EmojiPackController, :download)
post("/files", EmojiFileController, :create)
patch("/files", EmojiFileController, :update)
delete("/files", EmojiFileController, :delete)
end
# Pack info / downloading
scope "/packs" do
pipe_through(:api)
get("/", EmojiPackController, :index)
get("/archive", EmojiPackController, :archive)
end
end
scope "/", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_html)
post("/main/ostatus", UtilController, :remote_subscribe)
get("/ostatus_subscribe", RemoteFollowController, :follow)
post("/ostatus_subscribe", RemoteFollowController, :do_follow)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:authenticated_api)
post("/change_email", UtilController, :change_email)
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
put("/notification_settings", UtilController, :update_notificaton_settings)
post("/disable_account", UtilController, :disable_account)
post("/move_account", UtilController, :move_account)
put("/aliases", UtilController, :add_alias)
get("/aliases", UtilController, :list_aliases)
delete("/aliases", UtilController, :delete_alias)
end
scope "/api/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
post("/mutes_import", UserImportController, :mutes)
post("/blocks_import", UserImportController, :blocks)
post("/follow_import", UserImportController, :follow)
get("/accounts/mfa", TwoFactorAuthenticationController, :settings)
get("/accounts/mfa/backup_codes", TwoFactorAuthenticationController, :backup_codes)
get("/accounts/mfa/setup/:method", TwoFactorAuthenticationController, :setup)
post("/accounts/mfa/confirm/:method", TwoFactorAuthenticationController, :confirm)
delete("/accounts/mfa/:method", TwoFactorAuthenticationController, :disable)
end
scope "/oauth", Pleroma.Web.OAuth do
# Note: use /api/v1/accounts/verify_credentials for userinfo of signed-in user
get("/registration_details", OAuthController, :registration_details)
post("/mfa/verify", MFAController, :verify, as: :mfa_verify)
get("/mfa", MFAController, :show)
scope [] do
pipe_through(:oauth)
get("/authorize", OAuthController, :authorize)
post("/authorize", OAuthController, :create_authorization)
end
scope [] do
pipe_through(:fetch_session)
post("/token", OAuthController, :token_exchange)
post("/revoke", OAuthController, :token_revoke)
post("/mfa/challenge", MFAController, :challenge)
end
scope [] do
pipe_through(:browser)
get("/prepare_request", OAuthController, :prepare_request)
get("/:provider", OAuthController, :request)
get("/:provider/callback", OAuthController, :callback)
post("/register", OAuthController, :register)
end
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
get("/apps", AppController, :index)
get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index)
get("/statuses/:id/reactions", EmojiReactionController, :index)
end
scope "/api/v0/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
get("/reports", ReportController, :index)
get("/reports/:id", ReportController, :show)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
get("/conversations/:id/statuses", ConversationController, :statuses)
get("/conversations/:id", ConversationController, :show)
post("/conversations/read", ConversationController, :mark_as_read)
patch("/conversations/:id", ConversationController, :update)
put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create)
delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
post("/notifications/read", NotificationController, :mark_as_read)
get("/mascot", MascotController, :show)
put("/mascot", MascotController, :update)
get("/backups", BackupController, :index)
post("/backups", BackupController, :create)
end
scope [] do
pipe_through(:api)
get("/accounts/:id/favourites", AccountController, :favourites)
end
scope [] do
pipe_through(:authenticated_api)
post("/accounts/:id/subscribe", AccountController, :subscribe)
post("/accounts/:id/unsubscribe", AccountController, :unsubscribe)
end
post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
get("/federation_status", InstancesController, :show)
end
scope "/api/v1", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
put("/statuses/:id/emoji_reactions/:emoji", EmojiReactionController, :create)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
get("/accounts/verify_credentials", AccountController, :verify_credentials)
patch("/accounts/update_credentials", AccountController, :update_credentials)
get("/accounts/relationships", AccountController, :relationships)
get("/accounts/:id/lists", AccountController, :lists)
get("/accounts/:id/identity_proofs", AccountController, :identity_proofs)
get("/endorsements", AccountController, :endorsements)
get("/blocks", AccountController, :blocks)
get("/mutes", AccountController, :mutes)
post("/follows", AccountController, :follow_by_uri)
post("/accounts/:id/follow", AccountController, :follow)
post("/accounts/:id/unfollow", AccountController, :unfollow)
post("/accounts/:id/block", AccountController, :block)
post("/accounts/:id/unblock", AccountController, :unblock)
post("/accounts/:id/mute", AccountController, :mute)
post("/accounts/:id/unmute", AccountController, :unmute)
post("/accounts/:id/note", AccountController, :note)
get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :mark_as_read)
delete("/conversations/:id", ConversationController, :delete)
get("/domain_blocks", DomainBlockController, :index)
post("/domain_blocks", DomainBlockController, :create)
delete("/domain_blocks", DomainBlockController, :delete)
get("/filters", FilterController, :index)
post("/filters", FilterController, :create)
get("/filters/:id", FilterController, :show)
put("/filters/:id", FilterController, :update)
delete("/filters/:id", FilterController, :delete)
get("/follow_requests", FollowRequestController, :index)
post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
post("/follow_requests/:id/reject", FollowRequestController, :reject)
get("/lists", ListController, :index)
get("/lists/:id", ListController, :show)
get("/lists/:id/accounts", ListController, :list_accounts)
delete("/lists/:id", ListController, :delete)
post("/lists", ListController, :create)
put("/lists/:id", ListController, :update)
post("/lists/:id/accounts", ListController, :add_to_list)
delete("/lists/:id/accounts", ListController, :remove_from_list)
get("/markers", MarkerController, :index)
post("/markers", MarkerController, :upsert)
post("/media", MediaController, :create)
get("/media/:id", MediaController, :show)
put("/media/:id", MediaController, :update)
get("/notifications", NotificationController, :index)
get("/notifications/:id", NotificationController, :show)
post("/notifications/:id/dismiss", NotificationController, :dismiss)
post("/notifications/clear", NotificationController, :clear)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
post("/polls/:id/votes", PollController, :vote)
post("/reports", ReportController, :create)
get("/scheduled_statuses", ScheduledActivityController, :index)
get("/scheduled_statuses/:id", ScheduledActivityController, :show)
put("/scheduled_statuses/:id", ScheduledActivityController, :update)
delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
# Unlike `GET /api/v1/accounts/:id/favourites`, demands authentication
get("/favourites", StatusController, :favourites)
get("/bookmarks", StatusController, :bookmarks)
post("/statuses", StatusController, :create)
delete("/statuses/:id", StatusController, :delete)
post("/statuses/:id/reblog", StatusController, :reblog)
post("/statuses/:id/unreblog", StatusController, :unreblog)
post("/statuses/:id/favourite", StatusController, :favourite)
post("/statuses/:id/unfavourite", StatusController, :unfavourite)
post("/statuses/:id/pin", StatusController, :pin)
post("/statuses/:id/unpin", StatusController, :unpin)
post("/statuses/:id/bookmark", StatusController, :bookmark)
post("/statuses/:id/unbookmark", StatusController, :unbookmark)
post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :show)
put("/push/subscription", SubscriptionController, :update)
delete("/push/subscription", SubscriptionController, :delete)
get("/suggestions", SuggestionController, :index)
delete("/suggestions/:account_id", SuggestionController, :dismiss)
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)
get("/timelines/list/:list_id", TimelineController, :list)
get("/timelines/bubble", TimelineController, :bubble)
get("/announcements", AnnouncementController, :index)
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
end
scope "/api/web", Pleroma.Web do
pipe_through(:authenticated_api)
# Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere
put("/settings", MastoFEController, :put_settings)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:app_api)
post("/apps", AppController, :create)
get("/apps/verify_credentials", AppController, :verify_credentials)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/accounts/search", SearchController, :account_search)
get("/accounts/lookup", AccountController, :lookup)
get("/accounts/:id/statuses", AccountController, :statuses)
get("/accounts/:id/followers", AccountController, :followers)
get("/accounts/:id/following", AccountController, :following)
get("/accounts/:id", AccountController, :show)
post("/accounts", AccountController, :create)
get("/instance", InstanceController, :show)
get("/instance/peers", InstanceController, :peers)
get("/statuses", StatusController, :index)
get("/statuses/:id", StatusController, :show)
get("/statuses/:id/context", StatusController, :context)
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
get("/custom_emojis", CustomEmojiController, :index)
get("/trends", MastodonAPIController, :empty_array)
get("/timelines/public", TimelineController, :public)
get("/timelines/tag/:tag", TimelineController, :hashtag)
get("/polls/:id", PollController, :show)
get("/directory", DirectoryController, :index)
end
scope "/api/v2", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/search", SearchController, :search2)
post("/media", MediaController, :create2)
get("/suggestions", SuggestionController, :index2)
end
scope "/api", Pleroma.Web do
pipe_through(:config)
get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)
end
scope "/api", Pleroma.Web do
pipe_through(:api)
get(
"/account/confirm_email/:user_id/:token",
TwitterAPI.Controller,
:confirm_email,
as: :confirm_email
)
end
scope "/api" do
pipe_through(:base_api)
get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
end
scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
pipe_through(:authenticated_api)
get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
# Note: http signature is only considered for json requests (no auth for non-json requests)
pipe_through([:accepts_html_json, :http_signature, :static_fe])
get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
# Notice compatibility routes for other frontends
get("/@:nickname/:id", OStatus.OStatusController, :notice)
get("/@:nickname/posts/:id", OStatus.OStatusController, :notice)
get("/:nickname/status/:id", OStatus.OStatusController, :notice)
# Mastodon compatibility routes
get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
# Note: http signature is only considered for json requests (no auth for non-json requests)
pipe_through([:accepts_html_xml_json, :http_signature, :static_fe])
# Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
pipe_through([:accepts_html_xml, :static_fe])
get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
end
scope "/", Pleroma.Web do
pipe_through(:accepts_html)
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
end
scope "/", Pleroma.Web do
pipe_through(:accepts_xml_rss_atom)
get("/tags/:tag", Feed.TagController, :feed, as: :tag_feed)
end
scope "/", Pleroma.Web do
pipe_through(:browser)
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
end
pipeline :ap_service_actor do
plug(:accepts, ["activity+json", "json"])
end
# Server to Server (S2S) AP interactions
pipeline :activitypub do
plug(:ap_service_actor)
plug(:http_signature)
end
# Client to Server (C2S) AP interactions
pipeline :activitypub_client do
plug(:ap_service_actor)
plug(:fetch_session)
plug(:authenticate)
plug(:after_auth)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through([:activitypub_client])
get("/api/ap/whoami", ActivityPubController, :whoami)
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
get("/users/:nickname/outbox", ActivityPubController, :outbox)
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
post("/api/ap/upload_media", ActivityPubController, :upload_media)
# The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`:
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub)
post("/inbox", ActivityPubController, :inbox)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
end
scope "/relay", Pleroma.Web.ActivityPub do
pipe_through(:ap_service_actor)
get("/", ActivityPubController, :relay)
scope [] do
pipe_through(:http_signature)
post("/inbox", ActivityPubController, :inbox)
end
get("/following", ActivityPubController, :relay_following)
get("/followers", ActivityPubController, :relay_followers)
end
scope "/internal/fetch", Pleroma.Web.ActivityPub do
pipe_through(:ap_service_actor)
get("/", ActivityPubController, :internal_fetch)
post("/inbox", ActivityPubController, :inbox)
end
scope "/.well-known", Pleroma.Web do
pipe_through(:well_known)
get("/host-meta", WebFinger.WebFingerController, :host_meta)
get("/webfinger", WebFinger.WebFingerController, :webfinger)
get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas)
end
scope "/nodeinfo", Pleroma.Web do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
scope "/", Pleroma.Web do
pipe_through(:api)
get("/manifest.json", ManifestController, :show)
get("/web/manifest.json", MastoFEController, :manifest)
end
scope "/", Pleroma.Web do
pipe_through(:mastodon_html)
get("/web/login", MastodonAPI.AuthController, :login)
delete("/auth/sign_out", MastodonAPI.AuthController, :logout)
- get("/web/*path", MastoFEController, :index)
+
post("/auth/password", MastodonAPI.AuthController, :password_reset)
+ get("/web/*path", MastoFEController, :index)
+
get("/embed/:id", EmbedController, :show)
end
scope "/proxy/", Pleroma.Web do
get("/preview/:sig/:url", MediaProxy.MediaProxyController, :preview)
get("/preview/:sig/:url/:filename", MediaProxy.MediaProxyController, :preview)
get("/:sig/:url", MediaProxy.MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxy.MediaProxyController, :remote)
end
if Pleroma.Config.get(:env) == :dev do
scope "/dev" do
pipe_through([:mailbox_preview])
forward("/mailbox", Plug.Swoosh.MailboxPreview, base_path: "/dev/mailbox")
end
end
scope "/" do
pipe_through([:pleroma_html, :authenticate, :require_admin])
live_dashboard("/phoenix/live_dashboard")
end
# Test-only routes needed to test action dispatching and plug chain execution
if Pleroma.Config.get(:env) == :test do
@test_actions [
:do_oauth_check,
:fallback_oauth_check,
:skip_oauth_check,
:fallback_oauth_skip_publicity_check,
:skip_oauth_skip_publicity_check,
:missing_oauth_check_definition
]
scope "/test/api", Pleroma.Tests do
pipe_through(:api)
for action <- @test_actions do
get("/#{action}", AuthTestController, action)
end
end
scope "/test/authenticated_api", Pleroma.Tests do
pipe_through(:authenticated_api)
for action <- @test_actions do
get("/#{action}", AuthTestController, action)
end
end
end
scope "/", Pleroma.Web.MongooseIM do
get("/user_exists", MongooseIMController, :user_exists)
get("/check_password", MongooseIMController, :check_password)
end
scope "/", Pleroma.Web.Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
match(:*, "/api/pleroma*path", LegacyPleromaApiRerouterPlug, [])
get("/api*path", RedirectController, :api_not_implemented)
get("/*path", RedirectController, :redirector_with_preload)
options("/*path", RedirectController, :empty)
end
# TODO: Change to Phoenix.Router.routes/1 for Phoenix 1.6.0+
def get_api_routes do
__MODULE__.__routes__()
|> Enum.reject(fn r -> r.plug == Pleroma.Web.Fallback.RedirectController end)
|> Enum.map(fn r ->
r.path
|> String.split("/", trim: true)
|> List.first()
end)
|> Enum.uniq()
end
end
diff --git a/mix.exs b/mix.exs
index 170276b0a..e7f491997 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,363 +1,363 @@
defmodule Pleroma.Mixfile do
use Mix.Project
def project do
[
app: :pleroma,
version: version("3.1.0"),
elixir: "~> 1.9",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
elixirc_options: [warnings_as_errors: warnings_as_errors()],
xref: [exclude: [:eldap]],
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps(),
test_coverage: [tool: ExCoveralls],
preferred_cli_env: ["coveralls.html": :test],
# Docs
name: "Akkoma",
homepage_url: "https://akkoma.dev/",
source_url: "https://akkoma.dev/AkkomaGang/akkoma",
docs: [
source_url_pattern: "https://akkoma.dev/AkkomaGang/akkoma/blob/develop/%{path}#L%{line}",
logo: "priv/static/images/logo.png",
extras: ["README.md", "CHANGELOG.md"] ++ Path.wildcard("docs/**/*.md"),
groups_for_extras: [
"Installation manuals": Path.wildcard("docs/installation/*.md"),
Configuration: Path.wildcard("docs/config/*.md"),
Administration: Path.wildcard("docs/admin/*.md"),
"Pleroma's APIs and Mastodon API extensions": Path.wildcard("docs/api/*.md")
],
main: "readme",
output: "priv/static/doc"
],
releases: [
pleroma: [
include_executables_for: [:unix],
applications: [ex_syslogger: :load, syslog: :load, eldap: :transient],
steps: [:assemble, &put_otp_version/1, ©_files/1, ©_nginx_config/1],
config_providers: [{Pleroma.Config.ReleaseRuntimeProvider, []}]
]
]
]
end
def put_otp_version(%{path: target_path} = release) do
File.write!(
Path.join([target_path, "OTP_VERSION"]),
Pleroma.OTPVersion.version()
)
release
end
def copy_files(%{path: target_path} = release) do
File.cp_r!("./rel/files", target_path)
release
end
def copy_nginx_config(%{path: target_path} = release) do
File.cp!(
"./installation/nginx/akkoma.nginx",
Path.join([target_path, "installation", "akkoma.nginx"])
)
release
end
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[
mod: {Pleroma.Application, []},
extra_applications: [
:logger,
:runtime_tools,
:comeonin,
:quack,
:fast_sanitize,
:os_mon,
:ssl
],
included_applications: [:ex_syslogger]
]
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:benchmark), do: ["lib", "benchmarks", "priv/scrubbers"]
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
defp warnings_as_errors, do: System.get_env("CI") == "true"
# Specifies OAuth dependencies.
defp oauth_deps do
oauth_strategy_packages =
System.get_env("OAUTH_CONSUMER_STRATEGIES")
|> to_string()
|> String.split()
|> Enum.map(fn strategy_entry ->
with [_strategy, dependency] <- String.split(strategy_entry, ":") do
dependency
else
[strategy] -> "ueberauth_#{strategy}"
end
end)
for s <- oauth_strategy_packages, do: {String.to_atom(s), ">= 0.0.0"}
end
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.6.11"},
{:tzdata, "~> 1.1.1"},
{:plug_cowboy, "~> 2.5"},
{:phoenix_pubsub, "~> 2.1"},
{:phoenix_ecto, "~> 4.4"},
{:ecto_enum, "~> 1.4"},
{:ecto_sql, "~> 3.8.3"},
{:postgrex, ">= 0.16.3"},
{:oban, "~> 2.12.1"},
{:gettext,
git: "https://github.com/tusooa/gettext.git",
ref: "72fb2496b6c5280ed911bdc3756890e7f38a4808",
override: true},
{:bcrypt_elixir, "~> 2.2"},
{:trailing_format_plug, "~> 0.0.7"},
{:fast_sanitize, "~> 0.2.3"},
{:html_entities, "~> 0.5", override: true},
- {:phoenix_html, "~> 3.0", override: true},
+ {:phoenix_html, "~> 3.1", override: true},
{:calendar, "~> 1.0"},
{:cachex, "~> 3.4"},
{:poison, "~> 3.0", override: true},
{:tesla, "~> 1.4.4", override: true},
{:castore, "~> 0.1"},
{:cowlib, "~> 2.9", override: true},
{:gun, "~> 2.0.0-rc.1", override: true},
{:finch, "~> 0.10.0"},
{:jason, "~> 1.2"},
{:mogrify, "~> 0.9.1"},
{:ex_aws, "~> 2.1.6"},
{:ex_aws_s3, "~> 2.0"},
{:sweet_xml, "~> 0.7.2"},
{:earmark, "~> 1.4.15"},
{:bbcode_pleroma, "~> 0.2.0"},
{:crypt,
git: "https://github.com/msantos/crypt.git",
ref: "f75cd55325e33cbea198fb41fe41871392f8fb76"},
{:cors_plug, "~> 2.0"},
{:web_push_encryption, "~> 0.3.1"},
- {:swoosh, "~> 1.3"},
+ {:swoosh, "~> 1.0"},
{:phoenix_swoosh, "~> 0.3"},
{:gen_smtp, "~> 0.13"},
{:ex_syslogger, "~> 1.4"},
{:floki, "~> 0.27"},
{:timex, "~> 3.6"},
{:ueberauth, "~> 0.4"},
{:linkify,
git: "https://akkoma.dev/AkkomaGang/linkify.git", branch: "bugfix/line-ending-buffer"},
{:http_signatures, "~> 0.1.1"},
{:telemetry, "~> 0.3"},
{:poolboy, "~> 1.5"},
{:recon, "~> 2.5"},
{:quack, "~> 0.1.1"},
{:joken, "~> 2.0"},
{:benchee, "~> 1.0"},
{:pot, "~> 1.0"},
{:ex_const, "~> 0.2"},
{:plug_static_index_html, "~> 1.0.0"},
{:flake_id, "~> 0.1.0"},
{:concurrent_limiter, "~> 0.1.1"},
{:remote_ip,
git: "https://git.pleroma.social/pleroma/remote_ip.git",
ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"},
{:captcha,
git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
{:restarter, path: "./restarter"},
{:majic, "~> 1.0"},
{:eblurhash, "~> 1.2.2"},
{:open_api_spex, "3.10.0"},
{:search_parser,
git: "https://github.com/FloatingGhost/pleroma-contrib-search-parser.git",
ref: "08971a81e68686f9ac465cfb6661d51c5e4e1e7f"},
{:nimble_parsec, "~> 1.0", override: true},
{:phoenix_live_dashboard, "~> 0.6.2"},
{:ecto_psql_extras, "~> 0.6"},
{:elasticsearch,
git: "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", ref: "main"},
{:mfm_parser,
git: "https://akkoma.dev/AkkomaGang/mfm-parser.git",
ref: "5054e0ba1ebcbd9a7916aec219528e3e58057241"},
# indirect dependency version override
{:plug, "~> 1.10.4", override: true},
## dev & test
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
{:ex_machina, "~> 2.4", only: :test},
{:credo, "~> 1.6", only: [:dev, :test], runtime: false},
{:mock, "~> 0.3.5", only: :test},
# temporary downgrade for excoveralls, hackney until hackney max_connections bug will be fixed
{:excoveralls, "0.12.3", only: :test},
{:mox, "~> 1.0", only: :test},
{:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test}
] ++ oauth_deps()
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to create, migrate and run the seeds file at once:
#
# $ mix ecto.setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
"ecto.migrate": ["pleroma.ecto.migrate"],
"ecto.rollback": ["pleroma.ecto.rollback"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate", "test"],
docs: ["pleroma.docs", "docs"],
analyze: ["credo --strict --only=warnings,todo,fixme,consistency,readability"],
copyright: &add_copyright/1,
"copyright.bump": &bump_copyright/1
]
end
# Builds a version string made of:
# * the application version
# * a pre-release if ahead of the tag: the describe string (-count-commithash)
# * branch name
# * build metadata:
# * a build name if `PLEROMA_BUILD_NAME` or `:pleroma, :build_name` is defined
# * the mix environment if different than prod
defp version(version) do
identifier_filter = ~r/[^0-9a-z\-]+/i
git_available? = match?({_output, 0}, System.cmd("sh", ["-c", "command -v git"]))
dotgit_present? = File.exists?(".git")
git_pre_release =
if git_available? and dotgit_present? do
{tag, tag_err} =
System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true)
{describe, describe_err} = System.cmd("git", ["describe", "--tags", "--abbrev=8"])
{commit_hash, commit_hash_err} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
# Pre-release version, denoted from patch version with a hyphen
cond do
tag_err == 0 and describe_err == 0 ->
describe
|> String.trim()
|> String.replace(String.trim(tag), "")
|> String.trim_leading("-")
|> String.trim()
commit_hash_err == 0 ->
"0-g" <> String.trim(commit_hash)
true ->
nil
end
end
# Branch name as pre-release version component, denoted with a dot
branch_name =
with true <- git_available?,
true <- dotgit_present?,
{branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
branch_name <- String.trim(branch_name),
branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
true <-
!Enum.any?(["master", "HEAD", "release/", "stable"], fn name ->
String.starts_with?(name, branch_name)
end) do
branch_name =
branch_name
|> String.trim()
|> String.replace(identifier_filter, "-")
branch_name
else
_ -> ""
end
build_name =
cond do
name = Application.get_env(:pleroma, :build_name) -> name
name = System.get_env("PLEROMA_BUILD_NAME") -> name
true -> nil
end
env_name = if Mix.env() != :prod, do: to_string(Mix.env())
env_override = System.get_env("PLEROMA_BUILD_ENV")
env_name =
case env_override do
nil -> env_name
env_override when env_override in ["", "prod"] -> nil
env_override -> env_override
end
# Pre-release version, denoted by appending a hyphen
# and a series of dot separated identifiers
pre_release =
[git_pre_release, branch_name]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join(".")
|> (fn
"" -> nil
string -> "-" <> String.replace(string, identifier_filter, "-")
end).()
# Build metadata, denoted with a plus sign
build_metadata =
[build_name, env_name]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join(".")
|> (fn
"" -> nil
string -> "+" <> String.replace(string, identifier_filter, "-")
end).()
[version, pre_release, build_metadata]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join()
end
defp add_copyright(_) do
year = NaiveDateTime.utc_now().year
template = ~s[\
# Pleroma: A lightweight social networking server
# Copyright © 2017-#{year} Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# Akkoma: The cooler pleroma
# Copyright © 2022-#{year} Akkoma Authors <https://akkoma.dev/>
# SPDX-License-Identifier: AGPL-3.0-only
] |> String.replace("\n", "\\n")
find = "find lib test priv -type f \\( -name '*.ex' -or -name '*.exs' \\) -exec "
grep = "grep -L '# Copyright © [0-9\-]* Pleroma' {} \\;"
xargs = "xargs -n1 sed -i'' '1s;^;#{template};'"
:os.cmd(String.to_charlist("#{find}#{grep} | #{xargs}"))
end
defp bump_copyright(_) do
year = NaiveDateTime.utc_now().year
find = "find lib test priv -type f \\( -name '*.ex' -or -name '*.exs' \\)"
xargs =
"xargs sed -i'' 's;# Copyright © [0-9\-]* Pleroma.*$;# Copyright © 2017-#{year} Pleroma Authors <https://pleroma.social/>;'"
:os.cmd(String.to_charlist("#{find} | #{xargs}"))
end
end
diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs
index 4e197a485..0fdd5b8e9 100644
--- a/test/pleroma/web/o_auth/o_auth_controller_test.exs
+++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs
@@ -1,1270 +1,1307 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.OAuthControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.MFA
alias Pleroma.MFA.TOTP
alias Pleroma.Repo
alias Pleroma.User
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])
clear_config([:instance, :account_approval_required])
end
describe "in OAuth consumer mode, " do
setup do
[
app: insert(:oauth_app),
conn:
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
]
end
setup do: clear_config([:auth, :oauth_consumer_strategies], ~w(twitter facebook))
test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{
app: app,
conn: conn
} do
conn =
get(
conn,
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read"
}
)
assert response = html_response(conn, 200)
assert response =~ "Sign in with Twitter"
assert response =~ o_auth_path(conn, :prepare_request)
end
test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{
app: app,
conn: conn
} do
conn =
get(
conn,
"/oauth/prepare_request",
%{
"provider" => "twitter",
"authorization" => %{
"scope" => "read follow",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "a_state"
}
}
)
assert html_response(conn, 302)
redirect_query = URI.parse(redirected_to(conn)).query
assert %{"state" => state_param} = URI.decode_query(redirect_query)
assert {:ok, state_components} = Jason.decode(state_param)
expected_client_id = app.client_id
expected_redirect_uri = app.redirect_uris
assert %{
"scope" => "read follow",
"client_id" => ^expected_client_id,
"redirect_uri" => ^expected_redirect_uri,
"state" => "a_state"
} = state_components
end
test "with user-bound registration, GET /oauth/<provider>/callback redirects to `redirect_uri` with `code`",
%{app: app, conn: conn} do
registration = insert(:registration)
redirect_uri = OAuthController.default_redirect_uri(app)
state_params = %{
"scope" => Enum.join(app.scopes, " "),
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => ""
}
conn =
conn
|> assign(:ueberauth_auth, %{provider: registration.provider, uid: registration.uid})
|> get(
"/oauth/twitter/callback",
%{
"oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
"oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
"provider" => "twitter",
"state" => Jason.encode!(state_params)
}
)
assert html_response(conn, 302)
assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
end
test "with user-unbound registration, GET /oauth/<provider>/callback renders registration_details page",
%{app: app, conn: conn} do
user = insert(:user)
state_params = %{
"scope" => "read write",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "a_state"
}
conn =
conn
|> assign(:ueberauth_auth, %{
provider: "twitter",
uid: "171799000",
info: %{nickname: user.nickname, email: user.email, name: user.name, description: nil}
})
|> get(
"/oauth/twitter/callback",
%{
"oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
"oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
"provider" => "twitter",
"state" => Jason.encode!(state_params)
}
)
assert response = html_response(conn, 200)
assert response =~ ~r/name="op" type="submit" value="register"/
assert response =~ ~r/name="op" type="submit" value="connect"/
assert response =~ user.email
assert response =~ user.nickname
end
test "on authentication error, GET /oauth/<provider>/callback redirects to `redirect_uri`", %{
app: app,
conn: conn
} do
state_params = %{
"scope" => Enum.join(app.scopes, " "),
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => ""
}
conn =
conn
|> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]})
|> get(
"/oauth/twitter/callback",
%{
"oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
"oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
"provider" => "twitter",
"state" => Jason.encode!(state_params)
}
)
assert html_response(conn, 302)
assert redirected_to(conn) == app.redirect_uris
assert get_flash(conn, :error) == "Failed to authenticate: (error description)."
end
test "GET /oauth/registration_details renders registration details form", %{
app: app,
conn: conn
} do
conn =
get(
conn,
"/oauth/registration_details",
%{
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "a_state",
"nickname" => nil,
"email" => "john@doe.com"
}
}
)
assert response = html_response(conn, 200)
assert response =~ ~r/name="op" type="submit" value="register"/
assert response =~ ~r/name="op" type="submit" value="connect"/
end
test "with valid params, POST /oauth/register?op=register redirects to `redirect_uri` with `code`",
%{
app: app,
conn: conn
} do
registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
redirect_uri = OAuthController.default_redirect_uri(app)
conn =
conn
|> put_session(:registration_id, registration.id)
|> post(
"/oauth/register",
%{
"op" => "register",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => "a_state",
"nickname" => "availablenick",
"email" => "available@email.com"
}
}
)
assert html_response(conn, 302)
assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
end
test "with unlisted `redirect_uri`, POST /oauth/register?op=register results in HTTP 401",
%{
app: app,
conn: conn
} do
registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
unlisted_redirect_uri = "http://cross-site-request.com"
conn =
conn
|> put_session(:registration_id, registration.id)
|> post(
"/oauth/register",
%{
"op" => "register",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => unlisted_redirect_uri,
"state" => "a_state",
"nickname" => "availablenick",
"email" => "available@email.com"
}
}
)
assert html_response(conn, 401)
end
test "with invalid params, POST /oauth/register?op=register renders registration_details page",
%{
app: app,
conn: conn
} do
another_user = insert(:user)
registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
params = %{
"op" => "register",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "a_state",
"nickname" => "availablenickname",
"email" => "available@email.com"
}
}
for {bad_param, bad_param_value} <-
[{"nickname", another_user.nickname}, {"email", another_user.email}] do
bad_registration_attrs = %{
"authorization" => Map.put(params["authorization"], bad_param, bad_param_value)
}
bad_params = Map.merge(params, bad_registration_attrs)
conn =
conn
|> put_session(:registration_id, registration.id)
|> post("/oauth/register", bad_params)
assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/
assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken."
end
end
test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`",
%{
app: app,
conn: conn
} do
user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("testpassword"))
registration = insert(:registration, user: nil)
redirect_uri = OAuthController.default_redirect_uri(app)
conn =
conn
|> put_session(:registration_id, registration.id)
|> post(
"/oauth/register",
%{
"op" => "connect",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => "a_state",
"name" => user.nickname,
"password" => "testpassword"
}
}
)
assert html_response(conn, 302)
assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
end
test "with unlisted `redirect_uri`, POST /oauth/register?op=connect results in HTTP 401`",
%{
app: app,
conn: conn
} do
user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("testpassword"))
registration = insert(:registration, user: nil)
unlisted_redirect_uri = "http://cross-site-request.com"
conn =
conn
|> put_session(:registration_id, registration.id)
|> post(
"/oauth/register",
%{
"op" => "connect",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => unlisted_redirect_uri,
"state" => "a_state",
"name" => user.nickname,
"password" => "testpassword"
}
}
)
assert html_response(conn, 401)
end
test "with invalid params, POST /oauth/register?op=connect renders registration_details page",
%{
app: app,
conn: conn
} do
user = insert(:user)
registration = insert(:registration, user: nil)
params = %{
"op" => "connect",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "a_state",
"name" => user.nickname,
"password" => "wrong password"
}
}
conn =
conn
|> put_session(:registration_id, registration.id)
|> post("/oauth/register", params)
assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/
assert get_flash(conn, :error) == "Invalid Username/Password"
end
end
describe "GET /oauth/authorize" do
setup do
[
app: insert(:oauth_app, redirect_uris: "https://redirect.url"),
conn:
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
]
end
test "renders authentication page", %{app: app, conn: conn} do
conn =
get(
conn,
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read"
}
)
assert html_response(conn, 200) =~ ~s(type="submit")
end
test "properly handles internal calls with `authorization`-wrapped params", %{
app: app,
conn: conn
} do
conn =
get(
conn,
"/oauth/authorize",
%{
"authorization" => %{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read"
}
}
)
assert html_response(conn, 200) =~ ~s(type="submit")
end
test "renders authentication page if user is already authenticated but `force_login` is tru-ish",
%{app: app, conn: conn} do
token = insert(:oauth_token, app: app)
conn =
conn
- |> put_req_header("authorization", "Bearer #{token.token}")
+ |> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read",
"force_login" => "true"
}
)
assert html_response(conn, 200) =~ ~s(type="submit")
end
test "renders authentication page if user is already authenticated but user request with another client",
%{
app: app,
conn: conn
} do
token = insert(:oauth_token, app: app)
conn =
conn
- |> put_req_header("authorization", "Bearer #{token.token}")
+ |> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => "another_client_id",
"redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read"
}
)
assert html_response(conn, 200) =~ ~s(type="submit")
end
test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params",
%{
app: app,
conn: conn
} do
token = insert(:oauth_token, app: app)
conn =
conn
- |> put_req_header("authorization", "Bearer #{token.token}")
+ |> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "specific_client_state",
"scope" => "read"
}
)
assert URI.decode(redirected_to(conn)) ==
"https://redirect.url?access_token=#{token.token}&state=specific_client_state"
end
test "with existing authentication and unlisted non-OOB `redirect_uri`, redirects without credentials",
%{
app: app,
conn: conn
} do
unlisted_redirect_uri = "http://cross-site-request.com"
token = insert(:oauth_token, app: app)
conn =
conn
- |> put_req_header("authorization", "Bearer #{token.token}")
+ |> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => unlisted_redirect_uri,
"state" => "specific_client_state",
"scope" => "read"
}
)
assert redirected_to(conn) == unlisted_redirect_uri
end
test "with existing authentication and OOB `redirect_uri`, redirects to app with `token` and `state` params",
%{
app: app,
conn: conn
} do
token = insert(:oauth_token, app: app)
conn =
conn
- |> put_req_header("authorization", "Bearer #{token.token}")
+ |> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
"scope" => "read"
}
)
assert html_response(conn, 200) =~ "Authorization exists"
end
end
describe "POST /oauth/authorize" do
test "redirects with oauth authorization, " <>
"granting requested app-supported scopes to both admin- and non-admin users" do
app_scopes = ["read", "write", "admin", "secret_scope"]
app = insert(:oauth_app, scopes: app_scopes)
redirect_uri = OAuthController.default_redirect_uri(app)
non_admin = insert(:user, is_admin: false)
admin = insert(:user, is_admin: true)
scopes_subset = ["read:subscope", "write", "admin"]
# In case scope param is missing, expecting _all_ app-supported scopes to be granted
for user <- [non_admin, admin],
{requested_scopes, expected_scopes} <-
%{scopes_subset => scopes_subset, nil: app_scopes} do
conn =
post(
build_conn(),
"/oauth/authorize",
%{
"authorization" => %{
"name" => user.nickname,
"password" => "test",
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"scope" => requested_scopes,
"state" => "statepassed"
}
}
)
target = redirected_to(conn)
assert target =~ redirect_uri
query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
assert %{"state" => "statepassed", "code" => code} = query
auth = Repo.get_by(Authorization, token: code)
assert auth
assert auth.scopes == expected_scopes
end
end
+ test "authorize from cookie" do
+ user = insert(:user)
+ app = insert(:oauth_app)
+ oauth_token = insert(:oauth_token, user: user, app: app)
+ redirect_uri = OAuthController.default_redirect_uri(app)
+
+ conn =
+ build_conn()
+ |> Plug.Session.call(Plug.Session.init(@session_opts))
+ |> fetch_session()
+ |> AuthHelper.put_session_token(oauth_token.token)
+ |> post(
+ "/oauth/authorize",
+ %{
+ "authorization" => %{
+ "name" => user.nickname,
+ "client_id" => app.client_id,
+ "redirect_uri" => redirect_uri,
+ "scope" => app.scopes,
+ "state" => "statepassed"
+ }
+ }
+ )
+
+ target = redirected_to(conn)
+ assert target =~ redirect_uri
+
+ query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
+
+ assert %{"state" => "statepassed", "code" => code} = query
+ auth = Repo.get_by(Authorization, token: code)
+ assert auth
+ assert auth.scopes == app.scopes
+ end
+
test "redirect to on two-factor auth page" do
otp_secret = TOTP.generate_secret()
user =
insert(:user,
multi_factor_authentication_settings: %MFA.Settings{
enabled: true,
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
}
)
app = insert(:oauth_app, scopes: ["read", "write", "follow"])
conn =
build_conn()
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "test",
"client_id" => app.client_id,
"redirect_uri" => app.redirect_uris,
"scope" => "read write",
"state" => "statepassed"
}
})
result = html_response(conn, 200)
mfa_token = Repo.get_by(MFA.Token, user_id: user.id)
assert result =~ app.redirect_uris
assert result =~ "statepassed"
assert result =~ mfa_token.token
assert result =~ "Two-factor authentication"
end
test "returns 401 for wrong credentials", %{conn: conn} do
user = insert(:user)
app = insert(:oauth_app)
redirect_uri = OAuthController.default_redirect_uri(app)
result =
conn
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "wrong",
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => "statepassed",
"scope" => Enum.join(app.scopes, " ")
}
})
|> html_response(:unauthorized)
# Keep the details
assert result =~ app.client_id
assert result =~ redirect_uri
# Error message
assert result =~ "Invalid Username/Password"
end
test "returns 401 for missing scopes" do
user = insert(:user, is_admin: false)
app = insert(:oauth_app, scopes: ["read", "write", "admin"])
redirect_uri = OAuthController.default_redirect_uri(app)
result =
build_conn()
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "test",
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => "statepassed",
"scope" => ""
}
})
|> html_response(:unauthorized)
# Keep the details
assert result =~ app.client_id
assert result =~ redirect_uri
# Error message
assert result =~ "This action is outside the authorized scopes"
end
test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
redirect_uri = OAuthController.default_redirect_uri(app)
result =
conn
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "test",
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => "statepassed",
"scope" => "read write follow"
}
})
|> html_response(:unauthorized)
# Keep the details
assert result =~ app.client_id
assert result =~ redirect_uri
# Error message
assert result =~ "This action is outside the authorized scopes"
end
end
describe "POST /oauth/token" do
test "issues a token for an all-body request" do
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => auth.token,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert %{"access_token" => token, "me" => ap_id} = json_response(conn, 200)
token = Repo.get_by(Token, token: token)
assert token
assert token.scopes == auth.scopes
assert user.ap_id == ap_id
end
test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do
password = "testpassword"
user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
app = insert(:oauth_app, scopes: ["read", "write"])
# Note: "scope" param is intentionally omitted
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert %{"id" => id, "access_token" => access_token} = json_response(conn, 200)
token = Repo.get_by(Token, token: access_token)
assert token
assert token.id == id
assert token.token == access_token
assert token.scopes == app.scopes
end
test "issues a mfa token for `password` grant_type, when MFA enabled" do
password = "testpassword"
otp_secret = TOTP.generate_secret()
user =
insert(:user,
password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
multi_factor_authentication_settings: %MFA.Settings{
enabled: true,
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
}
)
app = insert(:oauth_app, scopes: ["read", "write"])
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(403)
assert match?(
%{
"supported_challenge_types" => "totp",
"mfa_token" => _,
"error" => "mfa_required"
},
response
)
token = Repo.get_by(MFA.Token, token: response["mfa_token"])
assert token.user_id == user.id
assert token.authorization_id
end
test "issues a token for request with HTTP basic auth client credentials" do
user = insert(:user)
app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"])
{:ok, auth} = Authorization.create_authorization(app, user, ["scope1", "scope2"])
assert auth.scopes == ["scope1", "scope2"]
app_encoded =
(URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret))
|> Base.encode64()
conn =
build_conn()
|> put_req_header("authorization", "Basic " <> app_encoded)
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => auth.token,
"redirect_uri" => OAuthController.default_redirect_uri(app)
})
assert %{"access_token" => token, "scope" => scope} = json_response(conn, 200)
assert scope == "scope1 scope2"
token = Repo.get_by(Token, token: token)
assert token
assert token.scopes == ["scope1", "scope2"]
end
test "issue a token for client_credentials grant type" do
app = insert(:oauth_app, scopes: ["read", "write"])
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "client_credentials",
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
json_response(conn, 200)
assert token
token_from_db = Repo.get_by(Token, token: token)
assert token_from_db
assert refresh
assert scope == "read write"
end
test "rejects token exchange with invalid client credentials" do
user = insert(:user)
app = insert(:oauth_app)
{:ok, auth} = Authorization.create_authorization(app, user)
conn =
build_conn()
|> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=")
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => auth.token,
"redirect_uri" => OAuthController.default_redirect_uri(app)
})
assert resp = json_response(conn, 400)
assert %{"error" => _} = resp
refute Map.has_key?(resp, "access_token")
end
test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do
clear_config([:instance, :account_activation_required], true)
password = "testpassword"
{:ok, user} =
insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
|> User.confirmation_changeset(set_confirmation: false)
|> User.update_and_set_cache()
refute Pleroma.User.account_status(user) == :active
app = insert(:oauth_app)
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert resp = json_response(conn, 403)
assert %{"error" => _} = resp
refute Map.has_key?(resp, "access_token")
end
test "rejects token exchange for valid credentials belonging to deactivated user" do
password = "testpassword"
user =
insert(:user,
password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
is_active: false
)
app = insert(:oauth_app)
resp =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(403)
assert resp == %{
"error" => "Your account is currently disabled",
"identifier" => "account_is_disabled"
}
end
test "rejects token exchange for user with password_reset_pending set to true" do
password = "testpassword"
user =
insert(:user,
password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
password_reset_pending: true
)
app = insert(:oauth_app, scopes: ["read", "write"])
resp =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(403)
assert resp == %{
"error" => "Password reset is required",
"identifier" => "password_reset_required"
}
end
test "rejects token exchange for user with confirmation_pending set to true" do
clear_config([:instance, :account_activation_required], true)
password = "testpassword"
user =
insert(:user,
password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
is_confirmed: false
)
app = insert(:oauth_app, scopes: ["read", "write"])
resp =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(403)
assert resp == %{
"error" => "Your login is missing a confirmed e-mail address",
"identifier" => "missing_confirmed_email"
}
end
test "rejects token exchange for valid credentials belonging to an unapproved user" do
password = "testpassword"
user =
insert(:user,
password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
is_approved: false
)
refute Pleroma.User.account_status(user) == :active
app = insert(:oauth_app)
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert resp = json_response(conn, 403)
assert %{"error" => _} = resp
refute Map.has_key?(resp, "access_token")
end
test "rejects an invalid authorization code" do
app = insert(:oauth_app)
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => "Imobviouslyinvalid",
"redirect_uri" => OAuthController.default_redirect_uri(app),
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert resp = json_response(conn, 400)
assert %{"error" => _} = json_response(conn, 400)
refute Map.has_key?(resp, "access_token")
end
end
describe "POST /oauth/token - refresh token" do
setup do: clear_config([:oauth2, :issue_new_refresh_token])
test "issues a new access token with keep fresh token" do
clear_config([:oauth2, :issue_new_refresh_token], true)
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
{:ok, token} = Token.exchange_token(app, auth)
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => token.refresh_token,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(200)
ap_id = user.ap_id
assert match?(
%{
"scope" => "write",
"token_type" => "Bearer",
"access_token" => _,
"refresh_token" => _,
"me" => ^ap_id
},
response
)
refute Repo.get_by(Token, token: token.token)
new_token = Repo.get_by(Token, token: response["access_token"])
assert new_token.refresh_token == token.refresh_token
assert new_token.scopes == auth.scopes
assert new_token.user_id == user.id
assert new_token.app_id == app.id
end
test "issues a new access token with new fresh token" do
clear_config([:oauth2, :issue_new_refresh_token], false)
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
{:ok, token} = Token.exchange_token(app, auth)
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => token.refresh_token,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(200)
ap_id = user.ap_id
assert match?(
%{
"scope" => "write",
"token_type" => "Bearer",
"access_token" => _,
"refresh_token" => _,
"me" => ^ap_id
},
response
)
refute Repo.get_by(Token, token: token.token)
new_token = Repo.get_by(Token, token: response["access_token"])
refute new_token.refresh_token == token.refresh_token
assert new_token.scopes == auth.scopes
assert new_token.user_id == user.id
assert new_token.app_id == app.id
end
test "returns 400 if we try use access token" do
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
{:ok, token} = Token.exchange_token(app, auth)
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => token.token,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(400)
assert %{"error" => "Invalid credentials"} == response
end
test "returns 400 if refresh_token invalid" do
app = insert(:oauth_app, scopes: ["read", "write"])
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => "token.refresh_token",
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(400)
assert %{"error" => "Invalid credentials"} == response
end
test "issues a new token if token expired" do
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
{:ok, token} = Token.exchange_token(app, auth)
change =
Ecto.Changeset.change(
token,
%{valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -86_400 * 30)}
)
{:ok, access_token} = Repo.update(change)
response =
build_conn()
- |> put_req_header("authorization", "Bearer #{access_token.token}")
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => access_token.refresh_token,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(200)
ap_id = user.ap_id
assert match?(
%{
"scope" => "write",
"token_type" => "Bearer",
"access_token" => _,
"refresh_token" => _,
"me" => ^ap_id
},
response
)
refute Repo.get_by(Token, token: token.token)
token = Repo.get_by(Token, token: response["access_token"])
assert token
assert token.scopes == auth.scopes
assert token.user_id == user.id
assert token.app_id == app.id
end
end
describe "POST /oauth/token - bad request" do
test "returns 500" do
response =
build_conn()
|> post("/oauth/token", %{})
|> json_response(500)
assert %{"error" => "Bad request"} == response
end
end
describe "POST /oauth/revoke" do
test "when authenticated with request token, revokes it and clears it from session" do
oauth_token = insert(:oauth_token)
conn =
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
- |> put_req_header("authorization", "Bearer #{oauth_token.token}")
+ |> AuthHelper.put_session_token(oauth_token.token)
|> post("/oauth/revoke", %{"token" => oauth_token.token})
assert json_response(conn, 200)
+ refute AuthHelper.get_session_token(conn)
assert Token.get_by_token(oauth_token.token) == {:error, :not_found}
end
test "if request is authenticated with a different token, " <>
"revokes requested token but keeps session token" do
user = insert(:user)
oauth_token = insert(:oauth_token, user: user)
other_app_oauth_token = insert(:oauth_token, user: user)
conn =
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
- |> put_req_header("authorization", "Bearer #{oauth_token.token}")
+ |> AuthHelper.put_session_token(oauth_token.token)
|> post("/oauth/revoke", %{"token" => other_app_oauth_token.token})
assert json_response(conn, 200)
+ assert AuthHelper.get_session_token(conn) == oauth_token.token
assert Token.get_by_token(other_app_oauth_token.token) == {:error, :not_found}
end
test "returns 500 on bad request" do
response =
build_conn()
|> post("/oauth/revoke", %{})
|> json_response(500)
assert %{"error" => "Bad request"} == response
end
end
end
diff --git a/test/pleroma/web/plugs/o_auth_plug_test.exs b/test/pleroma/web/plugs/o_auth_plug_test.exs
index caabfc1cb..9e4be5559 100644
--- a/test/pleroma/web/plugs/o_auth_plug_test.exs
+++ b/test/pleroma/web/plugs/o_auth_plug_test.exs
@@ -1,72 +1,128 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.OAuthPlugTest do
use Pleroma.Web.ConnCase, async: true
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.OAuth.Token.Strategy.Revoke
alias Pleroma.Web.Plugs.OAuthPlug
+ alias Plug.Session
import Pleroma.Factory
setup %{conn: conn} do
user = insert(:user)
{:ok, oauth_token} = Token.create(insert(:oauth_app), user)
%{user: user, token: oauth_token, conn: conn}
end
test "it does nothing if a user is assigned", %{conn: conn} do
conn = assign(conn, :user, %Pleroma.User{})
ret_conn = OAuthPlug.call(conn, %{})
assert ret_conn == conn
end
test "with valid token (uppercase) in auth header, it assigns the user", %{conn: conn} = opts do
conn =
conn
|> put_req_header("authorization", "BEARER #{opts[:token].token}")
|> OAuthPlug.call(%{})
assert conn.assigns[:user] == opts[:user]
end
test "with valid token (downcase) in auth header, it assigns the user", %{conn: conn} = opts do
conn =
conn
|> put_req_header("authorization", "bearer #{opts[:token].token}")
|> OAuthPlug.call(%{})
assert conn.assigns[:user] == opts[:user]
end
test "with valid token (downcase) in url parameters, it assigns the user", opts do
conn =
:get
|> build_conn("/?access_token=#{opts[:token].token}")
|> put_req_header("content-type", "application/json")
|> fetch_query_params()
|> OAuthPlug.call(%{})
assert conn.assigns[:user] == opts[:user]
end
test "with valid token (downcase) in body parameters, it assigns the user", opts do
conn =
:post
|> build_conn("/api/v1/statuses", access_token: opts[:token].token, status: "test")
|> OAuthPlug.call(%{})
assert conn.assigns[:user] == opts[:user]
end
test "with invalid token, it does not assign the user", %{conn: conn} do
conn =
conn
|> put_req_header("authorization", "bearer TTTTT")
|> OAuthPlug.call(%{})
refute conn.assigns[:user]
end
+
+ describe "with :oauth_token in session, " do
+ setup %{token: oauth_token, conn: conn} do
+ session_opts = [
+ store: :cookie,
+ key: "_test",
+ signing_salt: "cooldude"
+ ]
+
+ conn =
+ conn
+ |> Session.call(Session.init(session_opts))
+ |> fetch_session()
+ |> AuthHelper.put_session_token(oauth_token.token)
+
+ %{conn: conn}
+ end
+
+ test "if session-stored token matches a valid OAuth token, assigns :user and :token", %{
+ conn: conn,
+ user: user,
+ token: oauth_token
+ } do
+ conn = OAuthPlug.call(conn, %{})
+
+ assert conn.assigns.user && conn.assigns.user.id == user.id
+ assert conn.assigns.token && conn.assigns.token.id == oauth_token.id
+ end
+
+ test "if session-stored token matches an expired OAuth token, does nothing", %{
+ conn: conn,
+ token: oauth_token
+ } do
+ expired_valid_until = NaiveDateTime.add(NaiveDateTime.utc_now(), -3600 * 24, :second)
+
+ oauth_token
+ |> Ecto.Changeset.change(valid_until: expired_valid_until)
+ |> Pleroma.Repo.update()
+
+ ret_conn = OAuthPlug.call(conn, %{})
+ assert ret_conn == conn
+ end
+
+ test "if session-stored token matches a revoked OAuth token, does nothing", %{
+ conn: conn,
+ token: oauth_token
+ } do
+ Revoke.revoke(oauth_token)
+
+ ret_conn = OAuthPlug.call(conn, %{})
+ assert ret_conn == conn
+ end
+ end
end
diff --git a/test/pleroma/web/plugs/set_user_session_id_plug_test.exs b/test/pleroma/web/plugs/set_user_session_id_plug_test.exs
new file mode 100644
index 000000000..9814c80d8
--- /dev/null
+++ b/test/pleroma/web/plugs/set_user_session_id_plug_test.exs
@@ -0,0 +1,43 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do
+ use Pleroma.Web.ConnCase, async: true
+
+ alias Pleroma.Helpers.AuthHelper
+ alias Pleroma.Web.Plugs.SetUserSessionIdPlug
+
+ setup %{conn: conn} do
+ session_opts = [
+ store: :cookie,
+ key: "_test",
+ signing_salt: "cooldude"
+ ]
+
+ conn =
+ conn
+ |> Plug.Session.call(Plug.Session.init(session_opts))
+ |> fetch_session()
+
+ %{conn: conn}
+ end
+
+ test "doesn't do anything if the user isn't set", %{conn: conn} do
+ ret_conn = SetUserSessionIdPlug.call(conn, %{})
+
+ assert ret_conn == conn
+ end
+
+ test "sets session token basing on :token assign", %{conn: conn} do
+ %{user: user, token: oauth_token} = oauth_access(["read"])
+
+ ret_conn =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, oauth_token)
+ |> SetUserSessionIdPlug.call(%{})
+
+ assert AuthHelper.get_session_token(ret_conn) == oauth_token.token
+ end
+end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Sep 19, 6:21 AM (1 d, 14 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1769015
Default Alt Text
(128 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline
Log In to Comment