Merge branch 'auth-improvements' into 'develop'
Cookie auth rework / Auth subsystem refactoring and tweaks Closes pleroma/secteam/pleroma#3 See merge request pleroma/pleroma!3112
This commit is contained in:
commit
477c6c8e55
|
@ -1,8 +1,10 @@
|
|||
*.ex diff=elixir
|
||||
*.exs diff=elixir
|
||||
# At the time of writing all js/css files included
|
||||
# in the repo are minified bundles, and we don't want
|
||||
# to search/diff those as text files.
|
||||
|
||||
priv/static/instance/static.css diff=css
|
||||
|
||||
# Most of js/css files included in the repo are minified bundles,
|
||||
# and we don't want to search/diff those as text files.
|
||||
*.js binary
|
||||
*.js.map binary
|
||||
*.css binary
|
||||
|
|
|
@ -25,6 +25,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- The site title is now injected as a `title` tag like preloads or metadata.
|
||||
- Password reset tokens now are not accepted after a certain age.
|
||||
- Mix tasks to help with displaying and removing ConfigDB entries. See `mix pleroma.config`
|
||||
- OAuth form improvements: users are remembered by their cookie, the CSS is overridable by the admin, and the style has been improved.
|
||||
- OAuth improvements and fixes: more secure session-based authentication (by token that could be revoked anytime), ability to revoke belonging OAuth token from any client etc.
|
||||
|
||||
<details>
|
||||
<summary>API Changes</summary>
|
||||
|
|
|
@ -88,3 +88,8 @@ config :pleroma, :frontend_configurations,
|
|||
Note the extra `static` folder for the terms-of-service.html
|
||||
|
||||
Terms of Service will be shown to all users on the registration page. It's the best place where to write down the rules for your instance. You can modify the rules by adding and changing `$static_dir/static/terms-of-service.html`.
|
||||
|
||||
|
||||
## Styling rendered pages
|
||||
|
||||
To overwrite the CSS stylesheet of the OAuth form and other static pages, you can upload your own CSS file to `instance/static/static.css`. This will completely replace the CSS used by those pages, so it might be a good idea to copy the one from `priv/static/instance/static.css` and make your changes.
|
||||
|
|
|
@ -14,9 +14,9 @@ This document contains notes and guidelines for Pleroma developers.
|
|||
|
||||
For `:api` pipeline routes, it'll be verified whether `OAuthScopesPlug` was called or explicitly skipped, and if it was not then auth information will be dropped for request. Then `EnsurePublicOrAuthenticatedPlug` will be called to ensure that either the instance is not private or user is authenticated (unless explicitly skipped). Such automated checks help to prevent human errors and result in higher security / privacy for users.
|
||||
|
||||
## [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization)
|
||||
## Non-OAuth authentication
|
||||
|
||||
* With HTTP Basic Auth, OAuth scopes check is _not_ performed for any action (since password is provided during the auth, requester is able to obtain a token with full permissions anyways). `Pleroma.Web.Plugs.AuthenticationPlug` and `Pleroma.Web.Plugs.LegacyAuthenticationPlug` both call `Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug(conn)` when password is provided.
|
||||
* With non-OAuth authentication ([HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) or HTTP header- or params-provided auth), OAuth scopes check is _not_ performed for any action (since password is provided during the auth, requester is able to obtain a token with full permissions anyways); auth plugs invoke `Pleroma.Helpers.AuthHelper.skip_oauth(conn)` in this case.
|
||||
|
||||
## Auth-related configuration, OAuth consumer mode etc.
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 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
|
|
@ -2408,4 +2408,8 @@ def sanitize_html(%User{} = user, filter) do
|
|||
|> Map.put(:bio, HTML.filter_tags(user.bio, filter))
|
||||
|> Map.put(:fields, fields)
|
||||
end
|
||||
|
||||
def get_host(%User{ap_id: ap_id} = _user) do
|
||||
URI.parse(ap_id).host
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,6 +20,7 @@ defmodule Pleroma.Web do
|
|||
below.
|
||||
"""
|
||||
|
||||
alias Pleroma.Helpers.AuthHelper
|
||||
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
|
||||
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||
alias Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug
|
||||
|
@ -75,7 +76,7 @@ defp action(conn, params) do
|
|||
defp maybe_drop_authentication_if_oauth_check_ignored(conn) do
|
||||
if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and
|
||||
not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
|
||||
OAuthScopesPlug.drop_auth_info(conn)
|
||||
AuthHelper.drop_auth_info(conn)
|
||||
else
|
||||
conn
|
||||
end
|
||||
|
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.MastoFEController do
|
|||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.MastodonAPI.AuthController
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
|
||||
|
@ -26,27 +28,27 @@ defmodule Pleroma.Web.MastoFEController do
|
|||
)
|
||||
|
||||
@doc "GET /web/*path"
|
||||
def index(%{assigns: %{user: user, token: token}} = conn, _params)
|
||||
when not is_nil(user) and not is_nil(token) do
|
||||
conn
|
||||
|> put_layout(false)
|
||||
|> render("index.html",
|
||||
token: token.token,
|
||||
user: user,
|
||||
custom_emojis: Pleroma.Emoji.get_all()
|
||||
)
|
||||
end
|
||||
|
||||
def index(conn, _params) do
|
||||
conn
|
||||
|> put_session(:return_to, conn.request_path)
|
||||
|> redirect(to: "/web/login")
|
||||
with %{assigns: %{user: %User{} = user, token: %Token{app_id: token_app_id} = token}} <- conn,
|
||||
{:ok, %{id: ^token_app_id}} <- AuthController.local_mastofe_app() do
|
||||
conn
|
||||
|> put_layout(false)
|
||||
|> render("index.html",
|
||||
token: token.token,
|
||||
user: user,
|
||||
custom_emojis: Pleroma.Emoji.get_all()
|
||||
)
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
|> put_session(:return_to, conn.request_path)
|
||||
|> redirect(to: "/web/login")
|
||||
end
|
||||
end
|
||||
|
||||
@doc "GET /web/manifest.json"
|
||||
def manifest(conn, _params) do
|
||||
conn
|
||||
|> render("manifest.json")
|
||||
render(conn, "manifest.json")
|
||||
end
|
||||
|
||||
@doc "PUT /api/web/settings: Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere"
|
||||
|
|
|
@ -25,7 +25,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
alias Pleroma.Web.MastodonAPI.MastodonAPIController
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Web.OAuth.OAuthController
|
||||
alias Pleroma.Web.OAuth.OAuthView
|
||||
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Web.Plugs.RateLimiter
|
||||
|
@ -103,7 +102,7 @@ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
|
|||
{:ok, user} <- TwitterAPI.register_user(params),
|
||||
{_, {:ok, token}} <-
|
||||
{:login, OAuthController.login(user, app, app.scopes)} do
|
||||
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
|
||||
OAuthController.after_token_exchange(conn, %{user: user, token: token})
|
||||
else
|
||||
{:login, {:account_status, :confirmation_pending}} ->
|
||||
json_response(conn, :ok, %{
|
||||
|
|
|
@ -7,10 +7,13 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
|
|||
|
||||
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)
|
||||
|
@ -20,24 +23,35 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
|
|||
@local_mastodon_name "Mastodon-Local"
|
||||
|
||||
@doc "GET /web/login"
|
||||
def login(%{assigns: %{user: %User{}}} = conn, _params) do
|
||||
redirect(conn, to: local_mastodon_root_path(conn))
|
||||
end
|
||||
|
||||
# Local Mastodon FE login init action
|
||||
def login(conn, %{"code" => auth_token}) do
|
||||
with {:ok, app} <- get_or_make_app(),
|
||||
# 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, token} <- Token.exchange_token(app, auth) do
|
||||
{: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
|
||||
|> put_session(:oauth_token, token.token)
|
||||
|> redirect(to: local_mastodon_root_path(conn))
|
||||
|> AuthHelper.put_session_token(oauth_token.token)
|
||||
|> redirect(to: redirect_to)
|
||||
else
|
||||
_ -> redirect_to_oauth_form(conn, params)
|
||||
end
|
||||
end
|
||||
|
||||
# Local Mastodon FE callback action
|
||||
def login(conn, _) do
|
||||
with {:ok, app} <- get_or_make_app() do
|
||||
def login(conn, params) do
|
||||
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))
|
||||
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 =
|
||||
o_auth_path(conn, :authorize,
|
||||
response_type: "code",
|
||||
|
@ -52,9 +66,16 @@ def login(conn, _) do
|
|||
|
||||
@doc "DELETE /auth/sign_out"
|
||||
def logout(conn, _) do
|
||||
conn
|
||||
|> clear_session
|
||||
|> redirect(to: "/")
|
||||
conn =
|
||||
with %{assigns: %{token: %Token{} = oauth_token}} <- 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"
|
||||
|
@ -66,7 +87,7 @@ def password_reset(conn, params) do
|
|||
json_response(conn, :no_content, "")
|
||||
end
|
||||
|
||||
defp local_mastodon_root_path(conn) do
|
||||
defp local_mastodon_post_login_path(conn) do
|
||||
case get_session(conn, :return_to) do
|
||||
nil ->
|
||||
masto_fe_path(conn, :index, ["getting-started"])
|
||||
|
@ -77,9 +98,11 @@ defp local_mastodon_root_path(conn) do
|
|||
end
|
||||
end
|
||||
|
||||
@spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp get_or_make_app do
|
||||
%{client_name: @local_mastodon_name, redirect_uris: "."}
|
||||
|> App.get_or_make(["read", "write", "follow", "push", "admin"])
|
||||
@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
|
||||
|
|
|
@ -13,7 +13,6 @@ defmodule Pleroma.Web.OAuth.MFAController do
|
|||
alias Pleroma.Web.Auth.TOTPAuthenticator
|
||||
alias Pleroma.Web.OAuth.MFAView, as: View
|
||||
alias Pleroma.Web.OAuth.OAuthController
|
||||
alias Pleroma.Web.OAuth.OAuthView
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
|
||||
plug(:fetch_session when action in [:show, :verify])
|
||||
|
@ -75,7 +74,7 @@ def challenge(conn, %{"mfa_token" => mfa_token} = params) do
|
|||
{:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
|
||||
{:ok, _} <- validates_challenge(user, params),
|
||||
{:ok, token} <- Token.exchange_token(app, auth) do
|
||||
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
|
||||
OAuthController.after_token_exchange(conn, %{user: user, token: token})
|
||||
else
|
||||
_error ->
|
||||
conn
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
defmodule Pleroma.Web.OAuth.OAuthController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Helpers.AuthHelper
|
||||
alias Pleroma.Helpers.UriHelper
|
||||
alias Pleroma.Maps
|
||||
alias Pleroma.MFA
|
||||
|
@ -79,6 +80,13 @@ defp do_authorize(%Plug.Conn{} = conn, params) do
|
|||
available_scopes = (app && app.scopes) || []
|
||||
scopes = Scopes.fetch_scopes(params, available_scopes)
|
||||
|
||||
user =
|
||||
with %{assigns: %{user: %User{} = user}} <- conn do
|
||||
user
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
scopes =
|
||||
if scopes == [] do
|
||||
available_scopes
|
||||
|
@ -88,6 +96,8 @@ defp do_authorize(%Plug.Conn{} = conn, params) do
|
|||
|
||||
# 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,
|
||||
|
@ -131,11 +141,13 @@ defp handle_existing_authorization(
|
|||
end
|
||||
end
|
||||
|
||||
def create_authorization(
|
||||
%Plug.Conn{} = conn,
|
||||
%{"authorization" => _} = params,
|
||||
opts \\ []
|
||||
) do
|
||||
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)
|
||||
|
@ -248,7 +260,7 @@ def token_exchange(
|
|||
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
|
||||
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
|
||||
after_token_exchange(conn, %{user: user, token: token})
|
||||
else
|
||||
_error -> render_invalid_credentials_error(conn)
|
||||
end
|
||||
|
@ -260,7 +272,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_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
|
||||
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
|
||||
after_token_exchange(conn, %{user: user, token: token})
|
||||
else
|
||||
error ->
|
||||
handle_token_exchange_error(conn, error)
|
||||
|
@ -275,7 +287,7 @@ def token_exchange(
|
|||
{:ok, app} <- Token.Utils.fetch_app(conn),
|
||||
requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
|
||||
{:ok, token} <- login(user, app, requested_scopes) do
|
||||
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
|
||||
after_token_exchange(conn, %{user: user, token: token})
|
||||
else
|
||||
error ->
|
||||
handle_token_exchange_error(conn, error)
|
||||
|
@ -298,7 +310,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"}
|
|||
with {:ok, app} <- Token.Utils.fetch_app(conn),
|
||||
{:ok, auth} <- Authorization.create_authorization(app, %User{}),
|
||||
{:ok, token} <- Token.exchange_token(app, auth) do
|
||||
json(conn, OAuthView.render("token.json", %{token: token}))
|
||||
after_token_exchange(conn, %{token: token})
|
||||
else
|
||||
_error ->
|
||||
handle_token_exchange_error(conn, :invalid_credentails)
|
||||
|
@ -308,6 +320,12 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"}
|
|||
# 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
|
||||
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)
|
||||
|
@ -361,9 +379,17 @@ defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
|
|||
render_invalid_credentials_error(conn)
|
||||
end
|
||||
|
||||
def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
|
||||
with {:ok, app} <- Token.Utils.fetch_app(conn),
|
||||
{:ok, _token} <- RevokeToken.revoke(app, params) do
|
||||
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
|
||||
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 ->
|
||||
|
|
|
@ -27,6 +27,14 @@ defmodule Pleroma.Web.OAuth.Token do
|
|||
timestamps()
|
||||
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
|
||||
|
||||
@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
|
||||
|
|
|
@ -5,21 +5,14 @@
|
|||
defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug do
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.Helpers.AuthHelper
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Web.Plugs.RateLimiter
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
end
|
||||
|
||||
def secret_token do
|
||||
case Pleroma.Config.get(:admin_token) do
|
||||
blank when blank in [nil, ""] -> nil
|
||||
token -> token
|
||||
end
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
|
||||
|
||||
def call(conn, _) do
|
||||
|
@ -30,7 +23,7 @@ def call(conn, _) do
|
|||
end
|
||||
end
|
||||
|
||||
def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
|
||||
defp authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
|
||||
if admin_token == secret_token() do
|
||||
assign_admin_user(conn)
|
||||
else
|
||||
|
@ -38,7 +31,7 @@ def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
|
|||
end
|
||||
end
|
||||
|
||||
def authenticate(conn) do
|
||||
defp authenticate(conn) do
|
||||
token = secret_token()
|
||||
|
||||
case get_req_header(conn, "x-admin-token") do
|
||||
|
@ -48,10 +41,17 @@ def authenticate(conn) do
|
|||
end
|
||||
end
|
||||
|
||||
defp secret_token do
|
||||
case Pleroma.Config.get(:admin_token) do
|
||||
blank when blank in [nil, ""] -> nil
|
||||
token -> token
|
||||
end
|
||||
end
|
||||
|
||||
defp assign_admin_user(conn) do
|
||||
conn
|
||||
|> assign(:user, %User{is_admin: true})
|
||||
|> OAuthScopesPlug.skip_plug()
|
||||
|> AuthHelper.skip_oauth()
|
||||
end
|
||||
|
||||
defp handle_bad_token(conn) do
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.AuthenticationPlug do
|
||||
@moduledoc "Password authentication plug."
|
||||
|
||||
alias Pleroma.Helpers.AuthHelper
|
||||
alias Pleroma.User
|
||||
|
||||
import Plug.Conn
|
||||
|
@ -11,6 +14,30 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do
|
|||
|
||||
def init(options), do: options
|
||||
|
||||
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
|
||||
|
||||
def call(
|
||||
%{
|
||||
assigns: %{
|
||||
auth_user: %{password_hash: password_hash} = auth_user,
|
||||
auth_credentials: %{password: password}
|
||||
}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
if checkpw(password, password_hash) do
|
||||
{:ok, auth_user} = maybe_update_password(auth_user, password)
|
||||
|
||||
conn
|
||||
|> assign(:user, auth_user)
|
||||
|> AuthHelper.skip_oauth()
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, _), do: conn
|
||||
|
||||
def checkpw(password, "$6" <> _ = password_hash) do
|
||||
:crypt.crypt(password, password_hash) == password_hash
|
||||
end
|
||||
|
@ -40,40 +67,6 @@ def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do
|
|||
def maybe_update_password(user, _), do: {:ok, user}
|
||||
|
||||
defp do_update_password(user, password) do
|
||||
user
|
||||
|> User.password_update_changeset(%{
|
||||
"password" => password,
|
||||
"password_confirmation" => password
|
||||
})
|
||||
|> Pleroma.Repo.update()
|
||||
User.reset_password(user, %{password: password, password_confirmation: password})
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
|
||||
|
||||
def call(
|
||||
%{
|
||||
assigns: %{
|
||||
auth_user: %{password_hash: password_hash} = auth_user,
|
||||
auth_credentials: %{password: password}
|
||||
}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
if checkpw(password, password_hash) do
|
||||
{:ok, auth_user} = maybe_update_password(auth_user, password)
|
||||
|
||||
conn
|
||||
|> assign(:user, auth_user)
|
||||
|> Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug()
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do
|
||||
Pbkdf2.no_user_verify()
|
||||
conn
|
||||
end
|
||||
|
||||
def call(conn, _), do: conn
|
||||
end
|
||||
|
|
|
@ -3,6 +3,12 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.BasicAuthDecoderPlug do
|
||||
@moduledoc """
|
||||
Decodes HTTP Basic Auth information and assigns `:auth_credentials`.
|
||||
|
||||
NOTE: no checks are performed at this step, auth_credentials/username could be easily faked.
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
def init(options) do
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.EnsureUserKeyPlug do
|
||||
import Plug.Conn
|
||||
|
||||
def init(opts) do
|
||||
opts
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: _}} = conn, _), do: conn
|
||||
|
||||
def call(conn, _) do
|
||||
conn
|
||||
|> assign(:user, nil)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,36 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug do
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.Helpers.AuthHelper
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
|
||||
@moduledoc "Ensures presence and consistency of :user and :token assigns."
|
||||
|
||||
def init(opts) do
|
||||
opts
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{id: user_id}} = assigns} = conn, _) do
|
||||
with %Token{user_id: ^user_id} <- assigns[:token] do
|
||||
conn
|
||||
else
|
||||
%Token{} ->
|
||||
# A safety net for abnormal (unexpected) scenario: :token belongs to another user
|
||||
AuthHelper.drop_auth_info(conn)
|
||||
|
||||
_ ->
|
||||
assign(conn, :token, nil)
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, _) do
|
||||
conn
|
||||
|> assign(:user, nil)
|
||||
|> assign(:token, nil)
|
||||
end
|
||||
end
|
|
@ -1,41 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.LegacyAuthenticationPlug do
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.User
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
|
||||
|
||||
def call(
|
||||
%{
|
||||
assigns: %{
|
||||
auth_user: %{password_hash: "$6$" <> _ = password_hash} = auth_user,
|
||||
auth_credentials: %{password: password}
|
||||
}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
with ^password_hash <- :crypt.crypt(password, password_hash),
|
||||
{:ok, user} <-
|
||||
User.reset_password(auth_user, %{password: password, password_confirmation: password}) do
|
||||
conn
|
||||
|> assign(:auth_user, user)
|
||||
|> assign(:user, user)
|
||||
|> Pleroma.Web.Plugs.OAuthScopesPlug.skip_plug()
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, _) do
|
||||
conn
|
||||
end
|
||||
end
|
|
@ -3,6 +3,7 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
|
||||
alias Pleroma.Helpers.AuthHelper
|
||||
alias Pleroma.Signature
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
|
@ -12,6 +13,47 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
|
|||
|
||||
def init(options), do: options
|
||||
|
||||
def call(%{assigns: %{user: %User{}}} = conn, _opts), do: conn
|
||||
|
||||
# if this has payload make sure it is signed by the same actor that made it
|
||||
def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do
|
||||
with actor_id <- Utils.get_ap_id(actor),
|
||||
{:user, %User{} = user} <- {:user, user_from_key_id(conn)},
|
||||
{:user_match, true} <- {:user_match, user.ap_id == actor_id} do
|
||||
conn
|
||||
|> assign(:user, user)
|
||||
|> AuthHelper.skip_oauth()
|
||||
else
|
||||
{:user_match, false} ->
|
||||
Logger.debug("Failed to map identity from signature (payload actor mismatch)")
|
||||
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")
|
||||
assign(conn, :valid_signature, false)
|
||||
|
||||
# remove me once testsuite uses mapped capabilities instead of what we do now
|
||||
{:user, nil} ->
|
||||
Logger.debug("Failed to map identity from signature (lookup failure)")
|
||||
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
# no payload, probably a signed fetch
|
||||
def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
|
||||
with %User{} = user <- user_from_key_id(conn) do
|
||||
conn
|
||||
|> assign(:user, user)
|
||||
|> AuthHelper.skip_oauth()
|
||||
else
|
||||
_ ->
|
||||
Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
|
||||
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
|
||||
assign(conn, :valid_signature, false)
|
||||
end
|
||||
end
|
||||
|
||||
# no signature at all
|
||||
def call(conn, _opts), do: conn
|
||||
|
||||
defp key_id_from_conn(conn) do
|
||||
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
|
||||
|
@ -31,41 +73,4 @@ defp user_from_key_id(conn) do
|
|||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: _}} = conn, _opts), do: conn
|
||||
|
||||
# if this has payload make sure it is signed by the same actor that made it
|
||||
def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = conn, _opts) do
|
||||
with actor_id <- Utils.get_ap_id(actor),
|
||||
{:user, %User{} = user} <- {:user, user_from_key_id(conn)},
|
||||
{:user_match, true} <- {:user_match, user.ap_id == actor_id} do
|
||||
assign(conn, :user, user)
|
||||
else
|
||||
{:user_match, false} ->
|
||||
Logger.debug("Failed to map identity from signature (payload actor mismatch)")
|
||||
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")
|
||||
assign(conn, :valid_signature, false)
|
||||
|
||||
# remove me once testsuite uses mapped capabilities instead of what we do now
|
||||
{:user, nil} ->
|
||||
Logger.debug("Failed to map identity from signature (lookup failure)")
|
||||
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
# no payload, probably a signed fetch
|
||||
def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
|
||||
with %User{} = user <- user_from_key_id(conn) do
|
||||
assign(conn, :user, user)
|
||||
else
|
||||
_ ->
|
||||
Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
|
||||
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
|
||||
assign(conn, :valid_signature, false)
|
||||
end
|
||||
end
|
||||
|
||||
# no signature at all
|
||||
def call(conn, _opts), do: conn
|
||||
end
|
||||
|
|
|
@ -3,9 +3,12 @@
|
|||
# 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
|
||||
|
@ -17,45 +20,26 @@ def init(options), do: options
|
|||
|
||||
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
|
||||
|
||||
def call(%{params: %{"access_token" => access_token}} = conn, _) do
|
||||
with {:ok, user, token_record} <- fetch_user_and_token(access_token) do
|
||||
conn
|
||||
|> assign(:token, token_record)
|
||||
|> assign(:user, user)
|
||||
else
|
||||
_ ->
|
||||
# token found, but maybe only with app
|
||||
with {:ok, app, token_record} <- fetch_app_and_token(access_token) do
|
||||
conn
|
||||
|> assign(:token, token_record)
|
||||
|> assign(:app, app)
|
||||
else
|
||||
_ -> conn
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, _) do
|
||||
case fetch_token_str(conn) do
|
||||
{:ok, token} ->
|
||||
with {:ok, user, token_record} <- fetch_user_and_token(token) do
|
||||
conn
|
||||
|> assign(:token, token_record)
|
||||
|> assign(:user, user)
|
||||
else
|
||||
_ ->
|
||||
# token found, but maybe only with app
|
||||
with {:ok, app, token_record} <- fetch_app_and_token(token) do
|
||||
conn
|
||||
|> assign(:token, token_record)
|
||||
|> assign(:app, app)
|
||||
else
|
||||
_ -> conn
|
||||
end
|
||||
end
|
||||
|
||||
_ ->
|
||||
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
|
||||
|
||||
|
@ -70,7 +54,6 @@ defp fetch_user_and_token(token) do
|
|||
preload: [user: user]
|
||||
)
|
||||
|
||||
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
|
||||
with %Token{user: user} = token_record <- Repo.one(query) do
|
||||
{:ok, user, token_record}
|
||||
end
|
||||
|
@ -86,29 +69,23 @@ defp fetch_app_and_token(token) do
|
|||
end
|
||||
end
|
||||
|
||||
# Gets token from session by :oauth_token key
|
||||
# Gets token string from conn (in params / headers / session)
|
||||
#
|
||||
@spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
|
||||
defp fetch_token_from_session(conn) do
|
||||
case get_session(conn, :oauth_token) do
|
||||
nil -> :no_token_found
|
||||
token -> {:ok, token}
|
||||
end
|
||||
@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
|
||||
|
||||
# Gets token from headers
|
||||
#
|
||||
@spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
|
||||
defp fetch_token_str(%Plug.Conn{} = conn) do
|
||||
headers = get_req_header(conn, "authorization")
|
||||
|
||||
with :no_token_found <- fetch_token_str(headers),
|
||||
do: fetch_token_from_session(conn)
|
||||
with {:ok, token} <- fetch_token_str(headers) do
|
||||
{:ok, token}
|
||||
else
|
||||
_ -> fetch_token_from_session(conn)
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()}
|
||||
defp fetch_token_str([]), do: :no_token_found
|
||||
|
||||
defp fetch_token_str([token | tail]) do
|
||||
trimmed_token = String.trim(token)
|
||||
|
||||
|
@ -117,4 +94,14 @@ defp fetch_token_str([token | tail]) do
|
|||
_ -> 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
|
||||
|
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.Plugs.OAuthScopesPlug do
|
|||
import Pleroma.Web.Gettext
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Helpers.AuthHelper
|
||||
|
||||
use Pleroma.Web, :plug
|
||||
|
||||
|
@ -28,7 +29,7 @@ def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
|
|||
conn
|
||||
|
||||
options[:fallback] == :proceed_unauthenticated ->
|
||||
drop_auth_info(conn)
|
||||
AuthHelper.drop_auth_info(conn)
|
||||
|
||||
true ->
|
||||
missing_scopes = scopes -- matched_scopes
|
||||
|
@ -44,15 +45,6 @@ def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
|
|||
end
|
||||
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
|
||||
|> put_private(:authentication_ignored, true)
|
||||
|> assign(:user, nil)
|
||||
|> assign(:token, nil)
|
||||
end
|
||||
|
||||
@doc "Keeps those of `scopes` which are descendants of `supported_scopes`"
|
||||
def filter_descendants(scopes, supported_scopes) do
|
||||
Enum.filter(
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.SessionAuthenticationPlug do
|
||||
import Plug.Conn
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
end
|
||||
|
||||
def call(conn, _) do
|
||||
with saved_user_id <- get_session(conn, :user_id),
|
||||
%{auth_user: %{id: ^saved_user_id}} <- conn.assigns do
|
||||
conn
|
||||
|> assign(:user, conn.assigns.auth_user)
|
||||
else
|
||||
_ -> conn
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,16 +3,15 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do
|
||||
import Plug.Conn
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Helpers.AuthHelper
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
|
||||
def init(opts) do
|
||||
opts
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{id: id}}} = conn, _) do
|
||||
conn
|
||||
|> put_session(:user_id, id)
|
||||
def call(%{assigns: %{token: %Token{} = oauth_token}} = conn, _) do
|
||||
AuthHelper.put_session_token(conn, oauth_token.token)
|
||||
end
|
||||
|
||||
def call(conn, _), do: conn
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.UserEnabledPlug do
|
||||
import Plug.Conn
|
||||
alias Pleroma.Helpers.AuthHelper
|
||||
alias Pleroma.User
|
||||
|
||||
def init(options) do
|
||||
|
@ -11,9 +11,10 @@ def init(options) do
|
|||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{} = user}} = conn, _) do
|
||||
case User.account_status(user) do
|
||||
:active -> conn
|
||||
_ -> assign(conn, :user, nil)
|
||||
if User.account_status(user) == :active do
|
||||
conn
|
||||
else
|
||||
AuthHelper.drop_auth_info(conn)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,6 +3,12 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.UserFetcherPlug do
|
||||
@moduledoc """
|
||||
Assigns `:auth_user` basing on `:auth_credentials`.
|
||||
|
||||
NOTE: no checks are performed at this step, auth_credentials/username could be easily faked.
|
||||
"""
|
||||
|
||||
alias Pleroma.User
|
||||
import Plug.Conn
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ defmodule Pleroma.Web.Router do
|
|||
plug(:fetch_session)
|
||||
plug(Pleroma.Web.Plugs.OAuthPlug)
|
||||
plug(Pleroma.Web.Plugs.UserEnabledPlug)
|
||||
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
|
||||
end
|
||||
|
||||
pipeline :expect_authentication do
|
||||
|
@ -48,15 +49,13 @@ defmodule Pleroma.Web.Router do
|
|||
plug(Pleroma.Web.Plugs.OAuthPlug)
|
||||
plug(Pleroma.Web.Plugs.BasicAuthDecoderPlug)
|
||||
plug(Pleroma.Web.Plugs.UserFetcherPlug)
|
||||
plug(Pleroma.Web.Plugs.SessionAuthenticationPlug)
|
||||
plug(Pleroma.Web.Plugs.LegacyAuthenticationPlug)
|
||||
plug(Pleroma.Web.Plugs.AuthenticationPlug)
|
||||
end
|
||||
|
||||
pipeline :after_auth do
|
||||
plug(Pleroma.Web.Plugs.UserEnabledPlug)
|
||||
plug(Pleroma.Web.Plugs.SetUserSessionIdPlug)
|
||||
plug(Pleroma.Web.Plugs.EnsureUserKeyPlug)
|
||||
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
|
||||
end
|
||||
|
||||
pipeline :base_api do
|
||||
|
@ -100,7 +99,7 @@ defmodule Pleroma.Web.Router do
|
|||
pipeline :pleroma_html do
|
||||
plug(:browser)
|
||||
plug(:authenticate)
|
||||
plug(Pleroma.Web.Plugs.EnsureUserKeyPlug)
|
||||
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
|
||||
end
|
||||
|
||||
pipeline :well_known do
|
||||
|
@ -292,7 +291,6 @@ defmodule Pleroma.Web.Router do
|
|||
|
||||
post("/main/ostatus", UtilController, :remote_subscribe)
|
||||
get("/ostatus_subscribe", RemoteFollowController, :follow)
|
||||
|
||||
post("/ostatus_subscribe", RemoteFollowController, :do_follow)
|
||||
end
|
||||
|
||||
|
@ -321,20 +319,26 @@ defmodule Pleroma.Web.Router do
|
|||
end
|
||||
|
||||
scope "/oauth", Pleroma.Web.OAuth do
|
||||
scope [] do
|
||||
pipe_through(:oauth)
|
||||
get("/authorize", OAuthController, :authorize)
|
||||
end
|
||||
|
||||
post("/authorize", OAuthController, :create_authorization)
|
||||
post("/token", OAuthController, :token_exchange)
|
||||
post("/revoke", OAuthController, :token_revoke)
|
||||
get("/registration_details", OAuthController, :registration_details)
|
||||
|
||||
post("/mfa/challenge", MFAController, :challenge)
|
||||
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)
|
||||
|
||||
|
|
|
@ -1,233 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
|
||||
<title>
|
||||
<%= Pleroma.Config.get([:instance, :name]) %>
|
||||
</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #121a24;
|
||||
font-family: sans-serif;
|
||||
color: #b9b9ba;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 420px;
|
||||
padding: 20px;
|
||||
background-color: #182230;
|
||||
border-radius: 4px;
|
||||
margin: auto;
|
||||
margin-top: 10vh;
|
||||
box-shadow: 0 1px 4px 0px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #b9b9ba;
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #d8a070;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input {
|
||||
text-align: left;
|
||||
color: #89898a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
input {
|
||||
box-sizing: content-box;
|
||||
padding: 10px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #121a24;
|
||||
color: #b9b9ba;
|
||||
border: 0;
|
||||
transition-property: border-bottom;
|
||||
transition-duration: 0.35s;
|
||||
border-bottom: 2px solid #2a384a;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.scopes-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 1em;
|
||||
text-align: left;
|
||||
color: #89898a;
|
||||
}
|
||||
|
||||
.scopes-input label:first-child {
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.scopes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
text-align: left;
|
||||
color: #b9b9ba;
|
||||
}
|
||||
|
||||
.scope {
|
||||
display: flex;
|
||||
flex-basis: 100%;
|
||||
height: 2em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scope:before {
|
||||
color: #b9b9ba;
|
||||
content: "✔\fe0e";
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
[type="checkbox"] + label {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[type="checkbox"] + label:before {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
color: white;
|
||||
background-color: #121a24;
|
||||
border: 4px solid #121a24;
|
||||
box-shadow: 0px 0px 1px 0 #d8a070;
|
||||
box-sizing: border-box;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
margin-right: 1.0em;
|
||||
content: "";
|
||||
transition-property: background-color;
|
||||
transition-duration: 0.35s;
|
||||
color: #121a24;
|
||||
margin-bottom: -0.2em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
[type="checkbox"]:checked + label:before {
|
||||
background-color: #d8a070;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-bottom: 2px solid #d8a070;
|
||||
}
|
||||
|
||||
button {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
background-color: #1c2a3a;
|
||||
color: #b9b9ba;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
text-transform: uppercase;
|
||||
font-size: 16px;
|
||||
box-shadow: 0px 0px 2px 0px black,
|
||||
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
|
||||
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 0px 0px 1px #d8a070,
|
||||
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
|
||||
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
background-color: #931014;
|
||||
border: 1px solid #a06060;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #7d796a;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 440px) {
|
||||
.container {
|
||||
margin-top: 0
|
||||
}
|
||||
|
||||
.scope {
|
||||
flex-basis: 0%;
|
||||
}
|
||||
|
||||
.scope:before {
|
||||
content: "";
|
||||
margin-left: 0em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.scope:first-child:before {
|
||||
margin-left: 1em;
|
||||
content: "✔\fe0e";
|
||||
}
|
||||
|
||||
.scope:after {
|
||||
content: ",";
|
||||
}
|
||||
|
||||
.scope:last-child:after {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
}
|
||||
.form-row > label {
|
||||
text-align: left;
|
||||
line-height: 47px;
|
||||
flex: 1;
|
||||
}
|
||||
.form-row > input {
|
||||
flex: 2;
|
||||
}
|
||||
</style>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui">
|
||||
<title><%= Pleroma.Config.get([:instance, :name]) %></title>
|
||||
<link rel="stylesheet" href="/instance/static.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="instance-header">
|
||||
<a class="instance-header__content" href="/">
|
||||
<img class="instance-header__thumbnail" src="<%= Pleroma.Config.get([:instance, :instance_thumbnail]) %>">
|
||||
<h1 class="instance-header__title"><%= Pleroma.Config.get([:instance, :name]) %></h1>
|
||||
</a>
|
||||
</div>
|
||||
<div class="container">
|
||||
<h1><%= Pleroma.Config.get([:instance, :name]) %></h1>
|
||||
<%= @inner_content %>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
@ -5,32 +5,55 @@
|
|||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||
<% end %>
|
||||
|
||||
<h2>OAuth Authorization</h2>
|
||||
<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>
|
||||
|
||||
<%= if @params["registration"] in ["true", true] do %>
|
||||
<h3>This is the first time you visit! Please enter your Pleroma handle.</h3>
|
||||
<p>Choose carefully! You won't be able to change this later. You will be able to change your display name, though.</p>
|
||||
<div class="input">
|
||||
<%= label f, :nickname, "Pleroma Handle" %>
|
||||
<%= text_input f, :nickname, placeholder: "lain" %>
|
||||
<%= if @user do %>
|
||||
<div class="account-header">
|
||||
<div class="account-header__banner" style="background-image: url('<%= Pleroma.User.banner_url(@user) %>')"></div>
|
||||
<div class="account-header__avatar" style="background-image: url('<%= Pleroma.User.avatar_url(@user) %>')"></div>
|
||||
<div class="account-header__meta">
|
||||
<div class="account-header__display-name"><%= @user.name %></div>
|
||||
<div class="account-header__nickname">@<%= @user.nickname %>@<%= Pleroma.User.get_host(@user) %></div>
|
||||
</div>
|
||||
</div>
|
||||
<%= hidden_input f, :name, value: @params["name"] %>
|
||||
<%= hidden_input f, :password, value: @params["password"] %>
|
||||
<br>
|
||||
<% else %>
|
||||
<div class="input">
|
||||
<%= label f, :name, "Username" %>
|
||||
<%= text_input f, :name %>
|
||||
</div>
|
||||
<div class="input">
|
||||
<%= label f, :password, "Password" %>
|
||||
<%= password_input f, :password %>
|
||||
</div>
|
||||
<%= submit "Log In" %>
|
||||
<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
|
||||
<% end %>
|
||||
|
||||
<div class="container__content">
|
||||
<%= if @app do %>
|
||||
<p>Application <strong><%= @app.client_name %></strong> is requesting access to your account.</p>
|
||||
<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
|
||||
<% end %>
|
||||
|
||||
<%= if @user do %>
|
||||
<div class="actions">
|
||||
<a class="button button--cancel" href="/">Cancel</a>
|
||||
<%= submit "Approve", class: "button--approve" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= if @params["registration"] in ["true", true] do %>
|
||||
<h3>This is the first time you visit! Please enter your Pleroma handle.</h3>
|
||||
<p>Choose carefully! You won't be able to change this later. You will be able to change your display name, though.</p>
|
||||
<div class="input">
|
||||
<%= label f, :nickname, "Pleroma Handle" %>
|
||||
<%= text_input f, :nickname, placeholder: "lain" %>
|
||||
</div>
|
||||
<%= hidden_input f, :name, value: @params["name"] %>
|
||||
<%= hidden_input f, :password, value: @params["password"] %>
|
||||
<br>
|
||||
<% else %>
|
||||
<div class="input">
|
||||
<%= label f, :name, "Username" %>
|
||||
<%= text_input f, :name %>
|
||||
</div>
|
||||
<div class="input">
|
||||
<%= label f, :password, "Password" %>
|
||||
<%= password_input f, :password %>
|
||||
</div>
|
||||
<%= submit "Log In" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= hidden_input f, :client_id, value: @client_id %>
|
||||
<%= hidden_input f, :response_type, value: @response_type %>
|
||||
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
||||
|
@ -40,4 +63,3 @@
|
|||
<%= if Pleroma.Config.oauth_consumer_enabled?() do %>
|
||||
<%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %>
|
||||
<% end %>
|
||||
|
||||
|
|
Binary file not shown.
|
@ -2176,4 +2176,9 @@ test "avatar fallback" do
|
|||
|
||||
assert User.avatar_url(user, no_default: true) == nil
|
||||
end
|
||||
|
||||
test "get_host/1" do
|
||||
user = insert(:user, ap_id: "https://lain.com/users/lain", nickname: "lain")
|
||||
assert User.get_host(user) == "lain.com"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -941,7 +941,6 @@ test "it resend emails for two users", %{conn: conn, admin: admin} do
|
|||
|
||||
describe "/api/pleroma/admin/stats" do
|
||||
test "status visibility count", %{conn: conn} do
|
||||
admin = insert(:user, is_admin: true)
|
||||
user = insert(:user)
|
||||
CommonAPI.post(user, %{visibility: "public", status: "hey"})
|
||||
CommonAPI.post(user, %{visibility: "unlisted", status: "hey"})
|
||||
|
@ -949,7 +948,6 @@ test "status visibility count", %{conn: conn} do
|
|||
|
||||
response =
|
||||
conn
|
||||
|> assign(:user, admin)
|
||||
|> get("/api/pleroma/admin/stats")
|
||||
|> json_response(200)
|
||||
|
||||
|
@ -958,7 +956,6 @@ test "status visibility count", %{conn: conn} do
|
|||
end
|
||||
|
||||
test "by instance", %{conn: conn} do
|
||||
admin = insert(:user, is_admin: true)
|
||||
user1 = insert(:user)
|
||||
instance2 = "instance2.tld"
|
||||
user2 = insert(:user, %{ap_id: "https://#{instance2}/@actor"})
|
||||
|
@ -969,7 +966,6 @@ test "by instance", %{conn: conn} do
|
|||
|
||||
response =
|
||||
conn
|
||||
|> assign(:user, admin)
|
||||
|> get("/api/pleroma/admin/stats", instance: instance2)
|
||||
|> json_response(200)
|
||||
|
||||
|
|
|
@ -1417,11 +1417,7 @@ test "enables the welcome messages", %{conn: conn} do
|
|||
|
||||
describe "GET /api/pleroma/admin/config/descriptions" do
|
||||
test "structure", %{conn: conn} do
|
||||
admin = insert(:user, is_admin: true)
|
||||
|
||||
conn =
|
||||
assign(conn, :user, admin)
|
||||
|> get("/api/pleroma/admin/config/descriptions")
|
||||
conn = get(conn, "/api/pleroma/admin/config/descriptions")
|
||||
|
||||
assert [child | _others] = json_response_and_validate_schema(conn, 200)
|
||||
|
||||
|
@ -1439,11 +1435,7 @@ test "filters by database configuration whitelist", %{conn: conn} do
|
|||
{:esshd}
|
||||
])
|
||||
|
||||
admin = insert(:user, is_admin: true)
|
||||
|
||||
conn =
|
||||
assign(conn, :user, admin)
|
||||
|> get("/api/pleroma/admin/config/descriptions")
|
||||
conn = get(conn, "/api/pleroma/admin/config/descriptions")
|
||||
|
||||
children = json_response_and_validate_schema(conn, 200)
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ test "redirects to the saved path after log in", %{conn: conn, path: path} do
|
|||
|> get("/web/login", %{code: auth.token})
|
||||
|
||||
assert conn.status == 302
|
||||
assert redirected_to(conn) == path
|
||||
assert redirected_to(conn) =~ path
|
||||
end
|
||||
|
||||
test "redirects to the getting-started page when referer is not present", %{conn: conn} do
|
||||
|
@ -49,7 +49,7 @@ test "redirects to the getting-started page when referer is not present", %{conn
|
|||
conn = get(conn, "/web/login", %{code: auth.token})
|
||||
|
||||
assert conn.status == 302
|
||||
assert redirected_to(conn) == "/web/getting-started"
|
||||
assert redirected_to(conn) =~ "/web/getting-started"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -64,7 +64,8 @@ test "redirects not logged-in users to the login page on private instances", %{
|
|||
end
|
||||
|
||||
test "does not redirect logged in users to the login page", %{conn: conn, path: path} do
|
||||
token = insert(:oauth_token, scopes: ["read"])
|
||||
{:ok, app} = Pleroma.Web.MastodonAPI.AuthController.local_mastofe_app()
|
||||
token = insert(:oauth_token, app: app, scopes: ["read"])
|
||||
|
||||
conn =
|
||||
conn
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
|
||||
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
|
||||
|
@ -454,7 +456,7 @@ test "renders authentication page if user is already authenticated but `force_lo
|
|||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:oauth_token, token.token)
|
||||
|> AuthHelper.put_session_token(token.token)
|
||||
|> get(
|
||||
"/oauth/authorize",
|
||||
%{
|
||||
|
@ -478,7 +480,7 @@ test "renders authentication page if user is already authenticated but user requ
|
|||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:oauth_token, token.token)
|
||||
|> AuthHelper.put_session_token(token.token)
|
||||
|> get(
|
||||
"/oauth/authorize",
|
||||
%{
|
||||
|
@ -501,7 +503,7 @@ test "with existing authentication and non-OOB `redirect_uri`, redirects to app
|
|||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:oauth_token, token.token)
|
||||
|> AuthHelper.put_session_token(token.token)
|
||||
|> get(
|
||||
"/oauth/authorize",
|
||||
%{
|
||||
|
@ -527,7 +529,7 @@ test "with existing authentication and unlisted non-OOB `redirect_uri`, redirect
|
|||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:oauth_token, token.token)
|
||||
|> AuthHelper.put_session_token(token.token)
|
||||
|> get(
|
||||
"/oauth/authorize",
|
||||
%{
|
||||
|
@ -551,7 +553,7 @@ test "with existing authentication and OOB `redirect_uri`, redirects to app with
|
|||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:oauth_token, token.token)
|
||||
|> AuthHelper.put_session_token(token.token)
|
||||
|> get(
|
||||
"/oauth/authorize",
|
||||
%{
|
||||
|
@ -609,6 +611,41 @@ test "redirects with oauth authorization, " <>
|
|||
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()
|
||||
|
||||
|
@ -1219,8 +1256,43 @@ test "returns 500" do
|
|||
end
|
||||
end
|
||||
|
||||
describe "POST /oauth/revoke - bad request" do
|
||||
test "returns 500" do
|
||||
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()
|
||||
|> 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()
|
||||
|> 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", %{})
|
||||
|
|
|
@ -264,9 +264,10 @@ test "it returns the messages for a given chat", %{conn: conn, user: user} do
|
|||
assert length(result) == 3
|
||||
|
||||
# Trying to get the chat of a different user
|
||||
other_user_chat = Chat.get(other_user.id, user.ap_id)
|
||||
|
||||
conn
|
||||
|> assign(:user, other_user)
|
||||
|> get("/api/v1/pleroma/chats/#{chat.id}/messages")
|
||||
|> get("/api/v1/pleroma/chats/#{other_user_chat.id}/messages")
|
||||
|> json_response_and_validate_schema(404)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -49,6 +49,7 @@ test "with `admin_token` query parameter", %{conn: conn} do
|
|||
|> AdminSecretAuthenticationPlug.call(%{})
|
||||
|
||||
assert conn.assigns[:user].is_admin
|
||||
assert conn.assigns[:token] == nil
|
||||
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
|
||||
end
|
||||
|
||||
|
@ -69,6 +70,7 @@ test "with `x-admin-token` HTTP header", %{conn: conn} do
|
|||
|> AdminSecretAuthenticationPlug.call(%{})
|
||||
|
||||
assert conn.assigns[:user].is_admin
|
||||
assert conn.assigns[:token] == nil
|
||||
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,6 +48,7 @@ test "with a correct password in the credentials, " <>
|
|||
|> AuthenticationPlug.call(%{})
|
||||
|
||||
assert conn.assigns.user == conn.assigns.auth_user
|
||||
assert conn.assigns.token == nil
|
||||
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
|
||||
end
|
||||
|
||||
|
@ -62,6 +63,7 @@ test "with a bcrypt hash, it updates to a pkbdf2 hash", %{conn: conn} do
|
|||
|> AuthenticationPlug.call(%{})
|
||||
|
||||
assert conn.assigns.user.id == conn.assigns.auth_user.id
|
||||
assert conn.assigns.token == nil
|
||||
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
|
||||
|
||||
user = User.get_by_id(user.id)
|
||||
|
@ -83,6 +85,7 @@ test "with a crypt hash, it updates to a pkbdf2 hash", %{conn: conn} do
|
|||
|> AuthenticationPlug.call(%{})
|
||||
|
||||
assert conn.assigns.user.id == conn.assigns.auth_user.id
|
||||
assert conn.assigns.token == nil
|
||||
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
|
||||
|
||||
user = User.get_by_id(user.id)
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.EnsureUserKeyPlugTest do
|
||||
use Pleroma.Web.ConnCase, async: true
|
||||
|
||||
alias Pleroma.Web.Plugs.EnsureUserKeyPlug
|
||||
|
||||
test "if the conn has a user key set, it does nothing", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:user, 1)
|
||||
|
||||
ret_conn =
|
||||
conn
|
||||
|> EnsureUserKeyPlug.call(%{})
|
||||
|
||||
assert conn == ret_conn
|
||||
end
|
||||
|
||||
test "if the conn has no key set, it sets it to nil", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> EnsureUserKeyPlug.call(%{})
|
||||
|
||||
assert Map.has_key?(conn.assigns, :user)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,69 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.EnsureUserTokenAssignsPlugTest do
|
||||
use Pleroma.Web.ConnCase, async: true
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
alias Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug
|
||||
|
||||
test "with :user assign set to a User record " <>
|
||||
"and :token assign set to a Token belonging to this user, " <>
|
||||
"it does nothing" do
|
||||
%{conn: conn} = oauth_access(["read"])
|
||||
|
||||
ret_conn = EnsureUserTokenAssignsPlug.call(conn, %{})
|
||||
|
||||
assert conn == ret_conn
|
||||
end
|
||||
|
||||
test "with :user assign set to a User record " <>
|
||||
"but :token assign not set or not a Token, " <>
|
||||
"it assigns :token to `nil`",
|
||||
%{conn: conn} do
|
||||
user = insert(:user)
|
||||
conn = assign(conn, :user, user)
|
||||
|
||||
ret_conn = EnsureUserTokenAssignsPlug.call(conn, %{})
|
||||
|
||||
assert %{token: nil} = ret_conn.assigns
|
||||
|
||||
ret_conn2 =
|
||||
conn
|
||||
|> assign(:token, 1)
|
||||
|> EnsureUserTokenAssignsPlug.call(%{})
|
||||
|
||||
assert %{token: nil} = ret_conn2.assigns
|
||||
end
|
||||
|
||||
# Abnormal (unexpected) scenario
|
||||
test "with :user assign set to a User record " <>
|
||||
"but :token assign set to a Token NOT belonging to :user, " <>
|
||||
"it drops auth info" do
|
||||
%{conn: conn} = oauth_access(["read"])
|
||||
other_user = insert(:user)
|
||||
|
||||
conn = assign(conn, :user, other_user)
|
||||
|
||||
ret_conn = EnsureUserTokenAssignsPlug.call(conn, %{})
|
||||
|
||||
assert %{user: nil, token: nil} = ret_conn.assigns
|
||||
end
|
||||
|
||||
test "if :user assign is not set to a User record, it sets :user and :token to nil", %{
|
||||
conn: conn
|
||||
} do
|
||||
ret_conn = EnsureUserTokenAssignsPlug.call(conn, %{})
|
||||
|
||||
assert %{user: nil, token: nil} = ret_conn.assigns
|
||||
|
||||
ret_conn2 =
|
||||
conn
|
||||
|> assign(:user, 1)
|
||||
|> EnsureUserTokenAssignsPlug.call(%{})
|
||||
|
||||
assert %{user: nil, token: nil} = ret_conn2.assigns
|
||||
end
|
||||
end
|
|
@ -1,82 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.LegacyAuthenticationPlugTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Plugs.LegacyAuthenticationPlug
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Web.Plugs.PlugHelper
|
||||
|
||||
setup do
|
||||
user =
|
||||
insert(:user,
|
||||
password: "password",
|
||||
password_hash:
|
||||
"$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1"
|
||||
)
|
||||
|
||||
%{user: user}
|
||||
end
|
||||
|
||||
test "it does nothing if a user is assigned", %{conn: conn, user: user} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:auth_credentials, %{username: "dude", password: "password"})
|
||||
|> assign(:auth_user, user)
|
||||
|> assign(:user, %User{})
|
||||
|
||||
ret_conn =
|
||||
conn
|
||||
|> LegacyAuthenticationPlug.call(%{})
|
||||
|
||||
assert ret_conn == conn
|
||||
end
|
||||
|
||||
@tag :skip_on_mac
|
||||
test "if `auth_user` is present and password is correct, " <>
|
||||
"it authenticates the user, resets the password, marks OAuthScopesPlug as skipped",
|
||||
%{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:auth_credentials, %{username: "dude", password: "password"})
|
||||
|> assign(:auth_user, user)
|
||||
|
||||
conn = LegacyAuthenticationPlug.call(conn, %{})
|
||||
|
||||
assert conn.assigns.user.id == user.id
|
||||
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
|
||||
end
|
||||
|
||||
@tag :skip_on_mac
|
||||
test "it does nothing if the password is wrong", %{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:auth_credentials, %{username: "dude", password: "wrong_password"})
|
||||
|> assign(:auth_user, user)
|
||||
|
||||
ret_conn =
|
||||
conn
|
||||
|> LegacyAuthenticationPlug.call(%{})
|
||||
|
||||
assert conn == ret_conn
|
||||
end
|
||||
|
||||
test "with no credentials or user it does nothing", %{conn: conn} do
|
||||
ret_conn =
|
||||
conn
|
||||
|> LegacyAuthenticationPlug.call(%{})
|
||||
|
||||
assert ret_conn == conn
|
||||
end
|
||||
end
|
|
@ -5,43 +5,49 @@
|
|||
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
|
||||
import Pleroma.Factory
|
||||
alias Plug.Session
|
||||
|
||||
@session_opts [
|
||||
store: :cookie,
|
||||
key: "_test",
|
||||
signing_salt: "cooldude"
|
||||
]
|
||||
import Pleroma.Factory
|
||||
|
||||
setup %{conn: conn} do
|
||||
user = insert(:user)
|
||||
{:ok, %{token: token}} = Pleroma.Web.OAuth.Token.create(insert(:oauth_app), user)
|
||||
%{user: user, token: token, conn: conn}
|
||||
{:ok, oauth_token} = Token.create(insert(:oauth_app), user)
|
||||
%{user: user, token: oauth_token, conn: conn}
|
||||
end
|
||||
|
||||
test "with valid token(uppercase), it assigns the user", %{conn: conn} = opts do
|
||||
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]}")
|
||||
|> put_req_header("authorization", "BEARER #{opts[:token].token}")
|
||||
|> OAuthPlug.call(%{})
|
||||
|
||||
assert conn.assigns[:user] == opts[:user]
|
||||
end
|
||||
|
||||
test "with valid token(downcase), it assigns the user", %{conn: conn} = opts do
|
||||
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]}")
|
||||
|> 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
|
||||
test "with valid token (downcase) in url parameters, it assigns the user", opts do
|
||||
conn =
|
||||
:get
|
||||
|> build_conn("/?access_token=#{opts[:token]}")
|
||||
|> build_conn("/?access_token=#{opts[:token].token}")
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> fetch_query_params()
|
||||
|> OAuthPlug.call(%{})
|
||||
|
@ -49,16 +55,16 @@ test "with valid token(downcase) in url parameters, it assigns the user", opts d
|
|||
assert conn.assigns[:user] == opts[:user]
|
||||
end
|
||||
|
||||
test "with valid token(downcase) in body parameters, it assigns the user", opts do
|
||||
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], status: "test")
|
||||
|> 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 not assigns the user", %{conn: conn} do
|
||||
test "with invalid token, it does not assign the user", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "bearer TTTTT")
|
||||
|
@ -67,14 +73,56 @@ test "with invalid token, it not assigns the user", %{conn: conn} do
|
|||
refute conn.assigns[:user]
|
||||
end
|
||||
|
||||
test "when token is missed but token in session, it assigns the user", %{conn: conn} = opts do
|
||||
conn =
|
||||
conn
|
||||
|> Plug.Session.call(Plug.Session.init(@session_opts))
|
||||
|> fetch_session()
|
||||
|> put_session(:oauth_token, opts[:token])
|
||||
|> OAuthPlug.call(%{})
|
||||
describe "with :oauth_token in session, " do
|
||||
setup %{token: oauth_token, conn: conn} do
|
||||
session_opts = [
|
||||
store: :cookie,
|
||||
key: "_test",
|
||||
signing_salt: "cooldude"
|
||||
]
|
||||
|
||||
assert conn.assigns[:user] == opts[:user]
|
||||
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
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.SessionAuthenticationPlugTest do
|
||||
use Pleroma.Web.ConnCase, async: true
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Plugs.SessionAuthenticationPlug
|
||||
|
||||
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
|
||||
|> assign(:auth_user, %User{id: 1})
|
||||
|
||||
%{conn: conn}
|
||||
end
|
||||
|
||||
test "it does nothing if a user is assigned", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:user, %User{})
|
||||
|
||||
ret_conn =
|
||||
conn
|
||||
|> SessionAuthenticationPlug.call(%{})
|
||||
|
||||
assert ret_conn == conn
|
||||
end
|
||||
|
||||
test "if the auth_user has the same id as the user_id in the session, it assigns the user", %{
|
||||
conn: conn
|
||||
} do
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:user_id, conn.assigns.auth_user.id)
|
||||
|> SessionAuthenticationPlug.call(%{})
|
||||
|
||||
assert conn.assigns.user == conn.assigns.auth_user
|
||||
end
|
||||
|
||||
test "if the auth_user has a different id as the user_id in the session, it does nothing", %{
|
||||
conn: conn
|
||||
} do
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:user_id, -1)
|
||||
|
||||
ret_conn =
|
||||
conn
|
||||
|> SessionAuthenticationPlug.call(%{})
|
||||
|
||||
assert ret_conn == conn
|
||||
end
|
||||
end
|
|
@ -5,7 +5,7 @@
|
|||
defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do
|
||||
use Pleroma.Web.ConnCase, async: true
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Helpers.AuthHelper
|
||||
alias Pleroma.Web.Plugs.SetUserSessionIdPlug
|
||||
|
||||
setup %{conn: conn} do
|
||||
|
@ -18,28 +18,26 @@ defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do
|
|||
conn =
|
||||
conn
|
||||
|> Plug.Session.call(Plug.Session.init(session_opts))
|
||||
|> fetch_session
|
||||
|> fetch_session()
|
||||
|
||||
%{conn: conn}
|
||||
end
|
||||
|
||||
test "doesn't do anything if the user isn't set", %{conn: conn} do
|
||||
ret_conn =
|
||||
conn
|
||||
|> SetUserSessionIdPlug.call(%{})
|
||||
ret_conn = SetUserSessionIdPlug.call(conn, %{})
|
||||
|
||||
assert ret_conn == conn
|
||||
end
|
||||
|
||||
test "sets the user_id in the session to the user id of the user assign", %{conn: conn} do
|
||||
Code.ensure_compiled(Pleroma.User)
|
||||
test "sets session token basing on :token assign", %{conn: conn} do
|
||||
%{user: user, token: oauth_token} = oauth_access(["read"])
|
||||
|
||||
conn =
|
||||
ret_conn =
|
||||
conn
|
||||
|> assign(:user, %User{id: 1})
|
||||
|> assign(:user, user)
|
||||
|> assign(:token, oauth_token)
|
||||
|> SetUserSessionIdPlug.call(%{})
|
||||
|
||||
id = get_session(conn, :user_id)
|
||||
assert id == 1
|
||||
assert AuthHelper.get_session_token(ret_conn) == oauth_token.token
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue