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:
lain 2020-12-09 15:55:45 +00:00
commit 477c6c8e55
45 changed files with 974 additions and 791 deletions

8
.gitattributes vendored
View file

@ -1,8 +1,10 @@
*.ex diff=elixir *.ex diff=elixir
*.exs 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 priv/static/instance/static.css diff=css
# to search/diff those as text files.
# 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 binary
*.js.map binary *.js.map binary
*.css binary *.css binary

View file

@ -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. - 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. - 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` - 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> <details>
<summary>API Changes</summary> <summary>API Changes</summary>

View file

@ -88,3 +88,8 @@ config :pleroma, :frontend_configurations,
Note the extra `static` folder for the terms-of-service.html 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`. 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.

View file

@ -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. 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. ## Auth-related configuration, OAuth consumer mode etc.

View file

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

View file

@ -2408,4 +2408,8 @@ def sanitize_html(%User{} = user, filter) do
|> Map.put(:bio, HTML.filter_tags(user.bio, filter)) |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
|> Map.put(:fields, fields) |> Map.put(:fields, fields)
end end
def get_host(%User{ap_id: ap_id} = _user) do
URI.parse(ap_id).host
end
end end

View file

@ -20,6 +20,7 @@ defmodule Pleroma.Web do
below. below.
""" """
alias Pleroma.Helpers.AuthHelper
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug alias Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug
@ -75,7 +76,7 @@ defp action(conn, params) do
defp maybe_drop_authentication_if_oauth_check_ignored(conn) do defp maybe_drop_authentication_if_oauth_check_ignored(conn) do
if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and
not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
OAuthScopesPlug.drop_auth_info(conn) AuthHelper.drop_auth_info(conn)
else else
conn conn
end end

View file

@ -6,6 +6,8 @@ defmodule Pleroma.Web.MastoFEController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.MastodonAPI.AuthController
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.OAuthScopesPlug
@ -26,8 +28,9 @@ defmodule Pleroma.Web.MastoFEController do
) )
@doc "GET /web/*path" @doc "GET /web/*path"
def index(%{assigns: %{user: user, token: token}} = conn, _params) def index(conn, _params) do
when not is_nil(user) and not is_nil(token) do 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 conn
|> put_layout(false) |> put_layout(false)
|> render("index.html", |> render("index.html",
@ -35,18 +38,17 @@ def index(%{assigns: %{user: user, token: token}} = conn, _params)
user: user, user: user,
custom_emojis: Pleroma.Emoji.get_all() custom_emojis: Pleroma.Emoji.get_all()
) )
end else
_ ->
def index(conn, _params) do
conn conn
|> put_session(:return_to, conn.request_path) |> put_session(:return_to, conn.request_path)
|> redirect(to: "/web/login") |> redirect(to: "/web/login")
end end
end
@doc "GET /web/manifest.json" @doc "GET /web/manifest.json"
def manifest(conn, _params) do def manifest(conn, _params) do
conn render(conn, "manifest.json")
|> render("manifest.json")
end end
@doc "PUT /api/web/settings: Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere" @doc "PUT /api/web/settings: Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere"

View file

