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 @@ defmodule Pleroma.User 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 @@ defmodule Pleroma.Web 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,27 +28,27 @@ defmodule Pleroma.Web.MastoFEController do
) )
@doc "GET /web/*path" @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 def index(conn, _params) do
conn with %{assigns: %{user: %User{} = user, token: %Token{app_id: token_app_id} = token}} <- conn,
|> put_session(:return_to, conn.request_path) {:ok, %{id: ^token_app_id}} <- AuthController.local_mastofe_app() do
|> redirect(to: "/web/login") 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 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 @@ defmodule Pleroma.Web.MastodonAPI.AccountController 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
|> local_mastodon_post_login_path()
|> UriHelper.modify_uri_params(%{"access_token" => oauth_token.token})
conn conn
|> put_session(:oauth_token, token.token) |> AuthHelper.put_session_token(oauth_token.token)
|> redirect(to: local_mastodon_root_path(conn)) |> 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 @@ defmodule Pleroma.Web.MastodonAPI.AuthController 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 @@ defmodule Pleroma.Web.MastodonAPI.AuthController 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 @@ defmodule Pleroma.Web.MastodonAPI.AuthController 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 @@ defmodule Pleroma.Web.OAuth.MFAController 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 @@ defmodule Pleroma.Web.OAuth.OAuthController 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 @@ defmodule Pleroma.Web.OAuth.OAuthController 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 @@ defmodule Pleroma.Web.OAuth.OAuthController do
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 @@ defmodule Pleroma.Web.OAuth.OAuthController do
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 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{: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 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{: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 @@ defmodule Pleroma.Web.OAuth.OAuthController do
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 @@ defmodule Pleroma.Web.OAuth.OAuthController do
# 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 @@ defmodule Pleroma.Web.OAuth.OAuthController 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 @@ defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug 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 @@ defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug 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 @@ defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlug 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 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug 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 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 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 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug 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 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
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
|> 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
_ ->
conn 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
end end
@ -70,7 +54,6 @@ defmodule Pleroma.Web.Plugs.OAuthPlug 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,29 +69,23 @@ defmodule Pleroma.Web.Plugs.OAuthPlug 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 @@ defmodule Pleroma.Web.Plugs.OAuthPlug 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 @@ defmodule Pleroma.Web.Plugs.OAuthScopesPlug 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 @@ defmodule Pleroma.Web.Plugs.OAuthScopesPlug 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 @@ defmodule Pleroma.Web.Plugs.UserEnabledPlug 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,32 +5,55 @@
<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 @params["registration"] in ["true", true] do %> <%= if @user do %>
<h3>This is the first time you visit! Please enter your Pleroma handle.</h3> <div class="account-header">
<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="account-header__banner" style="background-image: url('<%= Pleroma.User.banner_url(@user) %>')"></div>
<div class="input"> <div class="account-header__avatar" style="background-image: url('<%= Pleroma.User.avatar_url(@user) %>')"></div>
<%= label f, :nickname, "Pleroma Handle" %> <div class="account-header__meta">
<%= text_input f, :nickname, placeholder: "lain" %> <div class="account-header__display-name"><%= @user.name %></div>
<div class="account-header__nickname">@<%= @user.nickname %>@<%= Pleroma.User.get_host(@user) %></div>
</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 %> <% 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, :client_id, value: @client_id %>
<%= hidden_input f, :response_type, value: @response_type %> <%= hidden_input f, :response_type, value: @response_type %>
<%= hidden_input f, :redirect_uri, value: @redirect_uri %> <%= hidden_input f, :redirect_uri, value: @redirect_uri %>
@ -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 @@ defmodule Pleroma.UserTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest 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 @@ defmodule Pleroma.Web.MastodonAPI.AuthControllerTest 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 @@ defmodule Pleroma.Web.MastodonAPI.AuthControllerTest do
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 @@ defmodule Pleroma.Web.MastodonAPI.MastoFEControllerTest do
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 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
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 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
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 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
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 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
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 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
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 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
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 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest 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 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest 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 @@ defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlugTest 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 @@ defmodule Pleroma.Web.Plugs.AdminSecretAuthenticationPlugTest 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 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlugTest do
|> 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 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlugTest 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 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlugTest 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,43 +5,49 @@
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]
end 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 = 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(%{})
@ -49,16 +55,16 @@ defmodule Pleroma.Web.Plugs.OAuthPlugTest do
assert conn.assigns[:user] == opts[:user] assert conn.assigns[:user] == opts[:user]
end 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 = 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 @@ defmodule Pleroma.Web.Plugs.OAuthPlugTest 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
conn = setup %{token: oauth_token, conn: conn} do
conn session_opts = [
|> Plug.Session.call(Plug.Session.init(@session_opts)) store: :cookie,
|> fetch_session() key: "_test",
|> put_session(:oauth_token, opts[:token]) signing_salt: "cooldude"
|> OAuthPlug.call(%{}) ]
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
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