@ -25,7 +25,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.MastodonAPI.MastodonAPIController alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.OAuthController alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter 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, user} <- TwitterAPI.register_user(params),
{_, {:ok, token}} <- {_, {:ok, token}} <-
{:login, OAuthController.login(user, app, app.scopes)} do {: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 else
{:login, {:account_status, :confirmation_pending}} -> {:login, {:account_status, :confirmation_pending}} ->
json_response(conn, :ok, %{ json_response(conn, :ok, %{

View file

@ -7,10 +7,13 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
import Pleroma.Web.ControllerHelper, only: [json_response: 3] import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.TwitterAPI.TwitterAPI
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@ -20,24 +23,35 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
@local_mastodon_name "Mastodon-Local" @local_mastodon_name "Mastodon-Local"
@doc "GET /web/login" @doc "GET /web/login"
def login(%{assigns: %{user: %User{}}} = conn, _params) do # Local Mastodon FE login callback action
redirect(conn, to: local_mastodon_root_path(conn)) def login(conn, %{"code" => auth_token} = params) do
end with {:ok, app} <- local_mastofe_app(),
# Local Mastodon FE login init action
def login(conn, %{"code" => auth_token}) do
with {:ok, app} <- get_or_make_app(),
{:ok, auth} <- Authorization.get_by_token(app, auth_token), {: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 conn
|> put_session(:oauth_token, token.token) |> local_mastodon_post_login_path()
|> redirect(to: local_mastodon_root_path(conn)) |> 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
end end
# Local Mastodon FE callback action def login(conn, params) do
def login(conn, _) do with %{assigns: %{user: %User{}, token: %Token{app_id: app_id}}} <- conn,
with {:ok, app} <- get_or_make_app() do {: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 = path =
o_auth_path(conn, :authorize, o_auth_path(conn, :authorize,
response_type: "code", response_type: "code",
@ -52,9 +66,16 @@ def login(conn, _) do
@doc "DELETE /auth/sign_out" @doc "DELETE /auth/sign_out"
def logout(conn, _) do def logout(conn, _) do
conn conn =
|> clear_session with %{assigns: %{token: %Token{} = oauth_token}} <- conn,
|> redirect(to: "/") 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 end
@doc "POST /auth/password" @doc "POST /auth/password"
@ -66,7 +87,7 @@ def password_reset(conn, params) do
json_response(conn, :no_content, "") json_response(conn, :no_content, "")
end end
defp local_mastodon_root_path(conn) do defp local_mastodon_post_login_path(conn) do
case get_session(conn, :return_to) do case get_session(conn, :return_to) do
nil -> nil ->
masto_fe_path(conn, :index, ["getting-started"]) masto_fe_path(conn, :index, ["getting-started"])
@ -77,9 +98,11 @@ defp local_mastodon_root_path(conn) do
end end
end end
@spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} @spec local_mastofe_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
defp get_or_make_app do def local_mastofe_app do
%{client_name: @local_mastodon_name, redirect_uris: "."} App.get_or_make(
|> App.get_or_make(["read", "write", "follow", "push", "admin"]) %{client_name: @local_mastodon_name, redirect_uris: "."},
["read", "write", "follow", "push", "admin"]
)
end end
end end

View file

@ -13,7 +13,6 @@ defmodule Pleroma.Web.OAuth.MFAController do
alias Pleroma.Web.Auth.TOTPAuthenticator alias Pleroma.Web.Auth.TOTPAuthenticator
alias Pleroma.Web.OAuth.MFAView, as: View alias Pleroma.Web.OAuth.MFAView, as: View
alias Pleroma.Web.OAuth.OAuthController alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
plug(:fetch_session when action in [:show, :verify]) 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, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
{:ok, _} <- validates_challenge(user, params), {:ok, _} <- validates_challenge(user, params),
{:ok, token} <- Token.exchange_token(app, auth) do {: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 else
_error -> _error ->
conn conn

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.OAuth.OAuthController do defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper alias Pleroma.Helpers.UriHelper
alias Pleroma.Maps alias Pleroma.Maps
alias Pleroma.MFA alias Pleroma.MFA
@ -79,6 +80,13 @@ defp do_authorize(%Plug.Conn{} = conn, params) do
available_scopes = (app && app.scopes) || [] available_scopes = (app && app.scopes) || []
scopes = Scopes.fetch_scopes(params, available_scopes) scopes = Scopes.fetch_scopes(params, available_scopes)
user =
with %{assigns: %{user: %User{} = user}} <- conn do
user
else
_ -> nil
end
scopes = scopes =
if scopes == [] do if scopes == [] do
available_scopes 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 # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
render(conn, Authenticator.auth_template(), %{ render(conn, Authenticator.auth_template(), %{
user: user,
app: app && Map.delete(app, :client_secret),
response_type: params["response_type"], response_type: params["response_type"],
client_id: params["client_id"], client_id: params["client_id"],
available_scopes: available_scopes, available_scopes: available_scopes,
@ -131,11 +141,13 @@ defp handle_existing_authorization(
end end
end end
def create_authorization( def create_authorization(_, _, opts \\ [])
%Plug.Conn{} = conn,
%{"authorization" => _} = params, def create_authorization(%Plug.Conn{assigns: %{user: %User{} = user}} = conn, params, []) do
opts \\ [] create_authorization(conn, params, user: user)
) do end
def create_authorization(%Plug.Conn{} = conn, %{"authorization" => _} = params, opts) do
with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]), with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
after_create_authorization(conn, auth, params) after_create_authorization(conn, auth, params)
@ -248,7 +260,7 @@ def token_exchange(
with {:ok, app} <- Token.Utils.fetch_app(conn), with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
{:ok, token} <- RefreshToken.grant(token) do {: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 else
_error -> render_invalid_credentials_error(conn) _error -> render_invalid_credentials_error(conn)
end end
@ -260,7 +272,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"}
{:ok, auth} <- Authorization.get_by_token(app, fixed_token), {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
%User{} = user <- User.get_cached_by_id(auth.user_id), %User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth) do {: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 else
error -> error ->
handle_token_exchange_error(conn, error) handle_token_exchange_error(conn, error)
@ -275,7 +287,7 @@ def token_exchange(
{:ok, app} <- Token.Utils.fetch_app(conn), {:ok, app} <- Token.Utils.fetch_app(conn),
requested_scopes <- Scopes.fetch_scopes(params, app.scopes), requested_scopes <- Scopes.fetch_scopes(params, app.scopes),
{:ok, token} <- login(user, app, requested_scopes) do {: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 else
error -> error ->
handle_token_exchange_error(conn, 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), with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, auth} <- Authorization.create_authorization(app, %User{}), {:ok, auth} <- Authorization.create_authorization(app, %User{}),
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
json(conn, OAuthView.render("token.json", %{token: token})) after_token_exchange(conn, %{token: token})
else else
_error -> _error ->
handle_token_exchange_error(conn, :invalid_credentails) handle_token_exchange_error(conn, :invalid_credentails)
@ -308,6 +320,12 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"}
# Bad request # Bad request
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) 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 defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do
conn conn
|> put_status(:forbidden) |> put_status(:forbidden)
@ -361,9 +379,17 @@ defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
render_invalid_credentials_error(conn) render_invalid_credentials_error(conn)
end end
def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do def token_revoke(%Plug.Conn{} = conn, %{"token" => token}) do
with {:ok, app} <- Token.Utils.fetch_app(conn), with {:ok, %Token{} = oauth_token} <- Token.get_by_token(token),
{:ok, _token} <- RevokeToken.revoke(app, params) 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, %{}) json(conn, %{})
else else
_error -> _error ->

View file

@ -27,6 +27,14 @@ defmodule Pleroma.Web.OAuth.Token do
timestamps() timestamps()
end 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" @doc "Gets token for app by access token"
@spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_token(%App{id: app_id} = _app, token) do def get_by_token(%App{id: app_id} = _app, token) do

View file

@ -5,21 +5,14 @@
defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug do defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug do
import Plug.Conn import Plug.Conn
alias Pleroma.Helpers.AuthHelper
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Web.Plugs.RateLimiter
def init(options) do def init(options) do
options options
end 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(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(conn, _) do def call(conn, _) do
@ -30,7 +23,7 @@ def call(conn, _) do
end end
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 if admin_token == secret_token() do
assign_admin_user(conn) assign_admin_user(conn)
else else
@ -38,7 +31,7 @@ def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
end end
end end
def authenticate(conn) do defp authenticate(conn) do
token = secret_token() token = secret_token()
case get_req_header(conn, "x-admin-token") do case get_req_header(conn, "x-admin-token") do
@ -48,10 +41,17 @@ def authenticate(conn) do
end end
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 defp assign_admin_user(conn) do
conn conn
|> assign(:user, %User{is_admin: true}) |> assign(:user, %User{is_admin: true})
|> OAuthScopesPlug.skip_plug() |> AuthHelper.skip_oauth()
end end
defp handle_bad_token(conn) do defp handle_bad_token(conn) do

View file

@ -3,6 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.AuthenticationPlug do defmodule Pleroma.Web.Plugs.AuthenticationPlug do
@moduledoc "Password authentication plug."
alias Pleroma.Helpers.AuthHelper
alias Pleroma.User alias Pleroma.User
import Plug.Conn import Plug.Conn
@ -11,6 +14,30 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do
def init(options), do: options 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 def checkpw(password, "$6" <> _ = password_hash) do
:crypt.crypt(password, password_hash) == password_hash :crypt.crypt(password, password_hash) == password_hash
end end
@ -40,40 +67,6 @@ def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do
def maybe_update_password(user, _), do: {:ok, user} def maybe_update_password(user, _), do: {:ok, user}
defp do_update_password(user, password) do defp do_update_password(user, password) do
user User.reset_password(user, %{password: password, password_confirmation: password})
|> User.password_update_changeset(%{
"password" => password,
"password_confirmation" => password
})
|> Pleroma.Repo.update()
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
end end
def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do
Pbkdf2.no_user_verify()
conn
end
def call(conn, _), do: conn
end

View file

@ -3,6 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.BasicAuthDecoderPlug do 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 import Plug.Conn
def init(options) do def init(options) do

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
alias Pleroma.Helpers.AuthHelper
alias Pleroma.Signature alias Pleroma.Signature
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
@ -12,6 +13,47 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
def init(options), do: options 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 defp key_id_from_conn(conn) do
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn), with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
{:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do {:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
@ -31,41 +73,4 @@ defp user_from_key_id(conn) do
nil nil
end end
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 end

View file

@ -3,9 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.OAuthPlug do defmodule Pleroma.Web.Plugs.OAuthPlug do
@moduledoc "Performs OAuth authentication by token from params / headers / cookies."
import Plug.Conn import Plug.Conn
import Ecto.Query import Ecto.Query
alias Pleroma.Helpers.AuthHelper
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.App
@ -17,45 +20,26 @@ def init(options), do: options
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn 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 def call(conn, _) do
case fetch_token_str(conn) do with {:ok, token_str} <- fetch_token_str(conn) do
{:ok, token} -> with {:ok, user, user_token} <- fetch_user_and_token(token_str),
with {:ok, user, token_record} <- fetch_user_and_token(token) do false <- Token.is_expired?(user_token) do
conn conn
|> assign(:token, token_record) |> assign(:token, user_token)
|> assign(:user, user) |> assign(:user, user)
else else
_ -> _ ->
# token found, but maybe only with app with {:ok, app, app_token} <- fetch_app_and_token(token_str),
with {:ok, app, token_record} <- fetch_app_and_token(token) do false <- Token.is_expired?(app_token) do
conn conn
|> assign(:token, token_record) |> assign(:token, app_token)
|> assign(:app, app) |> assign(:app, app)
else else
_ -> conn _ -> conn
end end
end end
else
_ -> _ -> conn
conn
end end
end end
@ -70,7 +54,6 @@ defp fetch_user_and_token(token) do
preload: [user: user] preload: [user: user]
) )
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
with %Token{user: user} = token_record <- Repo.one(query) do with %Token{user: user} = token_record <- Repo.one(query) do
{:ok, user, token_record} {:ok, user, token_record}
end end
@ -86,28 +69,22 @@ defp fetch_app_and_token(token) do
end end
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()} @spec fetch_token_str(Plug.Conn.t() | list(String.t())) :: :no_token_found | {:ok, String.t()}
defp fetch_token_from_session(conn) do defp fetch_token_str(%Plug.Conn{params: %{"access_token" => access_token}} = _conn) do
case get_session(conn, :oauth_token) do {:ok, access_token}
nil -> :no_token_found
token -> {:ok, token}
end
end 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 defp fetch_token_str(%Plug.Conn{} = conn) do
headers = get_req_header(conn, "authorization") headers = get_req_header(conn, "authorization")
with :no_token_found <- fetch_token_str(headers), with {:ok, token} <- fetch_token_str(headers) do
do: fetch_token_from_session(conn) {:ok, token}
else
_ -> fetch_token_from_session(conn)
end
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 defp fetch_token_str([token | tail]) do
trimmed_token = String.trim(token) trimmed_token = String.trim(token)
@ -117,4 +94,14 @@ defp fetch_token_str([token | tail]) do
_ -> fetch_token_str(tail) _ -> fetch_token_str(tail)
end end
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 end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.Plugs.OAuthScopesPlug do
import Pleroma.Web.Gettext import Pleroma.Web.Gettext
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Helpers.AuthHelper
use Pleroma.Web, :plug use Pleroma.Web, :plug
@ -28,7 +29,7 @@ def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
conn conn
options[:fallback] == :proceed_unauthenticated -> options[:fallback] == :proceed_unauthenticated ->
drop_auth_info(conn) AuthHelper.drop_auth_info(conn)
true -> true ->
missing_scopes = scopes -- matched_scopes missing_scopes = scopes -- matched_scopes
@ -44,15 +45,6 @@ def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
end end
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`" @doc "Keeps those of `scopes` which are descendants of `supported_scopes`"
def filter_descendants(scopes, supported_scopes) do def filter_descendants(scopes, supported_scopes) do
Enum.filter( Enum.filter(

View file

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

View file

@ -3,16 +3,15 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do
import Plug.Conn alias Pleroma.Helpers.AuthHelper
alias Pleroma.User alias Pleroma.Web.OAuth.Token
def init(opts) do def init(opts) do
opts opts
end end
def call(%{assigns: %{user: %User{id: id}}} = conn, _) do def call(%{assigns: %{token: %Token{} = oauth_token}} = conn, _) do
conn AuthHelper.put_session_token(conn, oauth_token.token)
|> put_session(:user_id, id)
end end
def call(conn, _), do: conn def call(conn, _), do: conn

View file

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.UserEnabledPlug do defmodule Pleroma.Web.Plugs.UserEnabledPlug do
import Plug.Conn alias Pleroma.Helpers.AuthHelper
alias Pleroma.User alias Pleroma.User
def init(options) do def init(options) do
@ -11,9 +11,10 @@ def init(options) do
end end
def call(%{assigns: %{user: %User{} = user}} = conn, _) do def call(%{assigns: %{user: %User{} = user}} = conn, _) do
case User.account_status(user) do if User.account_status(user) == :active do
:active -> conn conn
_ -> assign(conn, :user, nil) else
AuthHelper.drop_auth_info(conn)
end end
end end

View file

@ -3,6 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.UserFetcherPlug do 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 alias Pleroma.User
import Plug.Conn import Plug.Conn

View file

@ -34,6 +34,7 @@ defmodule Pleroma.Web.Router do
plug(:fetch_session) plug(:fetch_session)
plug(Pleroma.Web.Plugs.OAuthPlug) plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.UserEnabledPlug) plug(Pleroma.Web.Plugs.UserEnabledPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
end end
pipeline :expect_authentication do pipeline :expect_authentication do
@ -48,15 +49,13 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Web.Plugs.OAuthPlug) plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.BasicAuthDecoderPlug) plug(Pleroma.Web.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Web.Plugs.UserFetcherPlug) plug(Pleroma.Web.Plugs.UserFetcherPlug)
plug(Pleroma.Web.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Web.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Web.Plugs.AuthenticationPlug) plug(Pleroma.Web.Plugs.AuthenticationPlug)
end end
pipeline :after_auth do pipeline :after_auth do
plug(Pleroma.Web.Plugs.UserEnabledPlug) plug(Pleroma.Web.Plugs.UserEnabledPlug)
plug(Pleroma.Web.Plugs.SetUserSessionIdPlug) plug(Pleroma.Web.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Web.Plugs.EnsureUserKeyPlug) plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
end end
pipeline :base_api do pipeline :base_api do
@ -100,7 +99,7 @@ defmodule Pleroma.Web.Router do
pipeline :pleroma_html do pipeline :pleroma_html do
plug(:browser) plug(:browser)
plug(:authenticate) plug(:authenticate)
plug(Pleroma.Web.Plugs.EnsureUserKeyPlug) plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
end end
pipeline :well_known do pipeline :well_known do
@ -292,7 +291,6 @@ defmodule Pleroma.Web.Router do
post("/main/ostatus", UtilController, :remote_subscribe) post("/main/ostatus", UtilController, :remote_subscribe)
get("/ostatus_subscribe", RemoteFollowController, :follow) get("/ostatus_subscribe", RemoteFollowController, :follow)
post("/ostatus_subscribe", RemoteFollowController, :do_follow) post("/ostatus_subscribe", RemoteFollowController, :do_follow)
end end
@ -321,20 +319,26 @@ defmodule Pleroma.Web.Router do
end end
scope "/oauth", Pleroma.Web.OAuth do 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) get("/registration_details", OAuthController, :registration_details)
post("/mfa/challenge", MFAController, :challenge)
post("/mfa/verify", MFAController, :verify, as: :mfa_verify) post("/mfa/verify", MFAController, :verify, as: :mfa_verify)
get("/mfa", MFAController, :show) 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 scope [] do
pipe_through(:browser) pipe_through(:browser)

View file

@ -1,233 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" /> <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui">
<title> <title><%= Pleroma.Config.get([:instance, :name]) %></title>
<%= Pleroma.Config.get([:instance, :name]) %> <link rel="stylesheet" href="/instance/static.css">
</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>
</head> </head>
<body> <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"> <div class="container">
<h1><%= Pleroma.Config.get([:instance, :name]) %></h1>
<%= @inner_content %> <%= @inner_content %>
</div> </div>
</body> </body>

View file

@ -5,9 +5,31 @@
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p> <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<% end %> <% end %>
<h2>OAuth Authorization</h2>
<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %> <%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>
<%= 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>
<% 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 %> <%= if @params["registration"] in ["true", true] do %>
<h3>This is the first time you visit! Please enter your Pleroma handle.</h3> <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> <p>Choose carefully! You won't be able to change this later. You will be able to change your display name, though.</p>
@ -28,8 +50,9 @@
<%= password_input f, :password %> <%= password_input f, :password %>
</div> </div>
<%= submit "Log In" %> <%= submit "Log In" %>
<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
<% end %> <% end %>
<% end %>
</div>
<%= hidden_input f, :client_id, value: @client_id %> <%= hidden_input f, :client_id, value: @client_id %>
<%= hidden_input f, :response_type, value: @response_type %> <%= hidden_input f, :response_type, value: @response_type %>
@ -40,4 +63,3 @@
<%= if Pleroma.Config.oauth_consumer_enabled?() do %> <%= if Pleroma.Config.oauth_consumer_enabled?() do %>
<%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %> <%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %>
<% end %> <% end %>

View file

@ -0,0 +1,296 @@
* {
box-sizing: border-box;
}
:root {
--brand-color: #d8a070;
--background-color: #121a24;
--foreground-color: #182230;
--primary-text-color: #b9b9ba;
--muted-text-color: #89898a;
}
body {
background-color: var(--background-color);
font-family: sans-serif;
color: var(--primary-text-color);
padding: 0;
margin: 0;
}
.instance-header {
height: 60px;
padding: 10px;
background: var(--foreground-color);
box-shadow: 0 1px 4px 0px rgba(0, 0, 0, 0.5);
}
.instance-header__content {
display: flex;
align-items: center;
max-width: 400px;
margin: 0 auto;
}
.instance-header__thumbnail {
max-width: 40px;
border-radius: 4px;
margin-right: 12px;
}
.instance-header__title {
font-size: 16px;
font-weight: bold;
color: var(--primary-text-color);
}
.container {
max-width: 400px;
background-color: var(--foreground-color);
border-radius: 4px;
overflow: hidden;
margin: 35px auto;
box-shadow: 0 1px 4px 0px rgba(0, 0, 0, 0.5);
}
.container__content {
padding: 0 20px;
}
h1 {
margin: 0;
font-size: 24px;
text-align: center;
}
h2 {
color: var(--primary-text-color);
font-weight: normal;
font-size: 18px;
margin-bottom: 20px;
}
a {
color: var(--brand-color);
text-decoration: none;
}
form {
width: 100%;
}
.input {
color: var(--muted-text-color);
display: flex;
flex-direction: column;
}
input {
box-sizing: content-box;
padding: 10px;
margin-top: 5px;
margin-bottom: 10px;
background-color: var(--background-color);
color: var(--primary-text-color);
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: 1em 0;
color: var(--muted-text-color);
}
.scopes-input label:first-child {
height: 2em;
}
.scopes {
display: flex;
flex-wrap: wrap;
color: var(--primary-text-color);
}
.scope {
display: flex;
flex-basis: 100%;
height: 2em;
align-items: center;
}
.scope:before {
color: var(--primary-text-color);
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: var(--background-color);
border: 4px solid var(--background-color);
box-shadow: 0px 0px 1px 0 var(--brand-color);
width: 1.2em;
height: 1.2em;
margin-right: 1.0em;
content: "";
transition-property: background-color;
transition-duration: 0.35s;
color: var(--background-color);
margin-bottom: -0.2em;
border-radius: 2px;
}
[type="checkbox"]:checked + label:before {
background-color: var(--brand-color);
}
input:focus {
outline: none;
border-bottom: 2px solid var(--brand-color);
}
.actions {
display: flex;
justify-content: flex-end;
}
.actions button,
.actions a.button {
width: auto;
margin-left: 10px;
}
a.button,
button {
width: 100%;
background-color: #1c2a3a;
color: var(--primary-text-color);
border-radius: 4px;
border: none;
padding: 10px 16px;
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;
}
a.button:hover,
button:hover {
cursor: pointer;
box-shadow: 0px 0px 0px 1px var(--brand-color),
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
}
.alert-danger {
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 {
width: 100%;
border-radius: 4px;
border: 1px solid #7d796a;
padding: 10px;
margin-top: 20px;
font-weight: 500;
font-size: 16px;
}
.account-header__banner {
width: 100%;
height: 112px;
background-size: cover;
background-position: center;
}
.account-header__avatar {
width: 94px;
height: 94px;
background-size: cover;
background-position: center;
margin: -47px 10px 0;
border: 6px solid var(--foreground-color);
border-radius: 999px;
}
.account-header__meta {
padding: 6px 20px 17px;
}
.account-header__display-name {
font-size: 20px;
font-weight: bold;
}
.account-header__nickname {
font-size: 14px;
color: var(--muted-text-color);
}
@media all and (max-width: 420px) {
.container {
margin: 0 auto;
border-radius: 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 {
line-height: 47px;
flex: 1;
}
.form-row > input {
flex: 2;
}

View file

@ -2176,4 +2176,9 @@ test "avatar fallback" do
assert User.avatar_url(user, no_default: true) == nil assert User.avatar_url(user, no_default: true) == nil
end 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 end

View file

@ -941,7 +941,6 @@ test "it resend emails for two users", %{conn: conn, admin: admin} do
describe "/api/pleroma/admin/stats" do describe "/api/pleroma/admin/stats" do
test "status visibility count", %{conn: conn} do test "status visibility count", %{conn: conn} do
admin = insert(:user, is_admin: true)
user = insert(:user) user = insert(:user)
CommonAPI.post(user, %{visibility: "public", status: "hey"}) CommonAPI.post(user, %{visibility: "public", status: "hey"})
CommonAPI.post(user, %{visibility: "unlisted", status: "hey"}) CommonAPI.post(user, %{visibility: "unlisted", status: "hey"})
@ -949,7 +948,6 @@ test "status visibility count", %{conn: conn} do
response = response =
conn conn
|> assign(:user, admin)
|> get("/api/pleroma/admin/stats") |> get("/api/pleroma/admin/stats")
|> json_response(200) |> json_response(200)
@ -958,7 +956,6 @@ test "status visibility count", %{conn: conn} do
end end
test "by instance", %{conn: conn} do test "by instance", %{conn: conn} do
admin = insert(:user, is_admin: true)
user1 = insert(:user) user1 = insert(:user)
instance2 = "instance2.tld" instance2 = "instance2.tld"
user2 = insert(:user, %{ap_id: "https://#{instance2}/@actor"}) user2 = insert(:user, %{ap_id: "https://#{instance2}/@actor"})
@ -969,7 +966,6 @@ test "by instance", %{conn: conn} do
response = response =
conn conn
|> assign(:user, admin)
|> get("/api/pleroma/admin/stats", instance: instance2) |> get("/api/pleroma/admin/stats", instance: instance2)
|> json_response(200) |> json_response(200)

View file

@ -1417,11 +1417,7 @@ test "enables the welcome messages", %{conn: conn} do
describe "GET /api/pleroma/admin/config/descriptions" do describe "GET /api/pleroma/admin/config/descriptions" do
test "structure", %{conn: conn} do test "structure", %{conn: conn} do
admin = insert(:user, is_admin: true) conn = get(conn, "/api/pleroma/admin/config/descriptions")
conn =
assign(conn, :user, admin)
|> get("/api/pleroma/admin/config/descriptions")
assert [child | _others] = json_response_and_validate_schema(conn, 200) assert [child | _others] = json_response_and_validate_schema(conn, 200)
@ -1439,11 +1435,7 @@ test "filters by database configuration whitelist", %{conn: conn} do
{:esshd} {:esshd}
]) ])
admin = insert(:user, is_admin: true) conn = get(conn, "/api/pleroma/admin/config/descriptions")
conn =
assign(conn, :user, admin)
|> get("/api/pleroma/admin/config/descriptions")
children = json_response_and_validate_schema(conn, 200) children = json_response_and_validate_schema(conn, 200)

View file

@ -39,7 +39,7 @@ test "redirects to the saved path after log in", %{conn: conn, path: path} do
|> get("/web/login", %{code: auth.token}) |> get("/web/login", %{code: auth.token})
assert conn.status == 302 assert conn.status == 302
assert redirected_to(conn) == path assert redirected_to(conn) =~ path
end end
test "redirects to the getting-started page when referer is not present", %{conn: conn} do 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}) conn = get(conn, "/web/login", %{code: auth.token})
assert conn.status == 302 assert conn.status == 302
assert redirected_to(conn) == "/web/getting-started" assert redirected_to(conn) =~ "/web/getting-started"
end end
end end

View file

@ -64,7 +64,8 @@ test "redirects not logged-in users to the login page on private instances", %{
end end
test "does not redirect logged in users to the login page", %{conn: conn, path: path} do 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 =
conn conn

View file

@ -4,8 +4,10 @@
defmodule Pleroma.Web.OAuth.OAuthControllerTest do defmodule Pleroma.Web.OAuth.OAuthControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Helpers.AuthHelper
alias Pleroma.MFA alias Pleroma.MFA
alias Pleroma.MFA.TOTP alias Pleroma.MFA.TOTP
alias Pleroma.Repo alias Pleroma.Repo
@ -454,7 +456,7 @@ test "renders authentication page if user is already authenticated but `force_lo
conn = conn =
conn conn
|> put_session(:oauth_token, token.token) |> AuthHelper.put_session_token(token.token)
|> get( |> get(
"/oauth/authorize", "/oauth/authorize",
%{ %{
@ -478,7 +480,7 @@ test "renders authentication page if user is already authenticated but user requ
conn = conn =
conn conn
|> put_session(:oauth_token, token.token) |> AuthHelper.put_session_token(token.token)
|> get( |> get(
"/oauth/authorize", "/oauth/authorize",
%{ %{
@ -501,7 +503,7 @@ test "with existing authentication and non-OOB `redirect_uri`, redirects to app
conn = conn =
conn conn
|> put_session(:oauth_token, token.token) |> AuthHelper.put_session_token(token.token)
|> get( |> get(
"/oauth/authorize", "/oauth/authorize",
%{ %{
@ -527,7 +529,7 @@ test "with existing authentication and unlisted non-OOB `redirect_uri`, redirect
conn = conn =
conn conn
|> put_session(:oauth_token, token.token) |> AuthHelper.put_session_token(token.token)
|> get( |> get(
"/oauth/authorize", "/oauth/authorize",
%{ %{
@ -551,7 +553,7 @@ test "with existing authentication and OOB `redirect_uri`, redirects to app with
conn = conn =
conn conn
|> put_session(:oauth_token, token.token) |> AuthHelper.put_session_token(token.token)
|> get( |> get(
"/oauth/authorize", "/oauth/authorize",
%{ %{
@ -609,6 +611,41 @@ test "redirects with oauth authorization, " <>
end end
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 test "redirect to on two-factor auth page" do
otp_secret = TOTP.generate_secret() otp_secret = TOTP.generate_secret()
@ -1219,8 +1256,43 @@ test "returns 500" do
end end
end end
describe "POST /oauth/revoke - bad request" do describe "POST /oauth/revoke" do
test "returns 500" 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 = response =
build_conn() build_conn()
|> post("/oauth/revoke", %{}) |> post("/oauth/revoke", %{})

View file

@ -264,9 +264,10 @@ test "it returns the messages for a given chat", %{conn: conn, user: user} do
assert length(result) == 3 assert length(result) == 3
# Trying to get the chat of a different user # Trying to get the chat of a different user
other_user_chat = Chat.get(other_user.id, user.ap_id)
conn conn
|> assign(:user, other_user) |> get("/api/v1/pleroma/chats/#{other_user_chat.id}/messages")
|> get("/api/v1/pleroma/chats/#{chat.id}/messages")
|> json_response_and_validate_schema(404) |> json_response_and_validate_schema(404)
end end
end end

View file

@ -49,6 +49,7 @@ test "with `admin_token` query parameter", %{conn: conn} do
|> AdminSecretAuthenticationPlug.call(%{}) |> AdminSecretAuthenticationPlug.call(%{})
assert conn.assigns[:user].is_admin assert conn.assigns[:user].is_admin
assert conn.assigns[:token] == nil
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
end end
@ -69,6 +70,7 @@ test "with `x-admin-token` HTTP header", %{conn: conn} do
|> AdminSecretAuthenticationPlug.call(%{}) |> AdminSecretAuthenticationPlug.call(%{})
assert conn.assigns[:user].is_admin assert conn.assigns[:user].is_admin
assert conn.assigns[:token] == nil
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
end end
end end

View file

@ -48,6 +48,7 @@ test "with a correct password in the credentials, " <>
|> AuthenticationPlug.call(%{}) |> AuthenticationPlug.call(%{})
assert conn.assigns.user == conn.assigns.auth_user assert conn.assigns.user == conn.assigns.auth_user
assert conn.assigns.token == nil
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
end end
@ -62,6 +63,7 @@ test "with a bcrypt hash, it updates to a pkbdf2 hash", %{conn: conn} do
|> AuthenticationPlug.call(%{}) |> AuthenticationPlug.call(%{})
assert conn.assigns.user.id == conn.assigns.auth_user.id assert conn.assigns.user.id == conn.assigns.auth_user.id
assert conn.assigns.token == nil
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
user = User.get_by_id(user.id) 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(%{}) |> AuthenticationPlug.call(%{})
assert conn.assigns.user.id == conn.assigns.auth_user.id assert conn.assigns.user.id == conn.assigns.auth_user.id
assert conn.assigns.token == nil
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
user = User.get_by_id(user.id) user = User.get_by_id(user.id)

View file

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

View file

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

View file

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

View file

@ -5,34 +5,40 @@
defmodule Pleroma.Web.Plugs.OAuthPlugTest do defmodule Pleroma.Web.Plugs.OAuthPlugTest do
use Pleroma.Web.ConnCase, async: true 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 Pleroma.Web.Plugs.OAuthPlug
import Pleroma.Factory alias Plug.Session
@session_opts [ import Pleroma.Factory
store: :cookie,
key: "_test",
signing_salt: "cooldude"
]
setup %{conn: conn} do setup %{conn: conn} do
user = insert(:user) user = insert(:user)
{:ok, %{token: token}} = Pleroma.Web.OAuth.Token.create(insert(:oauth_app), user) {:ok, oauth_token} = Token.create(insert(:oauth_app), user)
%{user: user, token: token, conn: conn} %{user: user, token: oauth_token, conn: conn}
end 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 =
conn conn
|> put_req_header("authorization", "BEARER #{opts[:token]}") |> put_req_header("authorization", "BEARER #{opts[:token].token}")
|> OAuthPlug.call(%{}) |> OAuthPlug.call(%{})
assert conn.assigns[:user] == opts[:user] assert conn.assigns[:user] == opts[:user]
end 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 =
conn conn
|> put_req_header("authorization", "bearer #{opts[:token]}") |> put_req_header("authorization", "bearer #{opts[:token].token}")
|> OAuthPlug.call(%{}) |> OAuthPlug.call(%{})
assert conn.assigns[:user] == opts[:user] assert conn.assigns[:user] == opts[:user]
@ -41,7 +47,7 @@ test "with valid token(downcase), it assigns the user", %{conn: conn} = opts do
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 = conn =
:get :get
|> build_conn("/?access_token=#{opts[:token]}") |> build_conn("/?access_token=#{opts[:token].token}")
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> fetch_query_params() |> fetch_query_params()
|> OAuthPlug.call(%{}) |> OAuthPlug.call(%{})
@ -52,13 +58,13 @@ test "with valid token(downcase) in url parameters, it assigns the user", opts d
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 = conn =
:post :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(%{}) |> OAuthPlug.call(%{})
assert conn.assigns[:user] == opts[:user] assert conn.assigns[:user] == opts[:user]
end 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 =
conn conn
|> put_req_header("authorization", "bearer TTTTT") |> 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] refute conn.assigns[:user]
end end
test "when token is missed but token in session, it assigns the user", %{conn: conn} = opts do 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 =
conn conn
|> Plug.Session.call(Plug.Session.init(@session_opts)) |> Session.call(Session.init(session_opts))
|> fetch_session() |> fetch_session()
|> put_session(:oauth_token, opts[:token]) |> AuthHelper.put_session_token(oauth_token.token)
|> OAuthPlug.call(%{})
assert conn.assigns[:user] == opts[:user] %{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
end end

View file

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

View file

@ -5,7 +5,7 @@
defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do
use Pleroma.Web.ConnCase, async: true use Pleroma.Web.ConnCase, async: true
alias Pleroma.User alias Pleroma.Helpers.AuthHelper
alias Pleroma.Web.Plugs.SetUserSessionIdPlug alias Pleroma.Web.Plugs.SetUserSessionIdPlug
setup %{conn: conn} do setup %{conn: conn} do
@ -18,28 +18,26 @@ defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do
conn = conn =
conn conn
|> Plug.Session.call(Plug.Session.init(session_opts)) |> Plug.Session.call(Plug.Session.init(session_opts))
|> fetch_session |> fetch_session()
%{conn: conn} %{conn: conn}
end end
test "doesn't do anything if the user isn't set", %{conn: conn} do test "doesn't do anything if the user isn't set", %{conn: conn} do
ret_conn = ret_conn = SetUserSessionIdPlug.call(conn, %{})
conn
|> SetUserSessionIdPlug.call(%{})
assert ret_conn == conn assert ret_conn == conn
end end
test "sets the user_id in the session to the user id of the user assign", %{conn: conn} do test "sets session token basing on :token assign", %{conn: conn} do
Code.ensure_compiled(Pleroma.User) %{user: user, token: oauth_token} = oauth_access(["read"])
conn = ret_conn =
conn conn
|> assign(:user, %User{id: 1}) |> assign(:user, user)
|> assign(:token, oauth_token)
|> SetUserSessionIdPlug.call(%{}) |> SetUserSessionIdPlug.call(%{})
id = get_session(conn, :user_id) assert AuthHelper.get_session_token(ret_conn) == oauth_token.token
assert id == 1
end end
end end